/* 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 `
${val}
` } 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
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 => { 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, '*/}') 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)}} > ${h} ` } else { reactCode = ` const markdown =
${h}
` } 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, '>') } } } } }