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