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