highlight.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. 'use strict';
  2. const hljs = require('highlight.js');
  3. const stripIndent = require('strip-indent');
  4. const alias = require('../highlight_alias.json');
  5. function highlightUtil(str, options = {}) {
  6. if (typeof str !== 'string') throw new TypeError('str must be a string!');
  7. str = stripIndent(str);
  8. const useHljs = Object.prototype.hasOwnProperty.call(options, 'hljs') ? options.hljs : false;
  9. const {
  10. gutter = true,
  11. firstLine = 1,
  12. caption,
  13. mark = [],
  14. languageAttr = false,
  15. tab
  16. } = options;
  17. let { wrap = true } = options;
  18. hljs.configure({ classPrefix: useHljs ? 'hljs-' : ''});
  19. const data = highlight(str, options);
  20. const lang = options.lang || data.language || '';
  21. const classNames = (useHljs ? 'hljs' : 'highlight') + (lang ? ` ${lang}` : '');
  22. if (gutter && !wrap) wrap = true; // arbitrate conflict ("gutter:true" takes priority over "wrap:false")
  23. const before = useHljs ? `<pre><code class="${classNames}"${languageAttr && lang ? ` data-language="${lang}"` : ''}>` : '<pre>';
  24. const after = useHljs ? '</code></pre>' : '</pre>';
  25. const lines = data.value.split('\n');
  26. let numbers = '';
  27. let content = '';
  28. for (let i = 0, len = lines.length; i < len; i++) {
  29. let line = lines[i];
  30. if (tab) line = replaceTabs(line, tab);
  31. numbers += `<span class="line">${Number(firstLine) + i}</span><br>`;
  32. content += formatLine(line, Number(firstLine) + i, mark, options, wrap);
  33. }
  34. let codeCaption = '';
  35. if (caption) {
  36. codeCaption = wrap ? `<figcaption>${caption}</figcaption>` : `<div class="caption">${caption}</div>`;
  37. }
  38. if (!wrap) {
  39. // if original content has one trailing newline, replace it only once, else remove all trailing newlines
  40. content = /\r?\n$/.test(data.value) ? content.replace(/\n$/, '') : content.trimEnd();
  41. return `<pre>${codeCaption}<code class="${classNames}"${languageAttr && lang ? ` data-language="${lang}"` : ''}>${content}</code></pre>`;
  42. }
  43. let result = `<figure class="highlight${data.language ? ` ${data.language}` : ''}"${languageAttr && lang ? ` data-language="${lang}"` : ''}>`;
  44. result += codeCaption;
  45. result += '<table><tr>';
  46. if (gutter) {
  47. result += `<td class="gutter"><pre>${numbers}</pre></td>`;
  48. }
  49. result += `<td class="code">${before}${content}${after}</td>`;
  50. result += '</tr></table></figure>';
  51. return result;
  52. }
  53. function formatLine(line, lineno, marked, options, wrap) {
  54. const useHljs = (options.hljs || false) || !wrap;
  55. const br = wrap ? '<br>' : '\n';
  56. let res = useHljs ? '' : '<span class="line';
  57. if (marked.includes(lineno)) {
  58. // Handle marked lines.
  59. res += useHljs ? `<mark>${line}</mark>` : ` marked">${line}</span>`;
  60. } else {
  61. res += useHljs ? line : `">${line}</span>`;
  62. }
  63. res += br;
  64. return res;
  65. }
  66. function replaceTabs(str, tab) {
  67. return str.replace(/\t+/, match => tab.repeat(match.length));
  68. }
  69. function highlight(str, options) {
  70. let { lang } = options;
  71. const { autoDetect = false } = options;
  72. if (lang) {
  73. lang = lang.toLowerCase();
  74. } else if (autoDetect) {
  75. const result = hljs.highlightAuto(str);
  76. return closeTags(result);
  77. }
  78. if (!lang || !alias.aliases[lang]) {
  79. lang = 'plaintext';
  80. }
  81. const res = hljs.highlight(str, {
  82. language: lang,
  83. ignoreIllegals: true
  84. });
  85. return closeTags(res);
  86. }
  87. // https://github.com/hexojs/hexo-util/issues/10
  88. function closeTags(res) {
  89. const tokenStack = [];
  90. res.value = res.value.split('\n').map(line => {
  91. const prepend = tokenStack.map(token => `<span class="${token}">`).join('');
  92. const matches = line.matchAll(/(<span class="(.*?)">|<\/span>)/g);
  93. for (const match of matches) {
  94. if (match[0] === '</span>') tokenStack.shift();
  95. else tokenStack.unshift(match[2]);
  96. }
  97. const append = '</span>'.repeat(tokenStack.length);
  98. return prepend + line + append;
  99. }).join('\n');
  100. return res;
  101. }
  102. module.exports = highlightUtil;