/* 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}
${useOptions.wrapperComponentName}>
`
}
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, '>')
}
}
}
}
}