Skip to content

Commit

Permalink
fix: download component in the mdx (#2195)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Feb 20, 2025
1 parent 3f4f28e commit f3b6571
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 35 deletions.
1 change: 1 addition & 0 deletions packages/fern-docs/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions packages/fern-docs/mdx/src/hast-utils/hast-to-markdown.ts
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions packages/fern-docs/mdx/src/hast-utils/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
31 changes: 13 additions & 18 deletions packages/fern-docs/mdx/src/mdast-utils/mdast-from-markdown.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(),
]),
});
}
12 changes: 10 additions & 2 deletions packages/fern-docs/mdx/src/mdast-utils/mdast-to-markdown.ts
Original file line number Diff line number Diff line change
@@ -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(),
]),
});
}
1 change: 1 addition & 0 deletions packages/fern-docs/mdx/src/mdx-utils/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 2 additions & 0 deletions packages/fern-docs/ui/src/mdx/bundlers/mdx-bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -158,6 +159,7 @@ async function serializeMdxImpl(
rehypeMdxClassStyle,
[rehypeFiles, { replaceSrc }],
[rehypeLinks, { replaceHref }],
rehypeDownload, // must be after rehypeFiles
rehypeAcornErrorBoundary,
rehypeSlug,
rehypeKatex,
Expand Down
2 changes: 2 additions & 0 deletions packages/fern-docs/ui/src/mdx/bundlers/next-mdx-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -76,6 +77,7 @@ function withDefaultMdxOptions(
rehypeMdxClassStyle,
[rehypeFiles, { replaceSrc }],
[rehypeLinks, { replaceHref }],
rehypeDownload, // must be after rehypeFiles
rehypeAcornErrorBoundary,
rehypeSlug,
rehypeKatex,
Expand Down
1 change: 1 addition & 0 deletions packages/fern-docs/ui/src/mdx/components/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export declare namespace Button {
intent?: "none" | "primary" | "success" | "warning" | "danger";
text?: ReactNode;
href?: string;
download?: any;
}
}

Expand Down
17 changes: 17 additions & 0 deletions packages/fern-docs/ui/src/mdx/components/download/Download.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PropsWithChildren } from "react";

export function Download({
children,
src,
filename,
}: PropsWithChildren<{ src?: string; filename?: string }>) {
if (!src) {
return children;
}

return (
<a href={src} download={filename || true}>
{children}
</a>
);
}
1 change: 1 addition & 0 deletions packages/fern-docs/ui/src/mdx/components/download/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Download";
2 changes: 2 additions & 0 deletions packages/fern-docs/ui/src/mdx/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -63,6 +64,7 @@ const FERN_COMPONENTS = {
CodeGroup, // note: alias is handled in rehypeFernCode
Column,
ColumnGroup,
Download,
EndpointRequestSnippet,
EndpointResponseSnippet,
Feature,
Expand Down
58 changes: 58 additions & 0 deletions packages/fern-docs/ui/src/mdx/plugins/rehype-download.test.ts
Original file line number Diff line number Diff line change
@@ -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(`<Download><Button /></Download>`).hast;
run(ast);
expect(hastToMarkdown(ast)).toMatchInlineSnapshot(
`
"<Download>
<Button />
</Download>
"
`
);
});

it("should convert <Download> elements into <Button> elements with the `href` and `download` attributes set", () => {
const ast = toTree(
`<Download src="https://example.com/file.txt"><Button>Download me</Button></Download>`
).hast;
run(ast);
expect(hastToMarkdown(ast)).toMatchInlineSnapshot(
`
"<Button href="https://example.com/file.txt" download>Download me</Button>
"
`
);
});

it("should include the filename in the `download` attribute", () => {
const ast = toTree(
`<Download src="https://example.com/file.txt" filename="my-file"><Button>Download me</Button></Download>`
).hast;
run(ast);
expect(hastToMarkdown(ast)).toMatchInlineSnapshot(
`
"<Button href="https://example.com/file.txt" download="my-file">Download me</Button>
"
`
);
});

it("should be a noop if the child is not a <Button>", () => {
const ast = toTree(
`<Download src="https://example.com/file.txt"><strong>Download me</strong></Download>`
).hast;
run(ast);
expect(hastToMarkdown(ast)).toMatchInlineSnapshot(`
"<Download src="https://example.com/file.txt">
<strong>Download me</strong>
</Download>
"
`);
});
});
62 changes: 62 additions & 0 deletions packages/fern-docs/ui/src/mdx/plugins/rehype-download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
CONTINUE,
Hast,
isMdxJsxAttribute,
isMdxJsxElementHast,
SKIP,
visit,
} from "@fern-docs/mdx";

/**
* The goal of this plugin is to squeeze the <Download> element into a <Button> element with the `href` and `download` attributes set.
* This is to avoid <a><button /></a> which is invalid HTML.
*/
export function rehypeDownload() {
return (ast: Hast.Root) => {
visit(ast, (node, index, parent) => {
if (!isMdxJsxElementHast(node) || !parent || index == null) {
return CONTINUE;
}

if (
node.name === "Download" &&
node.children.length === 1 &&
node.children[0]
) {
const child = node.children[0];
if (isMdxJsxElementHast(child) && child.name === "Button") {
const srcAttr = node.attributes
.filter(isMdxJsxAttribute)
.find((attr) => attr.name === "src");

if (!srcAttr) {
return CONTINUE;
}

const filenameAttr = node.attributes
.filter(isMdxJsxAttribute)
.find((attr) => attr.name === "filename");

// inject the src attribute as a href
child.attributes.push({
type: "mdxJsxAttribute",
name: "href",
value: srcAttr.value,
});

// force the button to be a download
child.attributes.push({
type: "mdxJsxAttribute",
name: "download",
value: filenameAttr?.value,
});

parent.children[index] = child;

return SKIP;
}
}
return CONTINUE;
});
};
}
Loading

0 comments on commit f3b6571

Please sign in to comment.