prism-line-numbers.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. (function () {
  2. if (typeof Prism === 'undefined' || typeof document === 'undefined') {
  3. return;
  4. }
  5. /**
  6. * Plugin name which is used as a class name for <pre> which is activating the plugin
  7. *
  8. * @type {string}
  9. */
  10. var PLUGIN_NAME = 'line-numbers';
  11. /**
  12. * Regular expression used for determining line breaks
  13. *
  14. * @type {RegExp}
  15. */
  16. var NEW_LINE_EXP = /\n(?!$)/g;
  17. /**
  18. * Global exports
  19. */
  20. var config = Prism.plugins.lineNumbers = {
  21. /**
  22. * Get node for provided line number
  23. *
  24. * @param {Element} element pre element
  25. * @param {number} number line number
  26. * @returns {Element|undefined}
  27. */
  28. getLine: function (element, number) {
  29. if (element.tagName !== 'PRE' || !element.classList.contains(PLUGIN_NAME)) {
  30. return;
  31. }
  32. var lineNumberRows = element.querySelector('.line-numbers-rows');
  33. if (!lineNumberRows) {
  34. return;
  35. }
  36. var lineNumberStart = parseInt(element.getAttribute('data-start'), 10) || 1;
  37. var lineNumberEnd = lineNumberStart + (lineNumberRows.children.length - 1);
  38. if (number < lineNumberStart) {
  39. number = lineNumberStart;
  40. }
  41. if (number > lineNumberEnd) {
  42. number = lineNumberEnd;
  43. }
  44. var lineIndex = number - lineNumberStart;
  45. return lineNumberRows.children[lineIndex];
  46. },
  47. /**
  48. * Resizes the line numbers of the given element.
  49. *
  50. * This function will not add line numbers. It will only resize existing ones.
  51. *
  52. * @param {HTMLElement} element A `<pre>` element with line numbers.
  53. * @returns {void}
  54. */
  55. resize: function (element) {
  56. resizeElements([element]);
  57. },
  58. /**
  59. * Whether the plugin can assume that the units font sizes and margins are not depended on the size of
  60. * the current viewport.
  61. *
  62. * Setting this to `true` will allow the plugin to do certain optimizations for better performance.
  63. *
  64. * Set this to `false` if you use any of the following CSS units: `vh`, `vw`, `vmin`, `vmax`.
  65. *
  66. * @type {boolean}
  67. */
  68. assumeViewportIndependence: true
  69. };
  70. /**
  71. * Resizes the given elements.
  72. *
  73. * @param {HTMLElement[]} elements
  74. */
  75. function resizeElements(elements) {
  76. elements = elements.filter(function (e) {
  77. var codeStyles = getStyles(e);
  78. var whiteSpace = codeStyles['white-space'];
  79. return whiteSpace === 'pre-wrap' || whiteSpace === 'pre-line';
  80. });
  81. if (elements.length == 0) {
  82. return;
  83. }
  84. var infos = elements.map(function (element) {
  85. var codeElement = element.querySelector('code');
  86. var lineNumbersWrapper = element.querySelector('.line-numbers-rows');
  87. if (!codeElement || !lineNumbersWrapper) {
  88. return undefined;
  89. }
  90. /** @type {HTMLElement} */
  91. var lineNumberSizer = element.querySelector('.line-numbers-sizer');
  92. var codeLines = codeElement.textContent.split(NEW_LINE_EXP);
  93. if (!lineNumberSizer) {
  94. lineNumberSizer = document.createElement('span');
  95. lineNumberSizer.className = 'line-numbers-sizer';
  96. codeElement.appendChild(lineNumberSizer);
  97. }
  98. lineNumberSizer.innerHTML = '0';
  99. lineNumberSizer.style.display = 'block';
  100. var oneLinerHeight = lineNumberSizer.getBoundingClientRect().height;
  101. lineNumberSizer.innerHTML = '';
  102. return {
  103. element: element,
  104. lines: codeLines,
  105. lineHeights: [],
  106. oneLinerHeight: oneLinerHeight,
  107. sizer: lineNumberSizer,
  108. };
  109. }).filter(Boolean);
  110. infos.forEach(function (info) {
  111. var lineNumberSizer = info.sizer;
  112. var lines = info.lines;
  113. var lineHeights = info.lineHeights;
  114. var oneLinerHeight = info.oneLinerHeight;
  115. lineHeights[lines.length - 1] = undefined;
  116. lines.forEach(function (line, index) {
  117. if (line && line.length > 1) {
  118. var e = lineNumberSizer.appendChild(document.createElement('span'));
  119. e.style.display = 'block';
  120. e.textContent = line;
  121. } else {
  122. lineHeights[index] = oneLinerHeight;
  123. }
  124. });
  125. });
  126. infos.forEach(function (info) {
  127. var lineNumberSizer = info.sizer;
  128. var lineHeights = info.lineHeights;
  129. var childIndex = 0;
  130. for (var i = 0; i < lineHeights.length; i++) {
  131. if (lineHeights[i] === undefined) {
  132. lineHeights[i] = lineNumberSizer.children[childIndex++].getBoundingClientRect().height;
  133. }
  134. }
  135. });
  136. infos.forEach(function (info) {
  137. var lineNumberSizer = info.sizer;
  138. var wrapper = info.element.querySelector('.line-numbers-rows');
  139. lineNumberSizer.style.display = 'none';
  140. lineNumberSizer.innerHTML = '';
  141. info.lineHeights.forEach(function (height, lineNumber) {
  142. wrapper.children[lineNumber].style.height = height + 'px';
  143. });
  144. });
  145. }
  146. /**
  147. * Returns style declarations for the element
  148. *
  149. * @param {Element} element
  150. */
  151. function getStyles(element) {
  152. if (!element) {
  153. return null;
  154. }
  155. return window.getComputedStyle ? getComputedStyle(element) : (element.currentStyle || null);
  156. }
  157. var lastWidth = undefined;
  158. window.addEventListener('resize', function () {
  159. if (config.assumeViewportIndependence && lastWidth === window.innerWidth) {
  160. return;
  161. }
  162. lastWidth = window.innerWidth;
  163. resizeElements(Array.prototype.slice.call(document.querySelectorAll('pre.' + PLUGIN_NAME)));
  164. });
  165. Prism.hooks.add('complete', function (env) {
  166. if (!env.code) {
  167. return;
  168. }
  169. var code = /** @type {Element} */ (env.element);
  170. var pre = /** @type {HTMLElement} */ (code.parentNode);
  171. // works only for <code> wrapped inside <pre> (not inline)
  172. if (!pre || !/pre/i.test(pre.nodeName)) {
  173. return;
  174. }
  175. // Abort if line numbers already exists
  176. if (code.querySelector('.line-numbers-rows')) {
  177. return;
  178. }
  179. // only add line numbers if <code> or one of its ancestors has the `line-numbers` class
  180. if (!Prism.util.isActive(code, PLUGIN_NAME)) {
  181. return;
  182. }
  183. // Remove the class 'line-numbers' from the <code>
  184. code.classList.remove(PLUGIN_NAME);
  185. // Add the class 'line-numbers' to the <pre>
  186. pre.classList.add(PLUGIN_NAME);
  187. var match = env.code.match(NEW_LINE_EXP);
  188. var linesNum = match ? match.length + 1 : 1;
  189. var lineNumbersWrapper;
  190. var lines = new Array(linesNum + 1).join('<span></span>');
  191. lineNumbersWrapper = document.createElement('span');
  192. lineNumbersWrapper.setAttribute('aria-hidden', 'true');
  193. lineNumbersWrapper.className = 'line-numbers-rows';
  194. lineNumbersWrapper.innerHTML = lines;
  195. if (pre.hasAttribute('data-start')) {
  196. pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1);
  197. }
  198. env.element.appendChild(lineNumbersWrapper);
  199. resizeElements([pre]);
  200. Prism.hooks.run('line-numbers', env);
  201. });
  202. Prism.hooks.add('line-numbers', function (env) {
  203. env.plugins = env.plugins || {};
  204. env.plugins.lineNumbers = true;
  205. });
  206. }());