prism.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. 'use strict';
  2. const Prism = require('prismjs');
  3. const stripIndent = require('strip-indent');
  4. const prismLoadLanguages = require('prismjs/components/');
  5. // https://github.com/PrismJS/prism/issues/2145
  6. const prismComponents = require('prismjs/components');
  7. const prismAlias = Object.entries(prismComponents.languages).reduce((acc, [key, value]) => {
  8. if (value.alias) {
  9. if (Array.isArray(value.alias)) {
  10. value.alias.forEach(alias => (acc[alias] = key));
  11. } else if (typeof value.alias === 'string') {
  12. acc[value.alias] = key;
  13. }
  14. }
  15. return acc;
  16. }, {});
  17. const prismSupportedLanguages = Object.keys(prismComponents.languages).concat(Object.keys(prismAlias));
  18. const escapeHTML = require('./escape_html');
  19. /**
  20. * Wrapper of Prism.highlight()
  21. * @param {String} code
  22. * @param {String} language
  23. */
  24. function prismHighlight(code, language) {
  25. // Prism has not load the language pattern
  26. if (!Prism.languages[language] && prismSupportedLanguages.includes(language)) prismLoadLanguages(language);
  27. if (Prism.languages[language]) {
  28. // Prism escapes output by default
  29. return Prism.highlight(code, Prism.languages[language], language);
  30. }
  31. // Current language is not supported by Prism, return origin code;
  32. return escapeHTML(code);
  33. }
  34. /**
  35. * Generate line number required HTML snippet
  36. * @param {String} code - Highlighted code
  37. */
  38. function lineNumberUtil(code) {
  39. const matched = code.match(/\n(?!$)/g);
  40. const num = matched ? matched.length + 1 : 1;
  41. const lines = new Array(num + 1).join('<span></span>');
  42. return `<span aria-hidden="true" class="line-numbers-rows">${lines}</span>`;
  43. }
  44. function replaceTabs(str, tab) {
  45. return str.replace(/^\t+/gm, match => tab.repeat(match.length));
  46. }
  47. function PrismUtil(str, options = {}) {
  48. if (typeof str !== 'string') throw new TypeError('str must be a string!');
  49. str = stripIndent(str);
  50. const {
  51. lineNumber = true,
  52. lang = 'none',
  53. tab,
  54. mark,
  55. firstLine,
  56. isPreprocess = true,
  57. caption
  58. } = options;
  59. // To be consistent with highlight.js
  60. let language = lang === 'plaintext' || lang === 'none' ? 'none' : lang;
  61. if (prismAlias[language]) language = prismAlias[language];
  62. const preTagClassArr = [];
  63. const preTagAttrArr = [];
  64. let preTagAttr = '';
  65. if (lineNumber) preTagClassArr.push('line-numbers');
  66. preTagClassArr.push(`language-${language}`);
  67. // Show Languages plugin
  68. // https://prismjs.com/plugins/show-language/
  69. if (language !== 'none') preTagAttrArr.push(`data-language="${language}"`);
  70. if (!isPreprocess) {
  71. // Shift Line Numbers ('firstLine' option) should only be added under non-preprocess mode
  72. // https://prismjs.com/plugins/line-numbers/
  73. if (lineNumber && firstLine) preTagAttrArr.push(`data-start="${firstLine}"`);
  74. // Line Highlight ('mark' option) should only be added under non-preprocess mode
  75. // https://prismjs.com/plugins/line-highlight/
  76. if (mark) preTagAttrArr.push(`data-line="${mark}"`);
  77. // Apply offset for 'mark' option
  78. // https://github.com/hexojs/hexo-util/pull/172#issuecomment-571882480
  79. if (firstLine && mark) preTagAttrArr.push(`data-line-offset="${firstLine - 1}"`);
  80. }
  81. if (preTagAttrArr.length) preTagAttr = ' ' + preTagAttrArr.join(' ');
  82. if (tab) str = replaceTabs(str, tab);
  83. const codeCaption = caption ? `<div class="caption">${caption}</div>` : '';
  84. const startTag = `<pre class="${preTagClassArr.join(' ')}"${preTagAttr}>${codeCaption}<code class="language-${language}">`;
  85. const endTag = '</code></pre>';
  86. let parsedCode = '';
  87. if (language === 'none' || !isPreprocess) {
  88. parsedCode = escapeHTML(str);
  89. } else {
  90. parsedCode = prismHighlight(str, language);
  91. }
  92. // lineNumberUtil() should be used only under preprocess mode
  93. if (lineNumber && isPreprocess) {
  94. parsedCode += lineNumberUtil(parsedCode);
  95. }
  96. return startTag + parsedCode + endTag;
  97. }
  98. module.exports = PrismUtil;