]> BookStack Code Mirror - bookstack/blob - resources/js/code/index.mjs
Started codemirror update, In broken state
[bookstack] / resources / js / code / index.mjs
1 import {EditorView} from "@codemirror/view"
2 // import {EditorState} from "@codemirror/state"
3 import Clipboard from "clipboard/dist/clipboard.min";
4
5 // Modes
6 import {modes, modeMap, modesAsStreamLanguages} from "./modes";
7 import {viewer} from "./setups.js";
8
9 /**
10  * Highlight pre elements on a page
11  */
12 export function highlight() {
13     const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
14     for (const codeBlock of codeBlocks) {
15         highlightElem(codeBlock);
16     }
17 }
18
19 /**
20  * Highlight all code blocks within the given parent element
21  * @param {HTMLElement} parent
22  */
23 export function highlightWithin(parent) {
24     const codeBlocks = parent.querySelectorAll('pre');
25     for (const codeBlock of codeBlocks) {
26         highlightElem(codeBlock);
27     }
28 }
29
30 /**
31  * Add code highlighting to a single element.
32  * @param {HTMLElement} elem
33  */
34 function highlightElem(elem) {
35     const innerCodeElem = elem.querySelector('code[class^=language-]');
36     elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
37     const content = elem.textContent.trimEnd();
38
39     let mode = '';
40     if (innerCodeElem !== null) {
41         const langName = innerCodeElem.className.replace('language-', '');
42         mode = getMode(langName, content);
43     }
44
45     const wrapper = document.createElement('div');
46     elem.parentNode.insertBefore(wrapper, elem);
47
48     const cm = new EditorView({
49         parent: wrapper,
50         doc: content,
51         extensions: viewer(),
52     });
53
54     elem.remove();
55
56     // TODO - theme: getTheme(),
57     // TODO - mode,
58     addCopyIcon(cm);
59 }
60
61 /**
62  * Add a button to a CodeMirror instance which copies the contents to the clipboard upon click.
63  * @param cmInstance
64  */
65 function addCopyIcon(cmInstance) {
66     // TODO
67     // const copyIcon = `<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
68     // const copyButton = document.createElement('div');
69     // copyButton.classList.add('CodeMirror-copy');
70     // copyButton.innerHTML = copyIcon;
71     // cmInstance.display.wrapper.appendChild(copyButton);
72     //
73     // const clipboard = new Clipboard(copyButton, {
74     //     text: function(trigger) {
75     //         return cmInstance.getValue()
76     //     }
77     // });
78     //
79     // clipboard.on('success', event => {
80     //     copyButton.classList.add('success');
81     //     setTimeout(() => {
82     //         copyButton.classList.remove('success');
83     //     }, 240);
84     // });
85 }
86
87 /**
88  * Search for a codemirror code based off a user suggestion
89  * @param {String} suggestion
90  * @param {String} content
91  * @returns {string}
92  */
93 function getMode(suggestion, content) {
94     suggestion = suggestion.trim().replace(/^\./g, '').toLowerCase();
95
96     const modeMapType = typeof modeMap[suggestion];
97
98     if (modeMapType === 'undefined') {
99         return '';
100     }
101
102     if (modeMapType === 'function') {
103         return modeMap[suggestion](content);
104     }
105
106     return modeMap[suggestion];
107 }
108
109 /**
110  * Ge the theme to use for CodeMirror instances.
111  * @returns {*|string}
112  */
113 function getTheme() {
114     const darkMode = document.documentElement.classList.contains('dark-mode');
115     return window.codeTheme || (darkMode ? 'darcula' : 'default');
116 }
117
118 /**
119  * Create a CodeMirror instance for showing inside the WYSIWYG editor.
120  *  Manages a textarea element to hold code content.
121  * @param {HTMLElement} cmContainer
122  * @param {String} content
123  * @param {String} language
124  * @returns {{wrap: Element, editor: *}}
125  */
126 export function wysiwygView(cmContainer, content, language) {
127     return CodeMirror(cmContainer, {
128         value: content,
129         mode: getMode(language, content),
130         lineNumbers: true,
131         lineWrapping: false,
132         theme: getTheme(),
133         readOnly: true
134     });
135 }
136
137
138 /**
139  * Create a CodeMirror instance to show in the WYSIWYG pop-up editor
140  * @param {HTMLElement} elem
141  * @param {String} modeSuggestion
142  * @returns {*}
143  */
144 export function popupEditor(elem, modeSuggestion) {
145     const content = elem.textContent;
146
147     return CodeMirror(function(elt) {
148         elem.parentNode.insertBefore(elt, elem);
149         elem.style.display = 'none';
150     }, {
151         value: content,
152         mode:  getMode(modeSuggestion, content),
153         lineNumbers: true,
154         lineWrapping: false,
155         theme: getTheme()
156     });
157 }
158
159 /**
160  * Create an inline editor to replace the given textarea.
161  * @param {HTMLTextAreaElement} textArea
162  * @param {String} mode
163  * @returns {CodeMirror3}
164  */
165 export function inlineEditor(textArea, mode) {
166     return CodeMirror.fromTextArea(textArea, {
167         mode: getMode(mode, textArea.value),
168         lineNumbers: true,
169         lineWrapping: false,
170         theme: getTheme(),
171     });
172 }
173
174 /**
175  * Set the mode of a codemirror instance.
176  * @param cmInstance
177  * @param modeSuggestion
178  */
179 export function setMode(cmInstance, modeSuggestion, content) {
180       cmInstance.setOption('mode', getMode(modeSuggestion, content));
181 }
182
183 /**
184  * Set the content of a cm instance.
185  * @param cmInstance
186  * @param codeContent
187  */
188 export function setContent(cmInstance, codeContent) {
189     cmInstance.setValue(codeContent);
190     setTimeout(() => {
191         updateLayout(cmInstance);
192     }, 10);
193 }
194
195 /**
196  * Update the layout (codemirror refresh) of a cm instance.
197  * @param cmInstance
198  */
199 export function updateLayout(cmInstance) {
200     cmInstance.refresh();
201 }
202
203 /**
204  * Get a CodeMirror instance to use for the markdown editor.
205  * @param {HTMLElement} elem
206  * @returns {*}
207  */
208 export function markdownEditor(elem) {
209     const content = elem.textContent;
210     const config = {
211         value: content,
212         mode: "markdown",
213         lineNumbers: true,
214         lineWrapping: true,
215         theme: getTheme(),
216         scrollPastEnd: true,
217     };
218
219     window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config});
220
221     return CodeMirror(function (elt) {
222         elem.parentNode.insertBefore(elt, elem);
223         elem.style.display = 'none';
224     }, config);
225 }
226
227 /**
228  * Get the 'meta' key dependent on the user's system.
229  * @returns {string}
230  */
231 export function getMetaKey() {
232     let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
233     return mac ? "Cmd" : "Ctrl";
234 }