UNPKG

8.02 kBJavaScriptView Raw
1(function () {
2
3 if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) {
4 return;
5 }
6
7 /**
8 * @param {string} selector
9 * @param {ParentNode} [container]
10 * @returns {HTMLElement[]}
11 */
12 function $$(selector, container) {
13 return Array.prototype.slice.call((container || document).querySelectorAll(selector));
14 }
15
16 /**
17 * Returns whether the given element has the given class.
18 *
19 * @param {Element} element
20 * @param {string} className
21 * @returns {boolean}
22 */
23 function hasClass(element, className) {
24 className = " " + className + " ";
25 return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1
26 }
27
28 /**
29 * Calls the given function.
30 *
31 * @param {() => any} func
32 * @returns {void}
33 */
34 function callFunction(func) {
35 func();
36 }
37
38 // Some browsers round the line-height, others don't.
39 // We need to test for it to position the elements properly.
40 var isLineHeightRounded = (function () {
41 var res;
42 return function () {
43 if (typeof res === 'undefined') {
44 var d = document.createElement('div');
45 d.style.fontSize = '13px';
46 d.style.lineHeight = '1.5';
47 d.style.padding = '0';
48 d.style.border = '0';
49 d.innerHTML = '&nbsp;<br />&nbsp;';
50 document.body.appendChild(d);
51 // Browsers that round the line-height should have offsetHeight === 38
52 // The others should have 39.
53 res = d.offsetHeight === 38;
54 document.body.removeChild(d);
55 }
56 return res;
57 }
58 }());
59
60 /**
61 * Highlights the lines of the given pre.
62 *
63 * This function is split into a DOM measuring and mutate phase to improve performance.
64 * The returned function mutates the DOM when called.
65 *
66 * @param {HTMLElement} pre
67 * @param {string} [lines]
68 * @param {string} [classes='']
69 * @returns {() => void}
70 */
71 function highlightLines(pre, lines, classes) {
72 lines = typeof lines === 'string' ? lines : pre.getAttribute('data-line');
73
74 var ranges = lines.replace(/\s+/g, '').split(',').filter(Boolean);
75 var offset = +pre.getAttribute('data-line-offset') || 0;
76
77 var parseMethod = isLineHeightRounded() ? parseInt : parseFloat;
78 var lineHeight = parseMethod(getComputedStyle(pre).lineHeight);
79 var hasLineNumbers = hasClass(pre, 'line-numbers');
80 var parentElement = hasLineNumbers ? pre : pre.querySelector('code') || pre;
81 var mutateActions = /** @type {(() => void)[]} */ ([]);
82
83 ranges.forEach(function (currentRange) {
84 var range = currentRange.split('-');
85
86 var start = +range[0];
87 var end = +range[1] || start;
88
89 /** @type {HTMLElement} */
90 var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div');
91
92 mutateActions.push(function () {
93 line.setAttribute('aria-hidden', 'true');
94 line.setAttribute('data-range', currentRange);
95 line.className = (classes || '') + ' line-highlight';
96 });
97
98 // if the line-numbers plugin is enabled, then there is no reason for this plugin to display the line numbers
99 if (hasLineNumbers && Prism.plugins.lineNumbers) {
100 var startNode = Prism.plugins.lineNumbers.getLine(pre, start);
101 var endNode = Prism.plugins.lineNumbers.getLine(pre, end);
102
103 if (startNode) {
104 var top = startNode.offsetTop + 'px';
105 mutateActions.push(function () {
106 line.style.top = top;
107 });
108 }
109
110 if (endNode) {
111 var height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px';
112 mutateActions.push(function () {
113 line.style.height = height;
114 });
115 }
116 } else {
117 mutateActions.push(function () {
118 line.setAttribute('data-start', start);
119
120 if (end > start) {
121 line.setAttribute('data-end', end);
122 }
123
124 line.style.top = (start - offset - 1) * lineHeight + 'px';
125
126 line.textContent = new Array(end - start + 2).join(' \n');
127 });
128 }
129
130 mutateActions.push(function () {
131 // allow this to play nicely with the line-numbers plugin
132 // need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning
133 parentElement.appendChild(line);
134 });
135 });
136
137 var id = pre.id;
138 if (hasLineNumbers && id) {
139 // This implements linkable line numbers. Linkable line numbers use Line Highlight to create a link to a
140 // specific line. For this to work, the pre element has to:
141 // 1) have line numbers,
142 // 2) have the `linkable-line-numbers` class or an ascendant that has that class, and
143 // 3) have an id.
144
145 var linkableLineNumbersClass = 'linkable-line-numbers';
146 var linkableLineNumbers = false;
147 var node = pre;
148 while (node) {
149 if (hasClass(node, linkableLineNumbersClass)) {
150 linkableLineNumbers = true;
151 break;
152 }
153 node = node.parentElement;
154 }
155
156 if (linkableLineNumbers) {
157 if (!hasClass(pre, linkableLineNumbersClass)) {
158 // add class to pre
159 mutateActions.push(function () {
160 pre.className = (pre.className + ' ' + linkableLineNumbersClass).trim();
161 });
162 }
163
164 var start = parseInt(pre.getAttribute('data-start') || '1');
165
166 // iterate all line number spans
167 $$('.line-numbers-rows > span', pre).forEach(function (lineSpan, i) {
168 var lineNumber = i + start;
169 lineSpan.onclick = function () {
170 var hash = id + '.' + lineNumber;
171
172 // this will prevent scrolling since the span is obviously in view
173 scrollIntoView = false;
174 location.hash = hash;
175 setTimeout(function () {
176 scrollIntoView = true;
177 }, 1);
178 };
179 });
180 }
181 }
182
183 return function () {
184 mutateActions.forEach(callFunction);
185 };
186 }
187
188 var scrollIntoView = true;
189 function applyHash() {
190 var hash = location.hash.slice(1);
191
192 // Remove pre-existing temporary lines
193 $$('.temporary.line-highlight').forEach(function (line) {
194 line.parentNode.removeChild(line);
195 });
196
197 var range = (hash.match(/\.([\d,-]+)$/) || [, ''])[1];
198
199 if (!range || document.getElementById(hash)) {
200 return;
201 }
202
203 var id = hash.slice(0, hash.lastIndexOf('.')),
204 pre = document.getElementById(id);
205
206 if (!pre) {
207 return;
208 }
209
210 if (!pre.hasAttribute('data-line')) {
211 pre.setAttribute('data-line', '');
212 }
213
214 var mutateDom = highlightLines(pre, range, 'temporary ');
215 mutateDom();
216
217 if (scrollIntoView) {
218 document.querySelector('.temporary.line-highlight').scrollIntoView();
219 }
220 }
221
222 var fakeTimer = 0; // Hack to limit the number of times applyHash() runs
223
224 Prism.hooks.add('before-sanity-check', function (env) {
225 var pre = env.element.parentNode;
226 var lines = pre && pre.getAttribute('data-line');
227
228 if (!pre || !lines || !/pre/i.test(pre.nodeName)) {
229 return;
230 }
231
232 /*
233 * Cleanup for other plugins (e.g. autoloader).
234 *
235 * Sometimes <code> blocks are highlighted multiple times. It is necessary
236 * to cleanup any left-over tags, because the whitespace inside of the <div>
237 * tags change the content of the <code> tag.
238 */
239 var num = 0;
240 $$('.line-highlight', pre).forEach(function (line) {
241 num += line.textContent.length;
242 line.parentNode.removeChild(line);
243 });
244 // Remove extra whitespace
245 if (num && /^( \n)+$/.test(env.code.slice(-num))) {
246 env.code = env.code.slice(0, -num);
247 }
248 });
249
250 Prism.hooks.add('complete', function completeHook(env) {
251 var pre = env.element.parentNode;
252 var lines = pre && pre.getAttribute('data-line');
253
254 if (!pre || !lines || !/pre/i.test(pre.nodeName)) {
255 return;
256 }
257
258 clearTimeout(fakeTimer);
259
260 var hasLineNumbers = Prism.plugins.lineNumbers;
261 var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers;
262
263 if (hasClass(pre, 'line-numbers') && hasLineNumbers && !isLineNumbersLoaded) {
264 Prism.hooks.add('line-numbers', completeHook);
265 } else {
266 var mutateDom = highlightLines(pre, lines);
267 mutateDom();
268 fakeTimer = setTimeout(applyHash, 1);
269 }
270 });
271
272 window.addEventListener('hashchange', applyHash);
273 window.addEventListener('resize', function () {
274 var actions = $$('pre[data-line]').map(function (pre) {
275 return highlightLines(pre);
276 });
277 actions.forEach(callFunction);
278 });
279
280})();