highlight-line-number.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. // jshint multistr:true
  2. let TABLE_NAME = 'hljs-ln',
  3. LINE_NAME = 'hljs-ln-line',
  4. CODE_BLOCK_NAME = 'hljs-ln-code',
  5. NUMBERS_BLOCK_NAME = 'hljs-ln-numbers',
  6. NUMBER_LINE_NAME = 'hljs-ln-n',
  7. DATA_ATTR_NAME = 'data-line-number',
  8. BREAK_LINE_REGEXP = /\r\n|\r|\n/g;
  9. addStyles();
  10. function addStyles() {
  11. let css = document.createElement('style');
  12. css.type = 'text/css';
  13. css.innerHTML = format('.{0}{border-collapse:collapse}' + '.{0} td{padding:0}' + '.{1}:before{content:attr({2})}', [
  14. TABLE_NAME,
  15. NUMBER_LINE_NAME,
  16. DATA_ATTR_NAME,
  17. ]);
  18. document.getElementsByTagName('head')[0].appendChild(css);
  19. }
  20. function initLineNumbersOnLoad(options) {
  21. if (document.readyState === 'interactive' || document.readyState === 'complete') {
  22. documentReady(options);
  23. } else {
  24. window.addEventListener('DOMContentLoaded', function () {
  25. documentReady(options);
  26. });
  27. }
  28. }
  29. function documentReady(options) {
  30. try {
  31. let blocks = document.querySelectorAll('code.hljs,code.nohighlight');
  32. for (let i in blocks) {
  33. // eslint-disable-next-line no-prototype-builtins
  34. if (blocks.hasOwnProperty(i)) {
  35. if (!isPluginDisabledForBlock(blocks[i])) {
  36. lineNumbersBlock(blocks[i], options);
  37. }
  38. }
  39. }
  40. } catch (e) {
  41. window.console.error('LineNumbers error: ', e);
  42. }
  43. }
  44. function isPluginDisabledForBlock(element) {
  45. return element.classList.contains('nohljsln');
  46. }
  47. function lineNumbersBlock(element, options) {
  48. if (typeof element !== 'object') return;
  49. element.innerHTML = lineNumbersInternal(element, options);
  50. }
  51. function lineNumbersInternal(element, options) {
  52. let internalOptions = mapOptions(element, options);
  53. duplicateMultilineNodes(element);
  54. return addLineNumbersBlockFor(element.innerHTML, internalOptions);
  55. }
  56. function addLineNumbersBlockFor(inputHtml, options) {
  57. let lines = getLines(inputHtml);
  58. // if last line contains only carriage return remove it
  59. if (lines[lines.length - 1].trim() === '') {
  60. lines.pop();
  61. }
  62. if (lines.length > 1 || options.singleLine) {
  63. let html = '';
  64. for (let i = 0, l = lines.length; i < l; i++) {
  65. html += format(
  66. '<tr>' +
  67. '<td class="{0} {1}" {3}="{5}">' +
  68. '<div class="{2}" {3}="{5}"></div>' +
  69. '</td>' +
  70. '<td class="{0} {4}" {3}="{5}">' +
  71. '{6}' +
  72. '</td>' +
  73. '</tr>',
  74. [
  75. LINE_NAME,
  76. NUMBERS_BLOCK_NAME,
  77. NUMBER_LINE_NAME,
  78. DATA_ATTR_NAME,
  79. CODE_BLOCK_NAME,
  80. i + options.startFrom,
  81. lines[i].length > 0 ? lines[i] : ' ',
  82. ]
  83. );
  84. }
  85. return format('<table class="{0}">{1}</table>', [TABLE_NAME, html]);
  86. }
  87. return inputHtml;
  88. }
  89. /**
  90. * @param {HTMLElement} element Code block.
  91. * @param {Object} options External API options.
  92. * @returns {Object} Internal API options.
  93. */
  94. function mapOptions(element, options) {
  95. options = options || {};
  96. return {
  97. singleLine: getSingleLineOption(options),
  98. startFrom: getStartFromOption(element, options),
  99. };
  100. }
  101. function getSingleLineOption(options) {
  102. let defaultValue = false;
  103. if (options.singleLine) {
  104. return options.singleLine;
  105. }
  106. return defaultValue;
  107. }
  108. function getStartFromOption(element, options) {
  109. let defaultValue = 1;
  110. let startFrom = defaultValue;
  111. if (isFinite(options.startFrom)) {
  112. startFrom = options.startFrom;
  113. }
  114. // can be overridden because local option is priority
  115. let value = getAttribute(element, 'data-ln-start-from');
  116. if (value !== null) {
  117. startFrom = toNumber(value, defaultValue);
  118. }
  119. return startFrom;
  120. }
  121. /**
  122. * Recursive method for fix multi-line elements implementation in highlight.js
  123. * Doing deep passage on child nodes.
  124. * @param {HTMLElement} element
  125. */
  126. function duplicateMultilineNodes(element) {
  127. let nodes = element.childNodes;
  128. for (let node in nodes) {
  129. // eslint-disable-next-line no-prototype-builtins
  130. if (nodes.hasOwnProperty(node)) {
  131. let child = nodes[node];
  132. if (getLinesCount(child.textContent) > 0) {
  133. if (child.childNodes.length > 0) {
  134. duplicateMultilineNodes(child);
  135. } else {
  136. duplicateMultilineNode(child.parentNode);
  137. }
  138. }
  139. }
  140. }
  141. }
  142. /**
  143. * Method for fix multi-line elements implementation in highlight.js
  144. * @param {HTMLElement} element
  145. */
  146. function duplicateMultilineNode(element) {
  147. let className = element.className;
  148. if (!/hljs-/.test(className)) return;
  149. let lines = getLines(element.innerHTML);
  150. for (var i = 0, result = ''; i < lines.length; i++) {
  151. let lineText = lines[i].length > 0 ? lines[i] : ' ';
  152. result += format('<span class="{0}">{1}</span>\n', [className, lineText]);
  153. }
  154. element.innerHTML = result.trim();
  155. }
  156. function getLines(text) {
  157. if (text.length === 0) return [];
  158. return text.split(BREAK_LINE_REGEXP);
  159. }
  160. function getLinesCount(text) {
  161. return (text.trim().match(BREAK_LINE_REGEXP) || []).length;
  162. }
  163. /**
  164. * {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript}
  165. * @param {string} format
  166. * @param {array} args
  167. */
  168. function format(format, args) {
  169. return format.replace(/\{(\d+)\}/g, function (m, n) {
  170. return args[n] !== undefined ? args[n] : m;
  171. });
  172. }
  173. /**
  174. * @param {HTMLElement} element Code block.
  175. * @param {String} attrName Attribute name.
  176. * @returns {String} Attribute value or empty.
  177. */
  178. function getAttribute(element, attrName) {
  179. return element.hasAttribute(attrName) ? element.getAttribute(attrName) : null;
  180. }
  181. /**
  182. * @param {String} str Source string.
  183. * @param {Number} fallback Fallback value.
  184. * @returns Parsed number or fallback value.
  185. */
  186. function toNumber(str, fallback) {
  187. if (!str) return fallback;
  188. let number = Number(str);
  189. return isFinite(number) ? number : fallback;
  190. }
  191. export { lineNumbersBlock, initLineNumbersOnLoad };