prism-line-highlight.js 10 KB

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