prism-line-highlight.js 7.8 KB

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