markdown.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. /* eslint-disable @typescript-eslint/quotes */
  3. import path from 'node:path'
  4. import MarkdownIt from 'markdown-it'
  5. import { DomUtils, parseDOM } from 'htmlparser2'
  6. import { Element, Text } from 'domhandler'
  7. import { transformSync } from '@babel/core'
  8. import frontMatter from 'front-matter'
  9. import { toArray } from '@antfu/utils'
  10. import type { TransformResult } from 'vite'
  11. import type { Node as DomHandlerNode } from 'domhandler'
  12. import type { ResolvedOptions } from './type'
  13. import { transformAttribs } from './attribs'
  14. import { getComponentPath, getWrapperComponent } from './wrapperComponent'
  15. import hljs from 'highlight.js'
  16. import { fileURLToPath } from 'url'
  17. const __filename = fileURLToPath(import.meta.url)
  18. const __dirname = path.dirname(__filename)
  19. console.log(__dirname)
  20. const codeBlockStyle = (val: string): string => {
  21. return `<pre class="hljs"><code>${val}</code></pre>`
  22. }
  23. const highlightFormatCode = (str: string, lang: string, md): string => {
  24. if (lang && hljs.getLanguage(lang)) {
  25. try {
  26. return codeBlockStyle(hljs.highlight(lang, str, true).value)
  27. } catch (e) {
  28. console.error(e)
  29. }
  30. }
  31. return codeBlockStyle(md.utils.escapeHtml(str))
  32. }
  33. export function createMarkdown(useOptions: ResolvedOptions) {
  34. const markdown = new MarkdownIt({
  35. html: true,
  36. linkify: true, // Autoconvert URL-like text to links
  37. breaks: true, // Convert '\n' in paragraphs into <br>
  38. highlight: function (str, lang) {
  39. return highlightFormatCode(str, lang, markdown)
  40. },
  41. ...useOptions.markdownItOptions
  42. })
  43. useOptions.markdownItUses.forEach((e) => {
  44. const [plugin, options] = toArray(e)
  45. markdown.use(plugin, options)
  46. })
  47. useOptions.markdownItSetup(markdown)
  48. /**
  49. * Inject line number
  50. */
  51. const injectLineNumbers = (
  52. tokens,
  53. idx,
  54. options,
  55. _env,
  56. slf
  57. ) => {
  58. let line
  59. if (tokens[idx].map && tokens[idx].level === 0) {
  60. line = (tokens[idx].map)[0]
  61. tokens[idx].attrJoin('class', 'line')
  62. tokens[idx].attrSet('data-line', String(line))
  63. }
  64. return slf.renderToken(tokens, idx, options)
  65. }
  66. markdown.renderer.rules.heading_open = (tokens, idx, options, env, slf) => {
  67. injectLineNumbers(tokens, idx, options, env, slf)
  68. return slf.renderToken(tokens, idx, options)
  69. }
  70. markdown.renderer.rules.paragraph_open = injectLineNumbers
  71. return async (raw: string, id: string): Promise<TransformResult> => {
  72. const { body, attributes } = frontMatter(raw)
  73. const attributesString = JSON.stringify(attributes)
  74. const wrapperComponentData = await getWrapperComponent(useOptions.wrapperComponent)
  75. const importComponentName: string[] = []
  76. // partial transform code from : https://github.com/hmsk/vite-plugin-markdown/blob/main/src/index.ts
  77. const html = markdown.render(body, { id })
  78. const root = parseDOM(html, { lowerCaseTags: false })
  79. root.forEach(markCodeAsPre)
  80. const h = DomUtils.getOuterHTML(root, { selfClosingTags: true })
  81. .replace(/"vfm{{/g, '{{')
  82. .replace(/}}vfm"/g, '}}')
  83. .replace(/&lt;/g, '<')
  84. .replace(/&gt;/g, '>')
  85. .replace(/&quot;/g, '"')
  86. .replace(/&amp;/g, '&')
  87. // handle notes
  88. .replace(/<!--/g, '{/*')
  89. .replace(/-->/g, '*/}')
  90. let reactCode
  91. let wrapperComponent = ''
  92. if (useOptions.wrapperComponentPath) {
  93. const componentPath = getComponentPath(id, useOptions.wrapperComponentPath)
  94. wrapperComponent = `import ${useOptions.wrapperComponentName} from '${componentPath}'\n`
  95. reactCode = `
  96. const markdown =
  97. <${useOptions.wrapperComponentName}
  98. attributes={${attributesString}}
  99. importComponentName={${JSON.stringify(importComponentName)}}
  100. >
  101. <React.Fragment>
  102. ${h}
  103. </React.Fragment>
  104. </${useOptions.wrapperComponentName}>
  105. `
  106. }
  107. else {
  108. reactCode = `
  109. const markdown =
  110. <div className='${useOptions.wrapperClasses}'>
  111. ${h}
  112. </div>
  113. `
  114. }
  115. let importComponent = ''
  116. if (wrapperComponentData && typeof wrapperComponentData === 'object' && importComponentName.length > 0) {
  117. importComponentName.forEach((componentName) => {
  118. const path = wrapperComponentData![componentName]
  119. if (path)
  120. importComponent += `import ${componentName} from '${getComponentPath(id, path)}'\n`
  121. })
  122. }
  123. const compiledReactCode = `
  124. function (props) {
  125. ${transformSync(reactCode, { ast: false, presets: ['@babel/preset-react'] })!.code}
  126. return markdown
  127. }
  128. `
  129. let code = 'import React from \'react\'\n'
  130. code += 'import \'@/plugins/vite-plugin-react-markdown/css/markdown.min.css\'\n'
  131. code += `${wrapperComponent}`
  132. code += `${importComponent}`
  133. code += `const ReactComponent = ${compiledReactCode}\n`
  134. code += 'export default ReactComponent\n'
  135. code += `export const attributes = ${attributesString}`
  136. return {
  137. code,
  138. map: { mappings: '' } as any,
  139. }
  140. function markCodeAsPre(node: DomHandlerNode): void {
  141. if (node instanceof Element) {
  142. if (node.tagName.match(/^[A-Z].+/) && !importComponentName.includes(node.tagName))
  143. importComponentName.push(node.tagName)
  144. transformAttribs(node.attribs)
  145. if (node.tagName === 'code') {
  146. const codeContent = DomUtils.getInnerHTML(node, { decodeEntities: true })
  147. node.attribs.dangerouslySetInnerHTML = `vfm{{ __html: \`${codeContent.replace(/([\\`])/g, '\\$1')}\`}}vfm`
  148. node.childNodes = []
  149. }
  150. if (node.childNodes.length > 0)
  151. node.childNodes.forEach(markCodeAsPre)
  152. }
  153. if (node instanceof Text) {
  154. if (node.type === 'text') {
  155. // .replace(/&lt;/g, '<')
  156. // .replace(/&gt;/g, '>')
  157. node.data = node.data.replace(/</g, '&lt;').replace(/>/g, '&gt;')
  158. }
  159. }
  160. }
  161. }
  162. }