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