From 604f46f8bc462405bc585dd43fa9982f9040cad7 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Tue, 4 Feb 2025 16:44:17 +0200 Subject: [PATCH] docs: add util to generate clean markdown for a file (#11303) --- www/packages/docs-utils/package.json | 2 + .../utils => docs-utils/src}/estree-to-js.ts | 2 +- .../src}/expression-is-utils.ts | 2 +- www/packages/docs-utils/src/get-clean-md.ts | 136 ++++ www/packages/docs-utils/src/index.ts | 3 + .../docs-utils/src/utils/parse-elms.ts | 597 ++++++++++++++++++ .../src/add-url-to-relative-link.ts | 20 + .../src/broken-link-checker.ts | 4 +- .../src/cloudinary-img.ts | 2 +- .../src/cross-project-links.ts | 25 +- .../remark-rehype-plugins/src/index.ts | 1 + .../remark-rehype-plugins/src/local-links.ts | 32 +- .../remark-rehype-plugins/src/page-number.ts | 2 +- .../src/prerequisites-link-fixer.ts | 2 +- .../src/resolve-admonitions.ts | 2 +- .../src/type-list-link-fixer.ts | 2 +- .../src/utils/component-link-fixer.ts | 5 +- .../src/utils/get-attribute.ts | 2 +- .../src/utils/perform-action-on-literal.ts | 7 +- .../src/workflow-diagram-link-fixer.ts | 2 +- www/packages/types/src/index.ts | 1 + .../index.ts => types/src/remark-rehype.ts} | 4 + www/yarn.lock | 14 +- 23 files changed, 832 insertions(+), 37 deletions(-) rename www/packages/{remark-rehype-plugins/src/utils => docs-utils/src}/estree-to-js.ts (98%) rename www/packages/{remark-rehype-plugins/src/utils => docs-utils/src}/expression-is-utils.ts (85%) create mode 100644 www/packages/docs-utils/src/get-clean-md.ts create mode 100644 www/packages/docs-utils/src/utils/parse-elms.ts create mode 100644 www/packages/remark-rehype-plugins/src/add-url-to-relative-link.ts rename www/packages/{remark-rehype-plugins/src/types/index.ts => types/src/remark-rehype.ts} (97%) diff --git a/www/packages/docs-utils/package.json b/www/packages/docs-utils/package.json index c5c5c4a773..b12cd17325 100644 --- a/www/packages/docs-utils/package.json +++ b/www/packages/docs-utils/package.json @@ -28,7 +28,9 @@ "watch": "tsc --watch" }, "dependencies": { + "@mdx-js/mdx": "^3.1.0", "remark-frontmatter": "^5.0.0", + "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "to-vfile": "^8.0.0", diff --git a/www/packages/remark-rehype-plugins/src/utils/estree-to-js.ts b/www/packages/docs-utils/src/estree-to-js.ts similarity index 98% rename from www/packages/remark-rehype-plugins/src/utils/estree-to-js.ts rename to www/packages/docs-utils/src/estree-to-js.ts index 2073ec2904..3726e29042 100644 --- a/www/packages/remark-rehype-plugins/src/utils/estree-to-js.ts +++ b/www/packages/docs-utils/src/estree-to-js.ts @@ -7,7 +7,7 @@ import { ExpressionJsVarLiteral, LiteralExpression, ObjectExpression, -} from "../types/index.js" +} from "types" export function estreeToJs(estree: Estree) { // TODO improve on this utility. Currently it's implemented to work diff --git a/www/packages/remark-rehype-plugins/src/utils/expression-is-utils.ts b/www/packages/docs-utils/src/expression-is-utils.ts similarity index 85% rename from www/packages/remark-rehype-plugins/src/utils/expression-is-utils.ts rename to www/packages/docs-utils/src/expression-is-utils.ts index 455c3ef7e2..b08a0bea26 100644 --- a/www/packages/remark-rehype-plugins/src/utils/expression-is-utils.ts +++ b/www/packages/docs-utils/src/expression-is-utils.ts @@ -1,4 +1,4 @@ -import { ExpressionJsVarLiteral, ExpressionJsVarObj } from "../types/index.js" +import { ExpressionJsVarLiteral, ExpressionJsVarObj } from "types" export function isExpressionJsVarLiteral( expression: unknown diff --git a/www/packages/docs-utils/src/get-clean-md.ts b/www/packages/docs-utils/src/get-clean-md.ts new file mode 100644 index 0000000000..bb8af46259 --- /dev/null +++ b/www/packages/docs-utils/src/get-clean-md.ts @@ -0,0 +1,136 @@ +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 { Plugin, Transformer, unified } from "unified" +import { SKIP } from "unist-util-visit" +import type { VFile } from "vfile" +import { + parseCard, + parseCardList, + parseCodeTabs, + parseDetails, + parseNote, + parsePrerequisites, + parseSourceCodeLink, + parseTable, + parseTabs, + parseTypeList, + parseWorkflowDiagram, +} from "./utils/parse-elms.js" + +const parseComponentsPlugin = (): 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: (?.+),?/.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 && + node.children[0].value === "metadata.title" + ) { + node.children[0] = { + type: "text", + value: pageTitle, + } + } + return + } + if ( + node.type === "mdxjsEsm" || + node.name === "Feedback" || + node.name === "ChildDocs" || + node.name === "DetailsList" + ) { + 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 + ) + } + } + ) + } +} + +const getParsedAsString = (file: VFile): string => { + return file.toString().replaceAll(/^([\s]*)\* /gm, "$1- ") +} + +export const getCleanMd = async ( + filePath: string, + plugins?: { + before?: Plugin[] + after?: Plugin[] + } +): Promise<string> => { + if (!filePath.endsWith(".md") && !filePath.endsWith(".mdx")) { + return "" + } + const unifier = unified().use(remarkParse).use(remarkMdx).use(remarkStringify) + + plugins?.before?.forEach((plugin) => { + unifier.use(...(Array.isArray(plugin) ? plugin : [plugin])) + }) + + unifier.use(parseComponentsPlugin) + + plugins?.after?.forEach((plugin) => { + unifier.use(...(Array.isArray(plugin) ? plugin : [plugin])) + }) + + const parsed = await unifier.process(await read(filePath)) + + return getParsedAsString(parsed) +} diff --git a/www/packages/docs-utils/src/index.ts b/www/packages/docs-utils/src/index.ts index ba4bdd1147..aa40bbaf00 100644 --- a/www/packages/docs-utils/src/index.ts +++ b/www/packages/docs-utils/src/index.ts @@ -1,4 +1,7 @@ +export * from "./estree-to-js.js" +export * from "./expression-is-utils.js" export * from "./find-title.js" +export * from "./get-clean-md.js" export * from "./get-file-slug-sync.js" export * from "./get-file-slug.js" export * from "./get-front-matter.js" diff --git a/www/packages/docs-utils/src/utils/parse-elms.ts b/www/packages/docs-utils/src/utils/parse-elms.ts new file mode 100644 index 0000000000..c67779c8a3 --- /dev/null +++ b/www/packages/docs-utils/src/utils/parse-elms.ts @@ -0,0 +1,597 @@ +import { ExpressionJsVar, UnistNode, UnistNodeWithData, UnistTree } from "types" +import { SKIP, VisitorResult } from "unist-util-visit" +import { estreeToJs } from "../estree-to-js.js" +import { + isExpressionJsVarLiteral, + isExpressionJsVarObj, +} from "../expression-is-utils.js" + +export const parseCard = ( + node: UnistNode, + index: number, + parent: UnistTree +): VisitorResult => { + let title: string | undefined, + text: string | undefined, + href: string | undefined + node.attributes?.some((attr) => { + if (title && text && href) { + return true + } + if (attr.name === "title") { + title = attr.value as string + } else if (attr.name === "text") { + text = attr.value as string + } else if (attr.name === "href") { + href = attr.value as string + } + return false + }) + if (!title || !href) { + return + } + parent?.children.splice(index, 1, { + type: "paragraph", + children: [ + { + type: "link", + url: href, + children: [ + { + type: "text", + value: title, + }, + ], + }, + { + type: "text", + value: `: ${text}`, + }, + ], + }) + return [SKIP, index] +} + +export const parseCardList = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const items = node.attributes?.find((attr) => attr.name === "items") + if (!items || typeof items.value === "string" || !items.value.data?.estree) { + return + } + + const itemsJsVar = estreeToJs(items.value.data.estree) + + if (!itemsJsVar || !Array.isArray(itemsJsVar)) { + return + } + + const listItems = itemsJsVar + .map((item) => { + if ( + !isExpressionJsVarObj(item) || + !("text" in item) || + !("link" in item) || + !isExpressionJsVarLiteral(item.text) || + !isExpressionJsVarLiteral(item.link) + ) { + return null + } + return { + type: "listItem", + children: [ + { + type: "paragraph", + children: [ + { + type: "link", + url: `#${item.link.data}`, + children: [ + { + type: "text", + value: item.text.data, + }, + ], + }, + ], + }, + ], + } + }) + .filter(Boolean) as UnistNode[] + + parent?.children.splice(index, 1, { + type: "list", + ordered: false, + spread: false, + children: listItems, + }) + return [SKIP, index] +} + +export const parseCodeTabs = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const tabs = node.children?.filter((child) => child.name === "CodeTab") + if (!tabs) { + return + } + + const children: UnistNode[] = [] + + tabs.forEach((tab) => { + const label = tab.attributes?.find((attr) => attr.name === "label") + const code = tab.children?.find((child) => child.type === "code") + + if (!label || !code) { + return + } + + children.push({ + type: "mdxJsxFlowElement", + name: "details", + children: [ + { + type: "mdxJsxFlowElement", + name: "summary", + children: [ + { + type: "text", + value: (label.value as string) || "summary", + }, + ], + }, + code, + ], + }) + }) + + parent?.children.splice(index, 1, ...children) + return [SKIP, index] +} + +export const parseDetails = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const summary = node.attributes?.find( + (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 || []), + ], + }) + return [SKIP, index] +} + +export const parseNote = ( + node: UnistNode, + index: number, + parent: UnistTree +): VisitorResult => { + parent.children?.splice(index, 1, ...(node.children || [])) + return [SKIP, index] +} + +export const parsePrerequisites = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const items = node.attributes?.find((attr) => attr.name === "items") + if (!items || typeof items.value === "string" || !items.value.data?.estree) { + return + } + + const itemsJsVar = estreeToJs(items.value.data.estree) + + if (!itemsJsVar || !Array.isArray(itemsJsVar)) { + return + } + + const listItems = itemsJsVar + .map((item) => { + if ( + !isExpressionJsVarObj(item) || + !("text" in item) || + !("link" in item) || + !isExpressionJsVarLiteral(item.text) || + !isExpressionJsVarLiteral(item.link) + ) { + return null + } + return { + type: "listItem", + children: [ + { + type: "paragraph", + children: [ + { + type: "link", + url: `${item.link.data}`, + children: [ + { + type: "text", + value: item.text.data, + }, + ], + }, + ], + }, + ], + } + }) + .filter(Boolean) as UnistNode[] + + parent?.children.splice( + index, + 1, + { + type: "heading", + depth: 3, + children: [ + { + type: "text", + value: "Prerequisites", + }, + ], + }, + { + type: "list", + ordered: false, + spread: false, + children: listItems, + } + ) + return [SKIP, index] +} + +export const parseSourceCodeLink = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const link = node.attributes?.find((attr) => attr.name === "link") + if (!link) { + return + } + + parent?.children.splice(index, 1, { + type: "paragraph", + children: [ + { + type: "link", + url: link?.value as string, + children: [ + { + type: "text", + value: "Source code", + }, + ], + }, + ], + }) + return [SKIP, index] +} + +export const parseTable = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const headerNode = node.children?.find( + (child) => child.name === "Table.Header" + ) + const bodyNode = node.children?.find((child) => child.name === "Table.Body") + + let nodeText = `` + let headerCellsCount = 0 + + headerNode?.children?.forEach((headerRow, rowIndex) => { + if (rowIndex > 0) { + nodeText += `\n` + } + const childCells = + headerRow.children?.length === 1 && + headerRow.children[0].type === "paragraph" + ? headerRow.children[0].children + : headerRow.children + headerCellsCount = childCells?.length || 0 + childCells?.forEach((headerCell) => { + if (headerCell.name !== "Table.HeaderCell") { + return + } + nodeText += `|` + nodeText += formatNodeText(getTextNode(headerCell)) + }) + nodeText += `|` + }) + + nodeText += `\n|${new Array(headerCellsCount).fill(`---`).join(`|`)}|` + + bodyNode?.children?.forEach((bodyRow) => { + nodeText += `\n` + bodyRow.children?.forEach((bodyCell) => { + nodeText += `|` + nodeText += formatNodeText(getTextNode(bodyCell)) + }) + nodeText += `|` + }) + + parent.children?.splice(index, 1, { + type: "paragraph", + children: [ + { + type: "text", + value: nodeText, + }, + ], + }) +} + +export const parseTabs = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + if ((node.children?.length || 0) < 2) { + return + } + + const tabs: UnistNode[] = [] + + node.children![0].children?.forEach((tabList) => { + tabList.children?.forEach((tabTrigger, index) => { + const tabContentNode = node.children?.[1].children?.[index] + const tabLabel = formatNodeText(getTextNode(tabTrigger)) + const tabContent = tabContentNode?.children || [] + if (!tabLabel || !tabContent) { + return + } + + tabs.push({ + type: "mdxJsxFlowElement", + name: "details", + children: [ + { + type: "mdxJsxFlowElement", + name: "summary", + children: [ + { + type: "text", + value: tabLabel, + }, + ], + }, + ...tabContent, + ], + }) + }) + }) + + parent.children?.splice(index, 1, ...tabs) + return [SKIP, index] +} + +export const parseTypeList = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const types = node.attributes?.find((attr) => attr.name === "types") + if (!types || typeof types.value === "string" || !types.value.data?.estree) { + return + } + + const typesJsVar = estreeToJs(types.value.data.estree) + + if (!typesJsVar || !Array.isArray(typesJsVar)) { + return + } + + const generateTypeListItems = ( + typesJsVar: ExpressionJsVar[] + ): UnistNode[] => { + const listItems: UnistNode[] = [] + + typesJsVar.forEach((item) => { + if (!isExpressionJsVarObj(item)) { + return + } + + const typeName = isExpressionJsVarLiteral(item.name) ? item.name.data : "" + const itemType = isExpressionJsVarLiteral(item.type) ? item.type.data : "" + const itemDescription = isExpressionJsVarLiteral(item.description) + ? item.description.data + : "" + + if (!typeName || !itemType) { + return + } + + const itemListChildren = item.children || [] + const children: UnistNode[] = [ + { + type: "paragraph", + children: [ + { + type: "text", + value: `${typeName}: (${itemType}) ${itemDescription}`, + }, + ], + }, + ] + + if (Array.isArray(itemListChildren) && itemListChildren.length) { + children.push(...generateTypeListItems(itemListChildren)) + } + + listItems.push({ + type: "listItem", + children, + }) + }) + + return listItems + } + + const listItems = generateTypeListItems(typesJsVar) + + parent?.children.splice(index, 1, { + type: "list", + ordered: false, + spread: false, + children: listItems, + }) + return [SKIP, index] +} + +export const parseWorkflowDiagram = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const worflowItems = node.attributes?.find((attr) => attr.name === "workflow") + if ( + !worflowItems || + typeof worflowItems.value === "string" || + !worflowItems.value.data?.estree + ) { + return + } + + const workflowJsVar = estreeToJs(worflowItems.value.data.estree) + + if ( + !isExpressionJsVarObj(workflowJsVar) || + !("steps" in workflowJsVar) || + !Array.isArray(workflowJsVar.steps) + ) { + return + } + + const generateWorkflowItems = ( + jsVarItems: ExpressionJsVar[] + ): UnistNode[] => { + const listItems: UnistNode[] = [] + + jsVarItems.forEach((item) => { + if (!isExpressionJsVarObj(item)) { + return + } + + const stepName = isExpressionJsVarLiteral(item.name) ? item.name.data : "" + const stepDescription = isExpressionJsVarLiteral(item.description) + ? item.description.data + : "" + const stepLink = isExpressionJsVarLiteral(item.link) + ? (item.link.data as string) + : `#${stepName}` + + if (!stepName) { + return + } + + const stepChildren = item.steps || [] + const children: UnistNode[] = [ + { + type: "paragraph", + children: [ + { + type: "link", + url: stepLink, + children: [ + { + type: "text", + value: `${stepName}`, + }, + ], + }, + { + type: "text", + value: `: ${stepDescription}`, + }, + ], + }, + ] + + if (Array.isArray(stepChildren) && stepChildren.length) { + children.push(...generateWorkflowItems(stepChildren)) + } + + listItems.push({ + type: "listItem", + children, + }) + }) + + return listItems + } + + const listItems = generateWorkflowItems(workflowJsVar.steps) + + parent?.children.splice(index, 1, { + type: "list", + ordered: false, + spread: false, + children: listItems, + }) + return [SKIP, index] +} + +/** + * Helpers + */ +const getTextNode = (node: UnistNode): UnistNode | undefined => { + let textNode: UnistNode | undefined + node.children?.some((child) => { + if (textNode) { + return true + } + if (child.type === "text") { + textNode = child + } else if (child.type === "paragraph") { + textNode = getTextNode(child) + } else if (child.value) { + textNode = child + } + + return false + }) + + return textNode +} + +const formatNodeText = (node?: UnistNode): string => { + if (!node) { + return "" + } + if (node.type === "inlineCode") { + return `\`${node.value}\`` + } else if (node.type === "code") { + return `\`\`\`${"lang" in node ? node.lang : "ts"}\n${node.value}\n\`\`\`` + } else if (node.type === "link") { + return `[${node.children?.[0].value}](${node.url})` + } + + return node.value || "" +} diff --git a/www/packages/remark-rehype-plugins/src/add-url-to-relative-link.ts b/www/packages/remark-rehype-plugins/src/add-url-to-relative-link.ts new file mode 100644 index 0000000000..c947848e24 --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/add-url-to-relative-link.ts @@ -0,0 +1,20 @@ +import { UnistNode, UnistTree } from "types" +import { Transformer } from "unified" + +type Options = { + url: string +} + +export function addUrlToRelativeLink(options: Options): Transformer { + return async (tree) => { + const { visit } = await import("unist-util-visit") + + visit(tree as UnistTree, "link", (node: UnistNode) => { + if (!node.url || !node.url.startsWith("/")) { + return + } + + node.url = `${options.url}${node.url}` + }) + } +} diff --git a/www/packages/remark-rehype-plugins/src/broken-link-checker.ts b/www/packages/remark-rehype-plugins/src/broken-link-checker.ts index 9307bc44b4..9264ae96e8 100644 --- a/www/packages/remark-rehype-plugins/src/broken-link-checker.ts +++ b/www/packages/remark-rehype-plugins/src/broken-link-checker.ts @@ -6,12 +6,12 @@ import type { UnistNode, UnistNodeWithData, UnistTree, -} from "./types/index.js" +} from "types" import type { VFile } from "vfile" import { parseCrossProjectLink } from "./utils/cross-project-link-utils.js" import { SlugChange } from "types" import getAttribute from "./utils/get-attribute.js" -import { estreeToJs } from "./utils/estree-to-js.js" +import { estreeToJs } from "docs-utils" import { performActionOnLiteral } from "./utils/perform-action-on-literal.js" import { MD_LINK_REGEX } from "./constants.js" diff --git a/www/packages/remark-rehype-plugins/src/cloudinary-img.ts b/www/packages/remark-rehype-plugins/src/cloudinary-img.ts index b08cbf602e..ae06609d59 100644 --- a/www/packages/remark-rehype-plugins/src/cloudinary-img.ts +++ b/www/packages/remark-rehype-plugins/src/cloudinary-img.ts @@ -1,5 +1,5 @@ import type { Transformer } from "unified" -import type { CloudinaryConfig, UnistNode, UnistTree } from "./types/index.js" +import type { CloudinaryConfig, UnistNode, UnistTree } from "types" const cloudinaryImageRegex = /^https:\/\/res.cloudinary.com\/.*\/upload\/v[0-9]+\/(?<imageId>.*)$/ diff --git a/www/packages/remark-rehype-plugins/src/cross-project-links.ts b/www/packages/remark-rehype-plugins/src/cross-project-links.ts index 6adc53f277..bbaa6ec7c1 100644 --- a/www/packages/remark-rehype-plugins/src/cross-project-links.ts +++ b/www/packages/remark-rehype-plugins/src/cross-project-links.ts @@ -4,8 +4,8 @@ import type { UnistNode, UnistNodeWithData, UnistTree, -} from "./types/index.js" -import { estreeToJs } from "./utils/estree-to-js.js" +} from "types" +import { estreeToJs } from "docs-utils" import getAttribute from "./utils/get-attribute.js" import { performActionOnLiteral } from "./utils/perform-action-on-literal.js" @@ -48,6 +48,14 @@ function linkElmFixer(node: UnistNode, options: CrossProjectLinksOptions) { node.properties.href = matchAndFixLinks(node.properties.href, options) } +function linkNodeFixer(node: UnistNode, options: CrossProjectLinksOptions) { + if (!node.url) { + return + } + + node.url = matchAndFixLinks(node.url, options) +} + function componentFixer( node: UnistNodeWithData, options: CrossProjectLinksOptions @@ -128,20 +136,23 @@ export function crossProjectLinksPlugin( visit( tree as UnistTree, - ["element", "mdxJsxFlowElement"], + ["element", "mdxJsxFlowElement", "link"], (node: UnistNode) => { const isComponent = node.name && allowedComponentNames.includes(node.name) - const isLink = node.tagName === "a" && node.properties?.href - if (!isComponent && !isLink) { + const isLinkElm = node.tagName === "a" && node.properties?.href + const isLinkNode = node.type === "link" && node.url + if (!isComponent && !isLinkElm && !isLinkNode) { return } if (isComponent) { componentFixer(node as UnistNodeWithData, options) + } else if (isLinkElm) { + linkElmFixer(node, options) + } else { + linkNodeFixer(node, options) } - - linkElmFixer(node, options) } ) } diff --git a/www/packages/remark-rehype-plugins/src/index.ts b/www/packages/remark-rehype-plugins/src/index.ts index 9e1ab7c242..904bddd118 100644 --- a/www/packages/remark-rehype-plugins/src/index.ts +++ b/www/packages/remark-rehype-plugins/src/index.ts @@ -1,3 +1,4 @@ +export * from "./add-url-to-relative-link.js" export * from "./broken-link-checker.js" export * from "./cloudinary-img.js" export * from "./cross-project-links.js" diff --git a/www/packages/remark-rehype-plugins/src/local-links.ts b/www/packages/remark-rehype-plugins/src/local-links.ts index 357f95bc79..cd3263253c 100644 --- a/www/packages/remark-rehype-plugins/src/local-links.ts +++ b/www/packages/remark-rehype-plugins/src/local-links.ts @@ -1,6 +1,6 @@ import path from "path" import type { Transformer } from "unified" -import type { LocalLinkOptions, UnistNode, UnistTree } from "./types/index.js" +import type { LocalLinkOptions, UnistNode, UnistTree } from "types" import { fixLinkUtil } from "./index.js" export function localLinksRehypePlugin(options: LocalLinkOptions): Transformer { @@ -25,16 +25,28 @@ export function localLinksRehypePlugin(options: LocalLinkOptions): Transformer { "" ) const appsPath = basePath || path.join(file.cwd, "app") - visit(tree as UnistTree, "element", (node: UnistNode) => { - if (node.tagName !== "a" || !node.properties?.href?.match(/page\.mdx?/)) { - return - } + visit(tree as UnistTree, ["element", "link"], (node: UnistNode) => { + if (node.tagName === "a") { + if (!node.properties?.href?.match(/page\.mdx?/)) { + return + } - node.properties.href = fixLinkUtil({ - currentPageFilePath, - linkedPath: node.properties.href, - appsPath, - }) + node.properties.href = fixLinkUtil({ + currentPageFilePath, + linkedPath: node.properties.href, + appsPath, + }) + } else if (node.type === "link") { + if (!node.url?.match(/page\.mdx?/)) { + return + } + + node.url = fixLinkUtil({ + currentPageFilePath, + linkedPath: node.url, + appsPath, + }) + } }) } } diff --git a/www/packages/remark-rehype-plugins/src/page-number.ts b/www/packages/remark-rehype-plugins/src/page-number.ts index cc16a4d978..32c5b77a36 100644 --- a/www/packages/remark-rehype-plugins/src/page-number.ts +++ b/www/packages/remark-rehype-plugins/src/page-number.ts @@ -1,7 +1,7 @@ import path from "path" import type { Transformer } from "unified" import type { RawSidebarItem } from "types" -import type { UnistTree } from "./types/index.js" +import type { UnistTree } from "types" export function pageNumberRehypePlugin({ sidebar, diff --git a/www/packages/remark-rehype-plugins/src/prerequisites-link-fixer.ts b/www/packages/remark-rehype-plugins/src/prerequisites-link-fixer.ts index a9a220e564..a6f6213eaa 100644 --- a/www/packages/remark-rehype-plugins/src/prerequisites-link-fixer.ts +++ b/www/packages/remark-rehype-plugins/src/prerequisites-link-fixer.ts @@ -1,5 +1,5 @@ import { Transformer } from "unified" -import { ComponentLinkFixerOptions } from "./types/index.js" +import { ComponentLinkFixerOptions } from "types" import { componentLinkFixer } from "./utils/component-link-fixer.js" export function prerequisitesLinkFixerPlugin( diff --git a/www/packages/remark-rehype-plugins/src/resolve-admonitions.ts b/www/packages/remark-rehype-plugins/src/resolve-admonitions.ts index 1f579a160f..66e4456c39 100644 --- a/www/packages/remark-rehype-plugins/src/resolve-admonitions.ts +++ b/www/packages/remark-rehype-plugins/src/resolve-admonitions.ts @@ -1,5 +1,5 @@ import { Transformer } from "unified" -import { UnistNode } from "./types/index.js" +import { UnistNode } from "types" const ALLOWED_NODE_NAMES = ["note", "tip", "info", "warning"] diff --git a/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts b/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts index 7724f47700..73cd640024 100644 --- a/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts +++ b/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts @@ -1,5 +1,5 @@ import { Transformer } from "unified" -import { ComponentLinkFixerOptions } from "./types/index.js" +import { ComponentLinkFixerOptions } from "types" import { componentLinkFixer } from "./utils/component-link-fixer.js" export function typeListLinkFixerPlugin( diff --git a/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts b/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts index 0a08c11fd7..be908e3e7b 100644 --- a/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts +++ b/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts @@ -1,10 +1,9 @@ import path from "path" import { Transformer } from "unified" -import { UnistNodeWithData, UnistTree } from "../types/index.js" +import { UnistNodeWithData, UnistTree, ComponentLinkFixerOptions } from "types" import { FixLinkOptions, fixLinkUtil } from "../index.js" import getAttribute from "../utils/get-attribute.js" -import { estreeToJs } from "../utils/estree-to-js.js" -import { ComponentLinkFixerOptions } from "../types/index.js" +import { estreeToJs } from "docs-utils" import { performActionOnLiteral } from "./perform-action-on-literal.js" import { MD_LINK_REGEX } from "../constants.js" diff --git a/www/packages/remark-rehype-plugins/src/utils/get-attribute.ts b/www/packages/remark-rehype-plugins/src/utils/get-attribute.ts index a50abe2fb9..20d4ca6100 100644 --- a/www/packages/remark-rehype-plugins/src/utils/get-attribute.ts +++ b/www/packages/remark-rehype-plugins/src/utils/get-attribute.ts @@ -1,4 +1,4 @@ -import { UnistNodeWithData } from "../types/index.js" +import { UnistNodeWithData } from "types" export default function getAttribute( node: UnistNodeWithData, diff --git a/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts b/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts index 80a47c5fd5..5fee7746ad 100644 --- a/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts +++ b/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts @@ -1,8 +1,5 @@ -import { ExpressionJsVar, ExpressionJsVarLiteral } from "../types/index.js" -import { - isExpressionJsVarLiteral, - isExpressionJsVarObj, -} from "./expression-is-utils.js" +import { ExpressionJsVar, ExpressionJsVarLiteral } from "types" +import { isExpressionJsVarLiteral, isExpressionJsVarObj } from "docs-utils" export const performActionOnLiteral = ( item: ExpressionJsVar[] | ExpressionJsVar, diff --git a/www/packages/remark-rehype-plugins/src/workflow-diagram-link-fixer.ts b/www/packages/remark-rehype-plugins/src/workflow-diagram-link-fixer.ts index 534822552e..97ca819a9d 100644 --- a/www/packages/remark-rehype-plugins/src/workflow-diagram-link-fixer.ts +++ b/www/packages/remark-rehype-plugins/src/workflow-diagram-link-fixer.ts @@ -1,5 +1,5 @@ import { Transformer } from "unified" -import { ComponentLinkFixerOptions } from "./types/index.js" +import { ComponentLinkFixerOptions } from "types" import { componentLinkFixer } from "./utils/component-link-fixer.js" export function workflowDiagramLinkFixerPlugin( diff --git a/www/packages/types/src/index.ts b/www/packages/types/src/index.ts index 069be48cdc..9717696f3f 100644 --- a/www/packages/types/src/index.ts +++ b/www/packages/types/src/index.ts @@ -5,6 +5,7 @@ export * from "./frontmatter.js" export * from "./general.js" export * from "./menu.js" export * from "./navigation.js" +export * from "./remark-rehype.js" export * from "./navigation-dropdown.js" export * from "./sidebar.js" export * from "./tags.js" diff --git a/www/packages/remark-rehype-plugins/src/types/index.ts b/www/packages/types/src/remark-rehype.ts similarity index 97% rename from www/packages/remark-rehype-plugins/src/types/index.ts rename to www/packages/types/src/remark-rehype.ts index 7a0543b2c9..752db4b298 100644 --- a/www/packages/remark-rehype-plugins/src/types/index.ts +++ b/www/packages/types/src/remark-rehype.ts @@ -14,6 +14,10 @@ export interface UnistNode extends Node { type?: string }[] children?: UnistNode[] + ordered?: boolean + url?: string + spread?: boolean + depth?: number } export type ArrayExpression = { diff --git a/www/yarn.lock b/www/yarn.lock index 55bd29e05e..fabe4043e4 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -1583,7 +1583,7 @@ __metadata: languageName: node linkType: hard -"@mdx-js/mdx@npm:^3.0.1": +"@mdx-js/mdx@npm:^3.0.1, @mdx-js/mdx@npm:^3.1.0": version: 3.1.0 resolution: "@mdx-js/mdx@npm:3.1.0" dependencies: @@ -7196,8 +7196,10 @@ __metadata: version: 0.0.0-use.local resolution: "docs-utils@workspace:packages/docs-utils" dependencies: + "@mdx-js/mdx": ^3.1.0 "@types/node": ^20.11.20 remark-frontmatter: ^5.0.0 + remark-mdx: ^3.1.0 remark-parse: ^11.0.0 remark-stringify: ^11.0.0 rimraf: ^5.0.5 @@ -13297,6 +13299,16 @@ __metadata: languageName: node linkType: hard +"remark-mdx@npm:^3.1.0": + version: 3.1.0 + resolution: "remark-mdx@npm:3.1.0" + dependencies: + mdast-util-mdx: ^3.0.0 + micromark-extension-mdxjs: ^3.0.0 + checksum: 247800fa8561624bdca5776457c5965d99e5e60080e80262c600fe12ddd573862e029e39349e1e36e4c3bf79c8e571ecf4d3d2d8c13485b758391fb500e24a1a + languageName: node + linkType: hard + "remark-parse@npm:^10.0.0, remark-parse@npm:^10.0.2": version: 10.0.2 resolution: "remark-parse@npm:10.0.2"