index.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. /**
  2. * @typedef {import('unist-util-visit').Node & {properties: Object<any, any>}} Node
  3. * @typedef {import('unist-util-visit').Parent & {properties: Object<any, any>}} Parent
  4. * @typedef {import('unist-util-visit').Visitor<Node>} Visitor
  5. */
  6. import { visit } from 'unist-util-visit'
  7. import toString from 'hast-util-to-string'
  8. import { refractor } from 'refractor'
  9. import rangeParser from 'parse-numeric-range'
  10. /**
  11. * @param {Node} node
  12. * @return {string|null}
  13. */
  14. const getLanguage = (node) => {
  15. const className = node.properties.className || []
  16. for (const classListItem of className) {
  17. if (classListItem.slice(0, 9) === 'language-') {
  18. return classListItem.slice(9).toLowerCase()
  19. }
  20. }
  21. return null
  22. }
  23. /**
  24. * Create a closure that determines if we have to highlight the given index
  25. *
  26. * @param {string} meta
  27. * @return { (index:number) => boolean }
  28. */
  29. const calculateLinesToHighlight = (meta) => {
  30. const RE = /{([\d,-]+)}/
  31. // Remove space between {} e.g. {1, 3}
  32. const parsedMeta = meta
  33. .split(',')
  34. .map((str) => str.trim())
  35. .join()
  36. if (RE.test(parsedMeta)) {
  37. const strlineNumbers = RE.exec(parsedMeta)[1]
  38. const lineNumbers = rangeParser(strlineNumbers)
  39. return (index) => lineNumbers.includes(index + 1)
  40. } else {
  41. return () => false
  42. }
  43. }
  44. /**
  45. * Split line to div node with className `code-line`
  46. *
  47. * @param {string} text
  48. * @return {Node[]}
  49. */
  50. const splitLine = (text) => {
  51. // Xdm Markdown parser every code line with \n
  52. const textArray = text.split(/\n/)
  53. // Remove last line \n which results in empty array
  54. if (textArray[textArray.length - 1].trim() === '') {
  55. textArray.pop()
  56. }
  57. // Empty array are actually line segments so we convert them back to newlines
  58. return textArray.map((line) => {
  59. return {
  60. type: 'element',
  61. tagName: 'div',
  62. properties: { className: ['code-line'] },
  63. children: [{ type: 'text', value: line === '' ? '\n' : line }],
  64. }
  65. })
  66. }
  67. /**
  68. * Rehype plugin that highlights code blocks with refractor (prismjs)
  69. *
  70. * Set `showLineNumbers` to `true` to always display line number
  71. *
  72. * Set `ignoreMissing` to `true` to ignore unsupported languages and line highlighting when no language is specified
  73. *
  74. * @typedef {{ showLineNumbers?: boolean, ignoreMissing?: boolean }} RehypePrismOptions
  75. * @param {RehypePrismOptions} options
  76. * @return {Visitor}
  77. */
  78. const rehypePrism = (options) => {
  79. options = options || {}
  80. return (tree) => {
  81. visit(tree, 'element', visitor)
  82. }
  83. /**
  84. * @param {Node} node
  85. * @param {number} index
  86. * @param {Parent} parent
  87. */
  88. function visitor(node, index, parent) {
  89. if (!parent || parent.tagName !== 'pre' || node.tagName !== 'code') {
  90. return
  91. }
  92. const lang = getLanguage(node)
  93. /** @type {string} */
  94. // @ts-ignore
  95. let meta = node.data && node.data.meta ? node.data.meta : ''
  96. if (lang) {
  97. parent.properties.className = (parent.properties.className || []).concat('language-' + lang)
  98. // Add lang to meta to allow line highlighting even when no lang is specified
  99. meta = `${lang} ${meta}`
  100. }
  101. const shouldHighlightLine = calculateLinesToHighlight(meta)
  102. const codeLineArray = splitLine(toString(node))
  103. for (const [i, line] of codeLineArray.entries()) {
  104. // Code lines
  105. if (meta.toLowerCase().includes('showLineNumbers'.toLowerCase()) || options.showLineNumbers) {
  106. line.properties.line = [(i + 1).toString()]
  107. line.properties.className = [`${line.properties.className} line-number`]
  108. }
  109. // Line highlight
  110. if (shouldHighlightLine(i)) {
  111. line.properties.className = [`${line.properties.className} highlight-line`]
  112. }
  113. // Syntax highlight
  114. if (lang && line.children) {
  115. try {
  116. line.children = refractor.highlight(line.children[0].value, lang).children
  117. } catch (err) {
  118. // eslint-disable-next-line no-empty
  119. if (options.ignoreMissing && /Unknown language/.test(err.message)) {
  120. } else {
  121. throw err
  122. }
  123. }
  124. }
  125. }
  126. node.children = codeLineArray
  127. }
  128. }
  129. export default rehypePrism