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