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