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