UNPKG

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