Files
medusa-store/www/packages/docs-utils/src/get-clean-md.ts
2025-03-18 17:37:51 +02:00

206 lines
5.3 KiB
TypeScript

import remarkMdx from "remark-mdx"
import remarkParse from "remark-parse"
import remarkStringify from "remark-stringify"
import { FrontMatter, UnistNode, UnistNodeWithData, UnistTree } from "types"
import { Plugin, Transformer, unified } from "unified"
import { SKIP } from "unist-util-visit"
import type { VFile } from "vfile"
import {
ComponentParser,
parseCard,
parseCardList,
parseCodeTabs,
parseColors,
parseComponentExample,
parseComponentReference,
parseDetails,
parseHookValues,
parseIconSearch,
parseNote,
parsePackageInstall,
parsePrerequisites,
parseSourceCodeLink,
parseTable,
parseTabs,
parseTypeList,
parseWorkflowDiagram,
} from "./utils/parsers.js"
import remarkFrontmatter from "remark-frontmatter"
import { matter } from "vfile-matter"
const parsers: Record<string, ComponentParser> = {
Card: parseCard,
CardList: parseCardList,
CodeTabs: parseCodeTabs,
Details: parseDetails,
Note: parseNote,
Prerequisites: parsePrerequisites,
SourceCodeLink: parseSourceCodeLink,
Table: parseTable,
Tabs: parseTabs,
TypeList: parseTypeList,
WorkflowDiagram: parseWorkflowDiagram,
ComponentExample: parseComponentExample,
ComponentReference: parseComponentReference,
PackageInstall: parsePackageInstall,
IconSearch: parseIconSearch,
HookValues: parseHookValues,
Colors: parseColors,
}
const isComponentAllowed = (nodeName: string): boolean => {
return Object.keys(parsers).includes(nodeName)
}
type ParserPluginOptions = {
[key: string]: unknown
}
const parseComponentsPlugin = (options: ParserPluginOptions): Transformer => {
return async (tree) => {
const { visit } = await import("unist-util-visit")
let pageTitle = ""
visit(
tree as UnistTree,
["mdxJsxFlowElement", "element", "mdxjsEsm", "heading"],
(node: UnistNode, index, parent) => {
if (typeof index !== "number" || !parent) {
return
}
if (
node.type === "mdxjsEsm" &&
node.value?.startsWith("export const metadata = ") &&
node.data &&
"estree" in node.data
) {
const regexMatch = /title: (?<title>.+),?/.exec(node.value)
if (regexMatch?.groups?.title) {
pageTitle = regexMatch.groups.title
.replace(/,$/, "")
.replaceAll(/\$\{.+\}/g, "")
.replaceAll(/^['"`]/g, "")
.replaceAll(/['"`]$/g, "")
.trim()
}
}
if (node.type === "heading") {
if (node.depth === 1 && node.children?.length) {
if (node.children[0].value === "metadata.title") {
node.children[0] = {
type: "text",
value: pageTitle,
}
} else {
node.children = node.children
.filter((child) => child.type === "text")
.map((child) => ({
...child,
value: child.value?.trim(),
}))
}
}
return
}
if (
node.type === "mdxjsEsm" ||
!isComponentAllowed(node.name as string)
) {
parent?.children.splice(index, 1)
return [SKIP, index]
}
if (!node.name) {
return
}
const parser = parsers[node.name]
if (parser) {
const parserOptions = options[node.name] || {}
return parser(node as UnistNodeWithData, index, parent, parserOptions)
}
}
)
}
}
const removeFrontmatterPlugin = (): Transformer => {
return async (tree) => {
const { visit } = await import("unist-util-visit")
visit(
tree as UnistTree,
["yaml", "toml"],
(node: UnistNode, index, parent) => {
if (typeof index !== "number" || parent?.type !== "root") {
return
}
parent.children.splice(index, 1)
return [SKIP, index]
}
)
}
}
const getParsedAsString = (file: VFile): string => {
let content = file.toString().replaceAll(/^([\s]*)\* /gm, "$1- ")
const frontmatter = file.data.matter as FrontMatter | undefined
if (frontmatter?.title) {
content = `# ${frontmatter.title}\n\n${frontmatter.description ? `${frontmatter.description}\n\n` : ""}${content}`
}
return content
}
export type GetCleanMdOptions = {
file: string
plugins?: {
before?: Plugin[]
after?: Plugin[]
}
parserOptions?: ParserPluginOptions
type?: "file" | "content"
}
export const getCleanMd = async ({
file,
plugins,
parserOptions,
type = "file",
}: GetCleanMdOptions): Promise<string> => {
const { read } = await import("to-vfile")
if (type === "file" && !file.endsWith(".md") && !file.endsWith(".mdx")) {
return ""
}
const unifier = unified()
.use(remarkParse)
.use(remarkMdx)
.use(remarkStringify)
.use(remarkFrontmatter, ["yaml"])
.use(() => {
return (tree, file) => {
matter(file)
}
})
plugins?.before?.forEach((plugin) => {
unifier.use(...(Array.isArray(plugin) ? plugin : [plugin]))
})
unifier
.use(parseComponentsPlugin, parserOptions || {})
.use(removeFrontmatterPlugin)
plugins?.after?.forEach((plugin) => {
unifier.use(...(Array.isArray(plugin) ? plugin : [plugin]))
})
const content = type === "file" ? await read(file) : file
const parsed = await unifier.process(content)
return getParsedAsString(parsed)
}