UNPKG

9.46 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 * Returns the top offset of the content box of the given parent and the content box of one of its children.
62 *
63 * @param {HTMLElement} parent
64 * @param {HTMLElement} child
65 */
66 function getContentBoxTopOffset(parent, child) {
67 var parentStyle = getComputedStyle(parent);
68 var childStyle = getComputedStyle(child);
69
70 /**
71 * Returns the numeric value of the given pixel value.
72 *
73 * @param {string} px
74 */
75 function pxToNumber(px) {
76 return +px.substr(0, px.length - 2);
77 }
78
79 return child.offsetTop
80 + pxToNumber(childStyle.borderTopWidth)
81 + pxToNumber(childStyle.paddingTop)
82 - pxToNumber(parentStyle.paddingTop);
83 }
84
85 /**
86 * Highlights the lines of the given pre.
87 *
88 * This function is split into a DOM measuring and mutate phase to improve performance.
89 * The returned function mutates the DOM when called.
90 *
91 * @param {HTMLElement} pre
92 * @param {string | null} [lines]
93 * @param {string} [classes='']
94 * @returns {() => void}
95 */
96 function highlightLines(pre, lines, classes) {
97 lines = typeof lines === 'string' ? lines : pre.getAttribute('data-line');
98
99 var ranges = lines.replace(/\s+/g, '').split(',').filter(Boolean);
100 var offset = +pre.getAttribute('data-line-offset') || 0;
101
102 var parseMethod = isLineHeightRounded() ? parseInt : parseFloat;
103 var lineHeight = parseMethod(getComputedStyle(pre).lineHeight);
104 var hasLineNumbers = hasClass(pre, 'line-numbers');
105 var codeElement = pre.querySelector('code');
106 var parentElement = hasLineNumbers ? pre : codeElement || pre;
107 var mutateActions = /** @type {(() => void)[]} */ ([]);
108
109 /**
110 * The top offset between the content box of the <code> element and the content box of the parent element of
111 * the line highlight element (either `<pre>` or `<code>`).
112 *
113 * This offset might not be zero for some themes where the <code> element has a top margin. Some plugins
114 * (or users) might also add element above the <code> element. Because the line highlight is aligned relative
115 * to the <pre> element, we have to take this into account.
116 *
117 * This offset will be 0 if the parent element of the line highlight element is the `<code>` element.
118 */
119 var codePreOffset = !codeElement || parentElement == codeElement ? 0 : getContentBoxTopOffset(pre, codeElement);
120
121 ranges.forEach(function (currentRange) {
122 var range = currentRange.split('-');
123
124 var start = +range[0];
125 var end = +range[1] || start;
126
127 /** @type {HTMLElement} */
128 var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div');
129
130 mutateActions.push(function () {
131 line.setAttribute('aria-hidden', 'true');
132 line.setAttribute('data-range', currentRange);
133 line.className = (classes || '') + ' line-highlight';
134 });
135
136 // if the line-numbers plugin is enabled, then there is no reason for this plugin to display the line numbers
137 if (hasLineNumbers && Prism.plugins.lineNumbers) {
138 var startNode = Prism.plugins.lineNumbers.getLine(pre, start);
139 var endNode = Prism.plugins.lineNumbers.getLine(pre, end);
140
141 if (startNode) {
142 var top = startNode.offsetTop + codePreOffset + 'px';
143 mutateActions.push(function () {
144 line.style.top = top;
145 });
146 }
147
148 if (endNode) {
149 var height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px';
150 mutateActions.push(function () {
151 line.style.height = height;
152 });
153 }
154 } else {
155 mutateActions.push(function () {
156 line.setAttribute('data-start', String(start));
157
158 if (end > start) {
159 line.setAttribute('data-end', String(end));
160 }
161
162 line.style.top = (start - offset - 1) * lineHeight + codePreOffset + 'px';
163
164 line.textContent = new Array(end - start + 2).join(' \n');
165 });
166 }
167
168 mutateActions.push(function () {
169 // allow this to play nicely with the line-numbers plugin
170 // need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning
171 parentElement.appendChild(line);
172 });
173 });
174
175 var id = pre.id;
176 if (hasLineNumbers && id) {
177 // This implements linkable line numbers. Linkable line numbers use Line Highlight to create a link to a
178 // specific line. For this to work, the pre element has to:
179 // 1) have line numbers,
180 // 2) have the `linkable-line-numbers` class or an ascendant that has that class, and
181 // 3) have an id.
182
183 var linkableLineNumbersClass = 'linkable-line-numbers';
184 var linkableLineNumbers = false;
185 var node = pre;
186 while (node) {
187 if (hasClass(node, linkableLineNumbersClass)) {
188 linkableLineNumbers = true;
189 break;
190 }
191 node = node.parentElement;
192 }
193
194 if (linkableLineNumbers) {
195 if (!hasClass(pre, linkableLineNumbersClass)) {
196 // add class to pre
197 mutateActions.push(function () {
198 pre.className = (pre.className + ' ' + linkableLineNumbersClass).trim();
199 });
200 }
201
202 var start = parseInt(pre.getAttribute('data-start') || '1');
203
204 // iterate all line number spans
205 $$('.line-numbers-rows > span', pre).forEach(function (lineSpan, i) {
206 var lineNumber = i + start;
207 lineSpan.onclick = function () {
208 var hash = id + '.' + lineNumber;
209
210 // this will prevent scrolling since the span is obviously in view
211 scrollIntoView = false;
212 location.hash = hash;
213 setTimeout(function () {
214 scrollIntoView = true;
215 }, 1);
216 };
217 });
218 }
219 }
220
221 return function () {
222 mutateActions.forEach(callFunction);
223 };
224 }
225
226 var scrollIntoView = true;
227 function applyHash() {
228 var hash = location.hash.slice(1);
229
230 // Remove pre-existing temporary lines
231 $$('.temporary.line-highlight').forEach(function (line) {
232 line.parentNode.removeChild(line);
233 });
234
235 var range = (hash.match(/\.([\d,-]+)$/) || [, ''])[1];
236
237 if (!range || document.getElementById(hash)) {
238 return;
239 }
240
241 var id = hash.slice(0, hash.lastIndexOf('.')),
242 pre = document.getElementById(id);
243
244 if (!pre) {
245 return;
246 }
247
248 if (!pre.hasAttribute('data-line')) {
249 pre.setAttribute('data-line', '');
250 }
251
252 var mutateDom = highlightLines(pre, range, 'temporary ');
253 mutateDom();
254
255 if (scrollIntoView) {
256 document.querySelector('.temporary.line-highlight').scrollIntoView();
257 }
258 }
259
260 var fakeTimer = 0; // Hack to limit the number of times applyHash() runs
261
262 Prism.hooks.add('before-sanity-check', function (env) {
263 var pre = env.element.parentElement;
264 var lines = pre && pre.getAttribute('data-line');
265
266 if (!pre || !lines || !/pre/i.test(pre.nodeName)) {
267 return;
268 }
269
270 /*
271 * Cleanup for other plugins (e.g. autoloader).
272 *
273 * Sometimes <code> blocks are highlighted multiple times. It is necessary
274 * to cleanup any left-over tags, because the whitespace inside of the <div>
275 * tags change the content of the <code> tag.
276 */
277 var num = 0;
278 $$('.line-highlight', pre).forEach(function (line) {
279 num += line.textContent.length;
280 line.parentNode.removeChild(line);
281 });
282 // Remove extra whitespace
283 if (num && /^( \n)+$/.test(env.code.slice(-num))) {
284 env.code = env.code.slice(0, -num);
285 }
286 });
287
288 Prism.hooks.add('complete', function completeHook(env) {
289 var pre = env.element.parentElement;
290 var lines = pre && pre.getAttribute('data-line');
291
292 if (!pre || !lines || !/pre/i.test(pre.nodeName)) {
293 return;
294 }
295
296 clearTimeout(fakeTimer);
297
298 var hasLineNumbers = Prism.plugins.lineNumbers;
299 var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers;
300
301 if (hasClass(pre, 'line-numbers') && hasLineNumbers && !isLineNumbersLoaded) {
302 Prism.hooks.add('line-numbers', completeHook);
303 } else {
304 var mutateDom = highlightLines(pre, lines);
305 mutateDom();
306 fakeTimer = setTimeout(applyHash, 1);
307 }
308 });
309
310 window.addEventListener('hashchange', applyHash);
311 window.addEventListener('resize', function () {
312 var actions = $$('pre[data-line]').map(function (pre) {
313 return highlightLines(pre);
314 });
315 actions.forEach(callFunction);
316 });
317
318})();