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 convert elements into