Skip to content

Commit 8df913d

Browse files
committed
custom rendering of MDX
1 parent 1635863 commit 8df913d

9 files changed

Lines changed: 150 additions & 9 deletions

File tree

app/routes/BlogArticleRoute.res

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
type loaderData = {
2-
content: string,
2+
compiledMdx: CompiledMdx.t,
33
blogPost: BlogApi.post,
44
title: string,
55
}
@@ -11,13 +11,17 @@ let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
1111
~dir="markdown-pages/blog",
1212
~alias="blog",
1313
)
14-
let {content, frontmatter} = await MdxFile.loadFile(filePath)
14+
15+
let raw = await Node.Fs.readFile(filePath, "utf-8")
16+
let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw)
1517

1618
let frontmatter = switch BlogFrontmatter.decode(frontmatter) {
1719
| Ok(fm) => fm
1820
| Error(msg) => JsError.throwWithMessage(msg)
1921
}
2022

23+
let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins)
24+
2125
let archived = filePath->String.includes("/archived/")
2226

2327
let slug =
@@ -35,18 +39,16 @@ let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
3539
}
3640

3741
{
38-
content,
42+
compiledMdx,
3943
blogPost,
4044
title: `${frontmatter.title} | ReScript Blog`,
4145
}
4246
}
4347

4448
let default = () => {
45-
let {content, blogPost: {frontmatter, archived, path}} = ReactRouter.useLoaderData()
49+
let {compiledMdx, blogPost: {frontmatter, archived, path}} = ReactRouter.useLoaderData()
4650

4751
<BlogArticle frontmatter isArchived=archived path>
48-
<ReactMarkdown components=MarkdownComponents.default rehypePlugins=[Rehype.Plugin(Rehype.raw)]>
49-
content
50-
</ReactMarkdown>
52+
<MdxContent compiledMdx />
5153
</BlogArticle>
5254
}

app/routes/BlogArticleRoute.resi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
type loaderData = {
2-
content: string,
2+
compiledMdx: CompiledMdx.t,
33
blogPost: BlogApi.post,
44
title: string,
55
}

src/MdxFile.res

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@ type fileData = {
33
frontmatter: JSON.t,
44
}
55

6+
type compileInput = {value: string, path: string}
7+
type compileOptions = {
8+
outputFormat: string,
9+
remarkPlugins: array<Mdx.remarkPlugin>,
10+
}
11+
@module("@mdx-js/mdx")
12+
external compile: (compileInput, compileOptions) => promise<CompiledMdx.compileResult> = "compile"
13+
14+
@module("remark-frontmatter") external remarkFrontmatter: Mdx.remarkPlugin = "default"
15+
16+
let compileMdx = async (content, ~filePath, ~remarkPlugins=[]) => {
17+
let compiled = await compile(
18+
{value: content, path: filePath},
19+
{
20+
outputFormat: "function-body",
21+
remarkPlugins: [remarkFrontmatter, ...remarkPlugins],
22+
},
23+
)
24+
compiled->CompiledMdx.fromCompileResult
25+
}
26+
627
let resolveFilePath = (pathname, ~dir, ~alias) => {
728
let path = if pathname->String.startsWith("/") {
829
pathname->String.slice(~start=1, ~end=String.length(pathname))

src/MdxFile.resi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,10 @@ let loadFile: string => promise<fileData>
1717
* → ["blog/release-12-0-0", "blog/archived/some-post", ...]
1818
*/
1919
let scanPaths: (~dir: string, ~alias: string) => array<string>
20+
21+
/** Compile raw MDX content into a function-body string using @mdx-js/mdx. */
22+
let compileMdx: (
23+
string,
24+
~filePath: string,
25+
~remarkPlugins: array<Mdx.remarkPlugin>=?,
26+
) => promise<CompiledMdx.t>

src/bindings/Rehype.res

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,3 @@ type rec rehypePlugin =
1313
| WithOptions(array<pluginOrOption>)
1414

1515
@module("rehype-slug") external slug: plugin = "default"
16-
@module("rehype-raw") external raw: plugin = "default"

src/common/CompiledMdx.res

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type t = string
2+
3+
type compileResult
4+
5+
@send external fromCompileResult: compileResult => t = "toString"

src/common/CompiledMdx.resi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type t
2+
3+
type compileResult
4+
5+
@send external fromCompileResult: compileResult => t = "toString"

src/components/MdxContent.res

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// ---------------------------------------------------------------------------
2+
// JSX runtime values needed by runSync
3+
// ---------------------------------------------------------------------------
4+
5+
// We re-import the jsx-runtime exports as opaque values so we can pass them
6+
// through to runSync without running into ReScript's monomorphisation of
7+
// the polymorphic `React.jsx` / `React.jsxs` signatures.
8+
/**
9+
* MdxContent — renders compiled MDX content as a React component.
10+
*
11+
* Uses `runSync` from `@mdx-js/mdx` to evaluate compiled MDX (produced by
12+
* `MdxFile.compileMdx`) and renders the result with a shared component map.
13+
*/
14+
type jsxRuntimeValue
15+
16+
@module("react/jsx-runtime") external fragment: jsxRuntimeValue = "Fragment"
17+
@module("react/jsx-runtime") external jsx: jsxRuntimeValue = "jsx"
18+
@module("react/jsx-runtime") external jsxs: jsxRuntimeValue = "jsxs"
19+
20+
@val @scope(("import", "meta")) external importMetaUrl: string = "url"
21+
22+
// ---------------------------------------------------------------------------
23+
// @mdx-js/mdx runSync binding
24+
// ---------------------------------------------------------------------------
25+
26+
type runOptions = {
27+
@as("Fragment") fragment: jsxRuntimeValue,
28+
jsx: jsxRuntimeValue,
29+
jsxs: jsxRuntimeValue,
30+
baseUrl: string,
31+
}
32+
33+
type mdxModule
34+
35+
@module("@mdx-js/mdx")
36+
external runSync: (CompiledMdx.t, runOptions) => mdxModule = "runSync"
37+
38+
@get external getDefault: mdxModule => React.component<{..}> = "default"
39+
40+
let runOptions = {
41+
fragment,
42+
jsx,
43+
jsxs,
44+
baseUrl: importMetaUrl,
45+
}
46+
47+
// ---------------------------------------------------------------------------
48+
// Shared MDX component map
49+
// ---------------------------------------------------------------------------
50+
51+
let components = {
52+
// Standard HTML element overrides
53+
"a": Markdown.A.make,
54+
"blockquote": Markdown.Blockquote.make,
55+
"code": Markdown.Code.make,
56+
"h1": Markdown.H1.make,
57+
"h2": Markdown.H2.make,
58+
"h3": Markdown.H3.make,
59+
"h4": Markdown.H4.make,
60+
"h5": Markdown.H5.make,
61+
"hr": Markdown.Hr.make,
62+
"li": Markdown.Li.make,
63+
"ol": Markdown.Ol.make,
64+
"p": Markdown.P.make,
65+
"pre": Markdown.Pre.make,
66+
"strong": Markdown.Strong.make,
67+
"table": Markdown.Table.make,
68+
"th": Markdown.Th.make,
69+
"thead": Markdown.Thead.make,
70+
"td": Markdown.Td.make,
71+
"ul": Markdown.Ul.make,
72+
// Custom MDX components
73+
"Cite": Markdown.Cite.make,
74+
"CodeTab": Markdown.CodeTab.make,
75+
"Image": Markdown.Image.make,
76+
"Info": Markdown.Info.make,
77+
"Intro": Markdown.Intro.make,
78+
"UrlBox": Markdown.UrlBox.make,
79+
"Video": Markdown.Video.make,
80+
"Warn": Markdown.Warn.make,
81+
"CommunityContent": CommunityContent.make,
82+
"WarningTable": WarningTable.make,
83+
"Docson": DocsonLazy.make,
84+
"Suspense": React.Suspense.make,
85+
}
86+
87+
// ---------------------------------------------------------------------------
88+
// React component
89+
// ---------------------------------------------------------------------------
90+
91+
@react.component
92+
let make = (~compiledMdx: CompiledMdx.t) => {
93+
let element = React.useMemo(() => {
94+
let mdxModule = runSync(compiledMdx, runOptions)
95+
let content = getDefault(mdxModule)
96+
React.jsx(content, {"components": components})
97+
}, [compiledMdx])
98+
99+
element
100+
}

src/components/MdxContent.resi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@react.component
2+
let make: (~compiledMdx: CompiledMdx.t) => React.element

0 commit comments

Comments
 (0)