docs: add clean markdown version of all documentation pages (#11308)

* added route to book

* added to resources

* added route to ui

* added to user guide
This commit is contained in:
Shahed Nasser
2025-02-05 11:23:13 +02:00
committed by GitHub
parent 87db3f0c45
commit 98236c8262
30 changed files with 1086 additions and 192 deletions

View File

@@ -29,6 +29,7 @@
},
"dependencies": {
"@mdx-js/mdx": "^3.1.0",
"react-docgen": "^7.1.0",
"remark-frontmatter": "^5.0.0",
"remark-mdx": "^3.1.0",
"remark-parse": "^11.0.0",

View File

@@ -2,25 +2,62 @@ import remarkMdx from "remark-mdx"
import remarkParse from "remark-parse"
import remarkStringify from "remark-stringify"
import { read } from "to-vfile"
import { UnistNode, UnistNodeWithData, UnistTree } from "types"
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/parse-elms.js"
} from "./utils/parsers.js"
import remarkFrontmatter from "remark-frontmatter"
import { matter } from "vfile-matter"
const parseComponentsPlugin = (): Transformer => {
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")
@@ -50,81 +87,110 @@ const parseComponentsPlugin = (): Transformer => {
}
}
if (node.type === "heading") {
if (
node.depth === 1 &&
node.children?.length &&
node.children[0].value === "metadata.title"
) {
node.children[0] = {
type: "text",
value: pageTitle,
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" ||
node.name === "Feedback" ||
node.name === "ChildDocs" ||
node.name === "DetailsList"
!isComponentAllowed(node.name as string)
) {
parent?.children.splice(index, 1)
return [SKIP, index]
}
switch (node.name) {
case "Card":
return parseCard(node, index, parent)
case "CardList":
return parseCardList(node as UnistNodeWithData, index, parent)
case "CodeTabs":
return parseCodeTabs(node as UnistNodeWithData, index, parent)
case "Details":
return parseDetails(node as UnistNodeWithData, index, parent)
case "Note":
return parseNote(node, index, parent)
case "Prerequisites":
return parsePrerequisites(node as UnistNodeWithData, index, parent)
case "SourceCodeLink":
return parseSourceCodeLink(node as UnistNodeWithData, index, parent)
case "Table":
return parseTable(node as UnistNodeWithData, index, parent)
case "Tabs":
return parseTabs(node as UnistNodeWithData, index, parent)
case "TypeList":
return parseTypeList(node as UnistNodeWithData, index, parent)
case "WorkflowDiagram":
return parseWorkflowDiagram(
node as UnistNodeWithData,
index,
parent
)
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 getParsedAsString = (file: VFile): string => {
return file.toString().replaceAll(/^([\s]*)\* /gm, "$1- ")
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]
}
)
}
}
export const getCleanMd = async (
filePath: string,
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
}
type Options = {
filePath: string
plugins?: {
before?: Plugin[]
after?: Plugin[]
}
): Promise<string> => {
parserOptions?: ParserPluginOptions
}
export const getCleanMd = async ({
filePath,
plugins,
parserOptions,
}: Options): Promise<string> => {
if (!filePath.endsWith(".md") && !filePath.endsWith(".mdx")) {
return ""
}
const unifier = unified().use(remarkParse).use(remarkMdx).use(remarkStringify)
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)
unifier
.use(parseComponentsPlugin, parserOptions || {})
.use(removeFrontmatterPlugin)
plugins?.after?.forEach((plugin) => {
unifier.use(...(Array.isArray(plugin) ? plugin : [plugin]))

View File

@@ -5,9 +5,19 @@ import {
isExpressionJsVarLiteral,
isExpressionJsVarObj,
} from "../expression-is-utils.js"
import path from "path"
import { readFileSync } from "fs"
import type { Documentation } from "react-docgen"
export const parseCard = (
node: UnistNode,
export type ComponentParser<TOptions = any> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options?: TOptions
) => VisitorResult
export const parseCard: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
@@ -52,7 +62,7 @@ export const parseCard = (
return [SKIP, index]
}
export const parseCardList = (
export const parseCardList: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -72,30 +82,40 @@ export const parseCardList = (
.map((item) => {
if (
!isExpressionJsVarObj(item) ||
!("text" in item) ||
!("link" in item) ||
!isExpressionJsVarLiteral(item.text) ||
!isExpressionJsVarLiteral(item.link)
!("title" in item) ||
!("href" in item) ||
!isExpressionJsVarLiteral(item.title) ||
!isExpressionJsVarLiteral(item.href)
) {
return null
}
const description = isExpressionJsVarLiteral(item.text)
? (item.text.data as string)
: ""
const children: UnistNode[] = [
{
type: "link",
url: `${item.href.data}`,
children: [
{
type: "text",
value: item.title.data as string,
},
],
},
]
if (description.length) {
children.push({
type: "text",
value: `: ${description}`,
})
}
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "link",
url: `#${item.link.data}`,
children: [
{
type: "text",
value: item.text.data,
},
],
},
],
children,
},
],
}
@@ -111,7 +131,7 @@ export const parseCardList = (
return [SKIP, index]
}
export const parseCodeTabs = (
export const parseCodeTabs: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -131,30 +151,26 @@ export const parseCodeTabs = (
return
}
children.push({
type: "mdxJsxFlowElement",
name: "details",
children: [
{
type: "mdxJsxFlowElement",
name: "summary",
children: [
{
type: "text",
value: (label.value as string) || "summary",
},
],
},
code,
],
})
children.push(
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: label.value as string,
},
],
},
code
)
})
parent?.children.splice(index, 1, ...children)
return [SKIP, index]
}
export const parseDetails = (
export const parseDetails: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -163,28 +179,29 @@ export const parseDetails = (
(attr) => attr.name === "summaryContent"
)
parent?.children.splice(index, 1, {
type: "mdxJsxFlowElement",
name: "details",
children: [
{
type: "mdxJsxFlowElement",
name: "summary",
children: [
{
type: "text",
value: (summary?.value as string) || "Details",
},
],
},
...(node.children || []),
],
})
const children: UnistNode[] = []
if (summary?.value) {
children.push({
type: "heading",
depth: 3,
children: [
{
type: "text",
value: (summary?.value as string) || "Details",
},
],
})
}
children.push(...(node.children || []))
parent?.children.splice(index, 1, ...children)
return [SKIP, index]
}
export const parseNote = (
node: UnistNode,
export const parseNote: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
@@ -192,7 +209,7 @@ export const parseNote = (
return [SKIP, index]
}
export const parsePrerequisites = (
export const parsePrerequisites: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -265,7 +282,7 @@ export const parsePrerequisites = (
return [SKIP, index]
}
export const parseSourceCodeLink = (
export const parseSourceCodeLink: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -293,7 +310,7 @@ export const parseSourceCodeLink = (
return [SKIP, index]
}
export const parseTable = (
export const parseTable: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -348,7 +365,7 @@ export const parseTable = (
})
}
export const parseTabs = (
export const parseTabs: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -368,23 +385,19 @@ export const parseTabs = (
return
}
tabs.push({
type: "mdxJsxFlowElement",
name: "details",
children: [
{
type: "mdxJsxFlowElement",
name: "summary",
children: [
{
type: "text",
value: tabLabel,
},
],
},
...tabContent,
],
})
tabs.push(
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: tabLabel,
},
],
},
...tabContent
)
})
})
@@ -392,7 +405,7 @@ export const parseTabs = (
return [SKIP, index]
}
export const parseTypeList = (
export const parseTypeList: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -435,7 +448,7 @@ export const parseTypeList = (
children: [
{
type: "text",
value: `${typeName}: (${itemType}) ${itemDescription}`,
value: `${typeName}: (${itemType}) ${itemDescription}`.trim(),
},
],
},
@@ -465,7 +478,7 @@ export const parseTypeList = (
return [SKIP, index]
}
export const parseWorkflowDiagram = (
export const parseWorkflowDiagram: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
@@ -558,6 +571,321 @@ export const parseWorkflowDiagram = (
return [SKIP, index]
}
export const parseComponentExample: ComponentParser<{
examplesBasePath: string
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.examplesBasePath) {
return
}
const exampleName = node.attributes?.find((attr) => attr.name === "name")
if (!exampleName) {
return
}
const fileContent = readFileSync(
path.join(options.examplesBasePath, `${exampleName.value as string}.tsx`),
"utf-8"
)
parent.children?.splice(index, 1, {
type: "code",
lang: "tsx",
value: fileContent,
})
return [SKIP, index]
}
export const parseComponentReference: ComponentParser<{ specsPath: string }> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.specsPath) {
return
}
const mainComponent = node.attributes?.find(
(attr) => attr.name === "mainComponent"
)?.value as string
if (!mainComponent) {
return
}
const componentNames: string[] = []
const componentsToShowAttr = node.attributes?.find(
(attr) => attr.name === "componentsToShow"
)
if (
componentsToShowAttr &&
typeof componentsToShowAttr.value !== "string" &&
componentsToShowAttr.value.data?.estree
) {
const componentsToShowJsVar = estreeToJs(
componentsToShowAttr.value.data.estree
)
if (componentsToShowAttr && Array.isArray(componentsToShowJsVar)) {
componentNames.push(
...componentsToShowJsVar
.map((item) => {
return isExpressionJsVarLiteral(item) ? (item.data as string) : ""
})
.filter((name) => name.length > 0)
)
}
}
if (!componentNames.length) {
componentNames.push(mainComponent)
}
const getComponentNodes = (componentName: string): UnistNode[] => {
const componentSpecsFile = path.join(
options.specsPath,
mainComponent,
`${componentName}.json`
)
const componentSpecs: Documentation = JSON.parse(
readFileSync(componentSpecsFile, "utf-8")
)
const componentNodes: UnistNode[] = [
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: `${componentName} Props`,
},
],
},
]
if (componentSpecs.description) {
componentNodes.push({
type: "paragraph",
children: [
{
type: "text",
value: componentSpecs.description,
},
],
})
}
if (componentSpecs.props) {
const listNode: UnistNode = {
type: "list",
ordered: false,
spread: false,
children: [],
}
Object.entries(componentSpecs.props).forEach(([propName, propData]) => {
listNode.children?.push({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value:
`${propName}: (${propData.type?.name || propData.tsType?.name}) ${propData.description || ""}${propData.defaultValue ? ` Default: ${propData.defaultValue.value}` : ""}`.trim(),
},
],
},
],
})
})
componentNodes.push(listNode)
}
return componentNodes
}
parent.children?.splice(
index,
1,
...componentNames.flatMap(getComponentNodes)
)
}
export const parsePackageInstall: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const packageName = node.attributes?.find(
(attr) => attr.name === "packageName"
)
if (!packageName) {
return
}
parent.children?.splice(index, 1, {
type: "code",
lang: "bash",
value: `npm install ${packageName.value}`,
})
return [SKIP, index]
}
export const parseIconSearch: ComponentParser<{ iconNames: string[] }> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.iconNames) {
return
}
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: options.iconNames.map((iconName) => ({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value: iconName,
},
],
},
],
})),
})
return [SKIP, index]
}
export const parseHookValues: ComponentParser<{
hooksData: {
[k: string]: {
value: string
type?: {
type: string
}
description?: string
}[]
}
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.hooksData) {
return
}
const hookName = node.attributes?.find((attr) => attr.name === "hook")
if (
!hookName ||
!hookName.value ||
typeof hookName.value !== "string" ||
!options.hooksData[hookName.value]
) {
return
}
const hookData = options.hooksData[hookName.value]
const listItems = hookData.map((item) => {
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value:
`${item.value}: (${item.type?.type}) ${item.description || ""}`.trim(),
},
],
},
],
}
})
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: listItems,
})
return [SKIP, index]
}
export const parseColors: ComponentParser<{
colors: {
[k: string]: Record<string, string>
}
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.colors) {
return
}
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: Object.entries(options.colors).flatMap(([section, colors]) => [
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: section,
},
],
},
...Object.entries(colors).map(([name, value]) => ({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value: name,
},
{
type: "text",
value: `: ${value}`,
},
],
},
],
})),
]),
})
}
/**
* Helpers
*/