123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- /* eslint-disable @typescript-eslint/no-explicit-any */
- /* eslint-disable @typescript-eslint/quotes */
- import path from 'node:path'
- import MarkdownIt from 'markdown-it'
- import { DomUtils, parseDOM } from 'htmlparser2'
- import { Element, Text } from 'domhandler'
- import { transformSync } from '@babel/core'
- import frontMatter from 'front-matter'
- import { toArray } from '@antfu/utils'
- import type { TransformResult } from 'vite'
- import type { Node as DomHandlerNode } from 'domhandler'
- import type { ResolvedOptions } from './type'
- import { transformAttribs } from './attribs'
- import { getComponentPath, getWrapperComponent } from './wrapperComponent'
- import hljs from 'highlight.js'
- import { fileURLToPath } from 'url'
- const __filename = fileURLToPath(import.meta.url)
- const __dirname = path.dirname(__filename)
- console.log(__dirname)
- const codeBlockStyle = (val: string): string => {
- return `<pre class="hljs"><code>${val}</code></pre>`
- }
- const highlightFormatCode = (str: string, lang: string, md): string => {
- if (lang && hljs.getLanguage(lang)) {
- try {
- return codeBlockStyle(hljs.highlight(lang, str, true).value)
- } catch (e) {
- console.error(e)
- }
- }
- return codeBlockStyle(md.utils.escapeHtml(str))
- }
- export function createMarkdown(useOptions: ResolvedOptions) {
- const markdown = new MarkdownIt({
- html: true,
- linkify: true, // Autoconvert URL-like text to links
- breaks: true, // Convert '\n' in paragraphs into <br>
- highlight: function (str, lang) {
- return highlightFormatCode(str, lang, markdown)
- },
- ...useOptions.markdownItOptions
- })
- useOptions.markdownItUses.forEach((e) => {
- const [plugin, options] = toArray(e)
- markdown.use(plugin, options)
- })
- useOptions.markdownItSetup(markdown)
- /**
- * Inject line number
- */
- const injectLineNumbers = (
- tokens,
- idx,
- options,
- _env,
- slf
- ) => {
- let line
- if (tokens[idx].map && tokens[idx].level === 0) {
- line = (tokens[idx].map)[0]
- tokens[idx].attrJoin('class', 'line')
- tokens[idx].attrSet('data-line', String(line))
- }
- return slf.renderToken(tokens, idx, options)
- }
- markdown.renderer.rules.heading_open = (tokens, idx, options, env, slf) => {
- injectLineNumbers(tokens, idx, options, env, slf)
- return slf.renderToken(tokens, idx, options)
- }
- markdown.renderer.rules.paragraph_open = injectLineNumbers
- return async (raw: string, id: string): Promise<TransformResult> => {
- const { body, attributes } = frontMatter(raw)
- const attributesString = JSON.stringify(attributes)
- const wrapperComponentData = await getWrapperComponent(useOptions.wrapperComponent)
- const importComponentName: string[] = []
- // partial transform code from : https://github.com/hmsk/vite-plugin-markdown/blob/main/src/index.ts
- const html = markdown.render(body, { id })
- const root = parseDOM(html, { lowerCaseTags: false })
- root.forEach(markCodeAsPre)
- const h = DomUtils.getOuterHTML(root, { selfClosingTags: true })
- .replace(/"vfm{{/g, '{{')
- .replace(/}}vfm"/g, '}}')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/&/g, '&')
- // handle notes
- .replace(/<!--/g, '{/*')
- .replace(/-->/g, '*/}')
- let reactCode
- let wrapperComponent = ''
- if (useOptions.wrapperComponentPath) {
- const componentPath = getComponentPath(id, useOptions.wrapperComponentPath)
- wrapperComponent = `import ${useOptions.wrapperComponentName} from '${componentPath}'\n`
- reactCode = `
- const markdown =
- <${useOptions.wrapperComponentName}
- attributes={${attributesString}}
- importComponentName={${JSON.stringify(importComponentName)}}
- >
- <React.Fragment>
- ${h}
- </React.Fragment>
- </${useOptions.wrapperComponentName}>
- `
- }
- else {
- reactCode = `
- const markdown =
- <div className='${useOptions.wrapperClasses}'>
- ${h}
- </div>
- `
- }
- let importComponent = ''
- if (wrapperComponentData && typeof wrapperComponentData === 'object' && importComponentName.length > 0) {
- importComponentName.forEach((componentName) => {
- const path = wrapperComponentData![componentName]
- if (path)
- importComponent += `import ${componentName} from '${getComponentPath(id, path)}'\n`
- })
- }
- const compiledReactCode = `
- function (props) {
- ${transformSync(reactCode, { ast: false, presets: ['@babel/preset-react'] })!.code}
- return markdown
- }
- `
- let code = 'import React from \'react\'\n'
- code += 'import \'@/plugins/vite-plugin-react-markdown/css/markdown.min.css\'\n'
- code += `${wrapperComponent}`
- code += `${importComponent}`
- code += `const ReactComponent = ${compiledReactCode}\n`
- code += 'export default ReactComponent\n'
- code += `export const attributes = ${attributesString}`
- return {
- code,
- map: { mappings: '' } as any,
- }
- function markCodeAsPre(node: DomHandlerNode): void {
- if (node instanceof Element) {
- if (node.tagName.match(/^[A-Z].+/) && !importComponentName.includes(node.tagName))
- importComponentName.push(node.tagName)
- transformAttribs(node.attribs)
- if (node.tagName === 'code') {
- const codeContent = DomUtils.getInnerHTML(node, { decodeEntities: true })
- node.attribs.dangerouslySetInnerHTML = `vfm{{ __html: \`${codeContent.replace(/([\\`])/g, '\\$1')}\`}}vfm`
- node.childNodes = []
- }
- if (node.childNodes.length > 0)
- node.childNodes.forEach(markCodeAsPre)
- }
- if (node instanceof Text) {
- if (node.type === 'text') {
- // .replace(/</g, '<')
- // .replace(/>/g, '>')
- node.data = node.data.replace(/</g, '<').replace(/>/g, '>')
- }
- }
- }
- }
- }
|