]> BookStack Code Mirror - bookstack/blob - resources/js/code.mjs
Refactored common list handling operations to new class
[bookstack] / resources / js / code.mjs
1 import CodeMirror from "codemirror";
2 import Clipboard from "clipboard/dist/clipboard.min";
3
4 // Modes
5 import 'codemirror/mode/css/css';
6 import 'codemirror/mode/clike/clike';
7 import 'codemirror/mode/diff/diff';
8 import 'codemirror/mode/fortran/fortran';
9 import 'codemirror/mode/go/go';
10 import 'codemirror/mode/haskell/haskell';
11 import 'codemirror/mode/htmlmixed/htmlmixed';
12 import 'codemirror/mode/javascript/javascript';
13 import 'codemirror/mode/julia/julia';
14 import 'codemirror/mode/lua/lua';
15 import 'codemirror/mode/markdown/markdown';
16 import 'codemirror/mode/mllike/mllike';
17 import 'codemirror/mode/nginx/nginx';
18 import 'codemirror/mode/octave/octave';
19 import 'codemirror/mode/perl/perl';
20 import 'codemirror/mode/pascal/pascal';
21 import 'codemirror/mode/php/php';
22 import 'codemirror/mode/powershell/powershell';
23 import 'codemirror/mode/properties/properties';
24 import 'codemirror/mode/python/python';
25 import 'codemirror/mode/ruby/ruby';
26 import 'codemirror/mode/rust/rust';
27 import 'codemirror/mode/shell/shell';
28 import 'codemirror/mode/sql/sql';
29 import 'codemirror/mode/stex/stex';
30 import 'codemirror/mode/toml/toml';
31 import 'codemirror/mode/vb/vb';
32 import 'codemirror/mode/vbscript/vbscript';
33 import 'codemirror/mode/xml/xml';
34 import 'codemirror/mode/yaml/yaml';
35
36 // Addons
37 import 'codemirror/addon/scroll/scrollpastend';
38
39 // Mapping of possible languages or formats from user input to their codemirror modes.
40 // Value can be a mode string or a function that will receive the code content & return the mode string.
41 // The function option is used in the event the exact mode could be dynamic depending on the code.
42 const modeMap = {
43     bash: 'shell',
44     css: 'css',
45     c: 'text/x-csrc',
46     java: 'text/x-java',
47     scala: 'text/x-scala',
48     kotlin: 'text/x-kotlin',
49     'c++': 'text/x-c++src',
50     'c#': 'text/x-csharp',
51     csharp: 'text/x-csharp',
52     diff: 'diff',
53     for: 'fortran',
54     fortran: 'fortran',
55     'f#': 'text/x-fsharp',
56     fsharp: 'text/x-fsharp',
57     go: 'go',
58     haskell: 'haskell',
59     hs: 'haskell',
60     html: 'htmlmixed',
61     ini: 'properties',
62     javascript: 'text/javascript',
63     json: 'application/json',
64     js: 'text/javascript',
65     jl: 'text/x-julia',
66     julia: 'text/x-julia',
67     latex: 'text/x-stex',
68     lua: 'lua',
69     matlab: 'text/x-octave',
70     md: 'markdown',
71     mdown: 'markdown',
72     markdown: 'markdown',
73     ml: 'mllike',
74     nginx: 'nginx',
75     octave: 'text/x-octave',
76     perl: 'perl',
77     pl: 'perl',
78     powershell: 'powershell',
79     properties: 'properties',
80     ocaml: 'text/x-ocaml',
81     pascal: 'text/x-pascal',
82     pas: 'text/x-pascal',
83     php: (content) => {
84         return content.includes('<?php') ? 'php' : 'text/x-php';
85     },
86     py: 'python',
87     python: 'python',
88     ruby: 'ruby',
89     rust: 'rust',
90     rb: 'ruby',
91     rs: 'rust',
92     shell: 'shell',
93     sh: 'shell',
94     stext: 'text/x-stex',
95     toml: 'toml',
96     ts: 'text/typescript',
97     typescript: 'text/typescript',
98     sql: 'text/x-sql',
99     vbs: 'vbscript',
100     vbscript: 'vbscript',
101     'vb.net': 'text/x-vb',
102     vbnet: 'text/x-vb',
103     xml: 'xml',
104     yaml: 'yaml',
105     yml: 'yaml',
106 };
107
108 /**
109  * Highlight pre elements on a page
110  */
111 export function highlight() {
112     const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
113     for (const codeBlock of codeBlocks) {
114         highlightElem(codeBlock);
115     }
116 }
117
118 /**
119  * Highlight all code blocks within the given parent element
120  * @param {HTMLElement} parent
121  */
122 export function highlightWithin(parent) {
123     const codeBlocks = parent.querySelectorAll('pre');
124     for (const codeBlock of codeBlocks) {
125         highlightElem(codeBlock);
126     }
127 }
128
129 /**
130  * Add code highlighting to a single element.
131  * @param {HTMLElement} elem
132  */
133 function highlightElem(elem) {
134     const innerCodeElem = elem.querySelector('code[class^=language-]');
135     elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
136     const content = elem.textContent.trimEnd();
137
138     let mode = '';
139     if (innerCodeElem !== null) {
140         const langName = innerCodeElem.className.replace('language-', '');
141         mode = getMode(langName, content);
142     }
143
144     const cm = CodeMirror(function(elt) {
145         elem.parentNode.replaceChild(elt, elem);
146     }, {
147         value: content,
148         mode:  mode,
149         lineNumbers: true,
150         lineWrapping: false,
151         theme: getTheme(),
152         readOnly: true
153     });
154
155     addCopyIcon(cm);
156 }
157
158 /**
159  * Add a button to a CodeMirror instance which copies the contents to the clipboard upon click.
160  * @param cmInstance
161  */
162 function addCopyIcon(cmInstance) {
163     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>`;
164     const copyButton = document.createElement('div');
165     copyButton.classList.add('CodeMirror-copy');
166     copyButton.innerHTML = copyIcon;
167     cmInstance.display.wrapper.appendChild(copyButton);
168
169     const clipboard = new Clipboard(copyButton, {
170         text: function(trigger) {
171             return cmInstance.getValue()
172         }
173     });
174
175     clipboard.on('success', event => {
176         copyButton.classList.add('success');
177         setTimeout(() => {
178             copyButton.classList.remove('success');
179         }, 240);
180     });
181 }
182
183 /**
184  * Search for a codemirror code based off a user suggestion
185  * @param {String} suggestion
186  * @param {String} content
187  * @returns {string}
188  */
189 function getMode(suggestion, content) {
190     suggestion = suggestion.trim().replace(/^\./g, '').toLowerCase();
191
192     const modeMapType = typeof modeMap[suggestion];
193
194     if (modeMapType === 'undefined') {
195         return '';
196     }
197
198     if (modeMapType === 'function') {
199         return modeMap[suggestion](content);
200     }
201
202     return modeMap[suggestion];
203 }
204
205 /**
206  * Ge the theme to use for CodeMirror instances.
207  * @returns {*|string}
208  */
209 function getTheme() {
210     const darkMode = document.documentElement.classList.contains('dark-mode');
211     return window.codeTheme || (darkMode ? 'darcula' : 'default');
212 }
213
214 /**
215  * Create a CodeMirror instance for showing inside the WYSIWYG editor.
216  *  Manages a textarea element to hold code content.
217  * @param {HTMLElement} cmContainer
218  * @param {String} content
219  * @param {String} language
220  * @returns {{wrap: Element, editor: *}}
221  */
222 export function wysiwygView(cmContainer, content, language) {
223     return CodeMirror(cmContainer, {
224         value: content,
225         mode: getMode(language, content),
226         lineNumbers: true,
227         lineWrapping: false,
228         theme: getTheme(),
229         readOnly: true
230     });
231 }
232
233
234 /**
235  * Create a CodeMirror instance to show in the WYSIWYG pop-up editor
236  * @param {HTMLElement} elem
237  * @param {String} modeSuggestion
238  * @returns {*}
239  */
240 export function popupEditor(elem, modeSuggestion) {
241     const content = elem.textContent;
242
243     return CodeMirror(function(elt) {
244         elem.parentNode.insertBefore(elt, elem);
245         elem.style.display = 'none';
246     }, {
247         value: content,
248         mode:  getMode(modeSuggestion, content),
249         lineNumbers: true,
250         lineWrapping: false,
251         theme: getTheme()
252     });
253 }
254
255 /**
256  * Create an inline editor to replace the given textarea.
257  * @param {HTMLTextAreaElement} textArea
258  * @param {String} mode
259  * @returns {CodeMirror3}
260  */
261 export function inlineEditor(textArea, mode) {
262     return CodeMirror.fromTextArea(textArea, {
263         mode: getMode(mode, textArea.value),
264         lineNumbers: true,
265         lineWrapping: false,
266         theme: getTheme(),
267     });
268 }
269
270 /**
271  * Set the mode of a codemirror instance.
272  * @param cmInstance
273  * @param modeSuggestion
274  */
275 export function setMode(cmInstance, modeSuggestion, content) {
276       cmInstance.setOption('mode', getMode(modeSuggestion, content));
277 }
278
279 /**
280  * Set the content of a cm instance.
281  * @param cmInstance
282  * @param codeContent
283  */
284 export function setContent(cmInstance, codeContent) {
285     cmInstance.setValue(codeContent);
286     setTimeout(() => {
287         updateLayout(cmInstance);
288     }, 10);
289 }
290
291 /**
292  * Update the layout (codemirror refresh) of a cm instance.
293  * @param cmInstance
294  */
295 export function updateLayout(cmInstance) {
296     cmInstance.refresh();
297 }
298
299 /**
300  * Get a CodeMirror instance to use for the markdown editor.
301  * @param {HTMLElement} elem
302  * @returns {*}
303  */
304 export function markdownEditor(elem) {
305     const content = elem.textContent;
306     const config = {
307         value: content,
308         mode: "markdown",
309         lineNumbers: true,
310         lineWrapping: true,
311         theme: getTheme(),
312         scrollPastEnd: true,
313     };
314
315     window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config});
316
317     return CodeMirror(function (elt) {
318         elem.parentNode.insertBefore(elt, elem);
319         elem.style.display = 'none';
320     }, config);
321 }
322
323 /**
324  * Get the 'meta' key dependent on the user's system.
325  * @returns {string}
326  */
327 export function getMetaKey() {
328     let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
329     return mac ? "Cmd" : "Ctrl";
330 }