From f3b6571a4ed5bbf0f38636bd06aa064d440f720f Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 20 Feb 2025 12:07:19 -0500 Subject: [PATCH] fix: download component in the mdx (#2195) --- packages/fern-docs/mdx/package.json | 1 + .../mdx/src/hast-utils/hast-to-markdown.ts | 26 +++++ .../fern-docs/mdx/src/hast-utils/index.ts | 1 + .../src/mdast-utils/mdast-from-markdown.ts | 31 +++--- .../mdx/src/mdast-utils/mdast-to-markdown.ts | 12 ++- packages/fern-docs/mdx/src/mdx-utils/index.ts | 1 + .../ui/src/mdx/bundlers/mdx-bundler.ts | 2 + .../ui/src/mdx/bundlers/next-mdx-remote.ts | 2 + .../ui/src/mdx/components/button/Button.tsx | 1 + .../src/mdx/components/download/Download.tsx | 17 +++ .../ui/src/mdx/components/download/index.ts | 1 + .../fern-docs/ui/src/mdx/components/index.tsx | 2 + .../src/mdx/plugins/rehype-download.test.ts | 58 ++++++++++ .../ui/src/mdx/plugins/rehype-download.ts | 62 +++++++++++ pnpm-lock.yaml | 100 +++++++++++++++--- 15 files changed, 282 insertions(+), 35 deletions(-) create mode 100644 packages/fern-docs/mdx/src/hast-utils/hast-to-markdown.ts create mode 100644 packages/fern-docs/ui/src/mdx/components/download/Download.tsx create mode 100644 packages/fern-docs/ui/src/mdx/components/download/index.ts create mode 100644 packages/fern-docs/ui/src/mdx/plugins/rehype-download.test.ts create mode 100644 packages/fern-docs/ui/src/mdx/plugins/rehype-download.ts diff --git a/packages/fern-docs/mdx/package.json b/packages/fern-docs/mdx/package.json index 6ff7a21612..cf9e2aaffc 100644 --- a/packages/fern-docs/mdx/package.json +++ b/packages/fern-docs/mdx/package.json @@ -53,6 +53,7 @@ "gray-matter": "^4.0.3", "hast-util-heading-rank": "^3.0.0", "hast-util-to-estree": "^3.1.1", + "hast-util-to-mdast": "^10.1.2", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.0", "mdast-util-from-markdown": "^2.0.2", diff --git a/packages/fern-docs/mdx/src/hast-utils/hast-to-markdown.ts b/packages/fern-docs/mdx/src/hast-utils/hast-to-markdown.ts new file mode 100644 index 0000000000..9d2e6e96a4 --- /dev/null +++ b/packages/fern-docs/mdx/src/hast-utils/hast-to-markdown.ts @@ -0,0 +1,26 @@ +import type { Root as HastRoot } from "hast"; +import { toMdast } from "hast-util-to-mdast"; +import { mdastToMarkdown } from "../mdast-utils/mdast-to-markdown"; + +export function hastToMarkdown( + hast: HastRoot, + format: "mdx" | "md" = "mdx" +): string { + const mdast = toMdast(hast, { + nodeHandlers: + format === "mdx" + ? { + // pass through node types + mdxFlowExpression: (_state, node) => node, + mdxJsxFlowElement: (_state, node) => node, + mdxJsxTextElement: (_state, node) => node, + mdxTextExpression: (_state, node) => node, + mdxjsEsm: (_state, node) => node, + } + : undefined, + }); + if (mdast.type !== "root") { + throw new Error("Expected root node"); + } + return mdastToMarkdown(mdast, format); +} diff --git a/packages/fern-docs/mdx/src/hast-utils/index.ts b/packages/fern-docs/mdx/src/hast-utils/index.ts index da9223c5e4..29100339f8 100644 --- a/packages/fern-docs/mdx/src/hast-utils/index.ts +++ b/packages/fern-docs/mdx/src/hast-utils/index.ts @@ -1,5 +1,6 @@ export * from "./hast-get-boolean-value"; export * from "./hast-mdx-to-props"; +export * from "./hast-to-markdown"; export * from "./hast-to-string"; export * from "./is-hast-element"; export * from "./is-hast-text"; diff --git a/packages/fern-docs/mdx/src/mdast-utils/mdast-from-markdown.ts b/packages/fern-docs/mdx/src/mdast-utils/mdast-from-markdown.ts index b335c755fd..d17e7421d5 100644 --- a/packages/fern-docs/mdx/src/mdast-utils/mdast-from-markdown.ts +++ b/packages/fern-docs/mdx/src/mdast-utils/mdast-from-markdown.ts @@ -1,3 +1,4 @@ +import { compact } from "es-toolkit"; import type { Root as MdastRoot } from "mdast"; import { fromMarkdown } from "mdast-util-from-markdown"; import { gfmFromMarkdown } from "mdast-util-gfm"; @@ -6,27 +7,21 @@ import { mdxFromMarkdown } from "mdast-util-mdx"; import { gfm } from "micromark-extension-gfm"; import { math } from "micromark-extension-math"; import { mdxjs } from "micromark-extension-mdxjs"; -import { UnreachableCaseError } from "ts-essentials"; export function mdastFromMarkdown( content: string, format: "mdx" | "md" = "mdx" ): MdastRoot { - if (format === "md") { - return fromMarkdown(content, { - extensions: [math(), gfm()], - mdastExtensions: [mathFromMarkdown(), gfmFromMarkdown()], - }); - } else if (format === "mdx") { - return fromMarkdown(content, { - extensions: [mdxjs(), math(), gfm()], - mdastExtensions: [ - mdxFromMarkdown(), - mathFromMarkdown(), - gfmFromMarkdown(), - ], - }); - } else { - throw new UnreachableCaseError(format); - } + return fromMarkdown(content, { + extensions: compact([ + format === "mdx" ? mdxjs() : undefined, + math(), + gfm(), + ]), + mdastExtensions: compact([ + format === "mdx" ? mdxFromMarkdown() : undefined, + mathFromMarkdown(), + gfmFromMarkdown(), + ]), + }); } diff --git a/packages/fern-docs/mdx/src/mdast-utils/mdast-to-markdown.ts b/packages/fern-docs/mdx/src/mdast-utils/mdast-to-markdown.ts index 0feb9c68a6..1cb8bdef32 100644 --- a/packages/fern-docs/mdx/src/mdast-utils/mdast-to-markdown.ts +++ b/packages/fern-docs/mdx/src/mdast-utils/mdast-to-markdown.ts @@ -1,11 +1,19 @@ +import { compact } from "es-toolkit"; import type { Root as MdastRoot } from "mdast"; import { gfmToMarkdown } from "mdast-util-gfm"; import { mathToMarkdown } from "mdast-util-math"; import { mdxToMarkdown } from "mdast-util-mdx"; import { toMarkdown } from "mdast-util-to-markdown"; -export function mdastToMarkdown(mdast: MdastRoot): string { +export function mdastToMarkdown( + mdast: MdastRoot, + format: "mdx" | "md" = "mdx" +): string { return toMarkdown(mdast, { - extensions: [mdxToMarkdown(), mathToMarkdown(), gfmToMarkdown()], + extensions: compact([ + format === "mdx" ? mdxToMarkdown() : undefined, + mathToMarkdown(), + gfmToMarkdown(), + ]), }); } diff --git a/packages/fern-docs/mdx/src/mdx-utils/index.ts b/packages/fern-docs/mdx/src/mdx-utils/index.ts index 094a8053ec..c90430d717 100644 --- a/packages/fern-docs/mdx/src/mdx-utils/index.ts +++ b/packages/fern-docs/mdx/src/mdx-utils/index.ts @@ -1,4 +1,5 @@ export * from "./extract-jsx"; +export * from "./extract-literal"; export * from "./is-mdx-element"; export * from "./is-mdx-expression"; export * from "./is-mdx-jsx-attr"; diff --git a/packages/fern-docs/ui/src/mdx/bundlers/mdx-bundler.ts b/packages/fern-docs/ui/src/mdx/bundlers/mdx-bundler.ts index 1119cd1609..1168bb0614 100644 --- a/packages/fern-docs/ui/src/mdx/bundlers/mdx-bundler.ts +++ b/packages/fern-docs/ui/src/mdx/bundlers/mdx-bundler.ts @@ -25,6 +25,7 @@ import remarkGemoji from "remark-gemoji"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import remarkSmartypants from "remark-smartypants"; +import { rehypeDownload } from "../plugins/rehype-download"; import { rehypeFiles } from "../plugins/rehype-files"; import { rehypeLinks } from "../plugins/rehype-links"; import { rehypeExtractAsides } from "../plugins/rehypeExtractAsides"; @@ -158,6 +159,7 @@ async function serializeMdxImpl( rehypeMdxClassStyle, [rehypeFiles, { replaceSrc }], [rehypeLinks, { replaceHref }], + rehypeDownload, // must be after rehypeFiles rehypeAcornErrorBoundary, rehypeSlug, rehypeKatex, diff --git a/packages/fern-docs/ui/src/mdx/bundlers/next-mdx-remote.ts b/packages/fern-docs/ui/src/mdx/bundlers/next-mdx-remote.ts index 23fad60839..9b548a9d59 100644 --- a/packages/fern-docs/ui/src/mdx/bundlers/next-mdx-remote.ts +++ b/packages/fern-docs/ui/src/mdx/bundlers/next-mdx-remote.ts @@ -22,6 +22,7 @@ import remarkGemoji from "remark-gemoji"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import remarkSmartypants from "remark-smartypants"; +import { rehypeDownload } from "../plugins/rehype-download"; import { rehypeFiles } from "../plugins/rehype-files"; import { rehypeLinks } from "../plugins/rehype-links"; import { rehypeExtractAsides } from "../plugins/rehypeExtractAsides"; @@ -76,6 +77,7 @@ function withDefaultMdxOptions( rehypeMdxClassStyle, [rehypeFiles, { replaceSrc }], [rehypeLinks, { replaceHref }], + rehypeDownload, // must be after rehypeFiles rehypeAcornErrorBoundary, rehypeSlug, rehypeKatex, diff --git a/packages/fern-docs/ui/src/mdx/components/button/Button.tsx b/packages/fern-docs/ui/src/mdx/components/button/Button.tsx index 3f0e94a84a..3ab2469d81 100644 --- a/packages/fern-docs/ui/src/mdx/components/button/Button.tsx +++ b/packages/fern-docs/ui/src/mdx/components/button/Button.tsx @@ -20,6 +20,7 @@ export declare namespace Button { intent?: "none" | "primary" | "success" | "warning" | "danger"; text?: ReactNode; href?: string; + download?: any; } } diff --git a/packages/fern-docs/ui/src/mdx/components/download/Download.tsx b/packages/fern-docs/ui/src/mdx/components/download/Download.tsx new file mode 100644 index 0000000000..50fef00f79 --- /dev/null +++ b/packages/fern-docs/ui/src/mdx/components/download/Download.tsx @@ -0,0 +1,17 @@ +import { PropsWithChildren } from "react"; + +export function Download({ + children, + src, + filename, +}: PropsWithChildren<{ src?: string; filename?: string }>) { + if (!src) { + return children; + } + + return ( + + {children} + + ); +} diff --git a/packages/fern-docs/ui/src/mdx/components/download/index.ts b/packages/fern-docs/ui/src/mdx/components/download/index.ts new file mode 100644 index 0000000000..fae0e38b6d --- /dev/null +++ b/packages/fern-docs/ui/src/mdx/components/download/index.ts @@ -0,0 +1 @@ +export * from "./Download"; diff --git a/packages/fern-docs/ui/src/mdx/components/index.tsx b/packages/fern-docs/ui/src/mdx/components/index.tsx index 18228febf1..94b4f0edb9 100644 --- a/packages/fern-docs/ui/src/mdx/components/index.tsx +++ b/packages/fern-docs/ui/src/mdx/components/index.tsx @@ -29,6 +29,7 @@ import { ClientLibraries } from "./client-libraries"; import { CodeBlock } from "./code/CodeBlock"; import { CodeGroup } from "./code/CodeGroup"; import { Column, ColumnGroup } from "./columns"; +import { Download } from "./download"; import { Feature } from "./feature"; import { Frame } from "./frame"; import { A, HeadingRenderer, Image, Li, Ol, Strong, Ul } from "./html"; @@ -63,6 +64,7 @@ const FERN_COMPONENTS = { CodeGroup, // note: alias is handled in rehypeFernCode Column, ColumnGroup, + Download, EndpointRequestSnippet, EndpointResponseSnippet, Feature, diff --git a/packages/fern-docs/ui/src/mdx/plugins/rehype-download.test.ts b/packages/fern-docs/ui/src/mdx/plugins/rehype-download.test.ts new file mode 100644 index 0000000000..fce891b56e --- /dev/null +++ b/packages/fern-docs/ui/src/mdx/plugins/rehype-download.test.ts @@ -0,0 +1,58 @@ +import { hastToMarkdown, toTree } from "@fern-docs/mdx"; +import { rehypeDownload } from "./rehype-download"; + +const run = rehypeDownload(); + +describe("rehypeDownload", () => { + it("should be a noop if src is not set", () => { + const ast = toTree(`` + ).hast; + run(ast); + expect(hastToMarkdown(ast)).toMatchInlineSnapshot( + ` + " + " + ` + ); + }); + + it("should include the filename in the `download` attribute", () => { + const ast = toTree( + `` + ).hast; + run(ast); + expect(hastToMarkdown(ast)).toMatchInlineSnapshot( + ` + " + " + ` + ); + }); + + it("should be a noop if the child is not a