UNPKG

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