Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
940 changes: 0 additions & 940 deletions .yarn/releases/yarn-4.13.0.cjs

This file was deleted.

6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ let default: unit => React.element
- **Lefthook** runs `yarn format` on pre-commit (auto-stages fixed files).
- Generated `.mjs`/`.jsx` output files from ReScript are git-tracked but excluded from Prettier.

## Pull Requests and Commits

- Use conventional commits format for commit messages (e.g. `feat: add new API docs`, `fix: resolve loader data issue`).
- Commit bodies should explain what changed with some concise details
- PR descriptions should provide context for the change, a summary of the changes with descriptions, and reference any related issues.

## Important Warnings

- Do **not** modify generated `.jsx` / `.mjs` files directly — they are ReScript compiler output.
Expand Down
13 changes: 12 additions & 1 deletion app/routes.res
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,20 @@ let blogArticleRoutes =
route(path, "./routes/BlogArticleRoute.jsx", ~options={id: path})
)

let docsManualRoutes =
MdxFile.scanPaths(~dir="markdown-pages/docs/manual", ~alias="docs/manual")
->Array.filter(path => !String.includes(path, "docs/manual/api"))
->Array.map(path => route(path, "./routes/DocsManualRoute.jsx", ~options={id: path}))

let mdxRoutes = mdxRoutes("./routes/MdxRoute.jsx")->Array.filter(r =>
!(
r.path
->Option.map(path => path === "blog" || String.startsWith(path, "blog/"))
->Option.map(path =>
path === "blog" ||
String.startsWith(path, "blog/") ||
((path === "docs/manual" || String.startsWith(path, "docs/manual/")) &&
path !== "docs/manual/api")
)
->Option.getOr(false)
)
)
Expand All @@ -56,6 +66,7 @@ let default = [
...stdlibRoutes,
...beltRoutes,
...blogArticleRoutes,
...docsManualRoutes,
...mdxRoutes,
route("*", "./routes/NotFoundRoute.jsx"),
]
159 changes: 159 additions & 0 deletions app/routes/DocsManualRoute.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
type loaderData = {
compiledMdx: CompiledMdx.t,
categories: array<SidebarLayout.Sidebar.Category.t>,
entries: array<TableOfContents.entry>,
title: string,
description: string,
filePath: string,
}

// Build sidebar categories from all manual docs, sorted by their "order" field in frontmatter
let manualTableOfContents = async () => {
let groups =
(await MdxFile.loadAllAttributes(~dir="markdown-pages/docs"))
->Mdx.filterMdxPages("docs/manual")
->Mdx.groupBySection
->Dict.mapValues(values =>
values->Mdx.sortSection->SidebarHelpers.convertToNavItems("/docs/manual")
)

SidebarHelpers.getAllGroups(
groups,
[
"Overview",
"Guides",
"Language Features",
"JavaScript Interop",
"Build System",
"Advanced Features",
],
)
}

let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
let {pathname} = WebAPI.URL.make(~url=request.url)
let filePath = MdxFile.resolveFilePath(
(pathname :> string),
~dir="markdown-pages/docs/manual",
~alias="docs/manual",
)

let raw = await Node.Fs.readFile(filePath, "utf-8")
let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw)

let description = switch frontmatter {
| Object(dict) =>
switch dict->Dict.get("description") {
| Some(String(s)) => s
| _ => ""
}
| _ => ""
}

let title = switch frontmatter {
| Object(dict) =>
switch dict->Dict.get("title") {
| Some(String(s)) => s
| _ => ""
}
| _ => ""
}

let categories = await manualTableOfContents()

let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins)

// Build table of contents entries from markdown headings
let markdownTree = Mdast.fromMarkdown(raw)
let tocResult = Mdast.toc(markdownTree, {maxDepth: 2})

let headers = Dict.make()
Mdast.reduceHeaders(tocResult.map, headers)

let entries =
headers
->Dict.toArray
->Array.map(((header, url)): TableOfContents.entry => {
header,
href: (url :> string),
})
->Array.slice(~start=2) // skip document entry and H1 title, keep h2 sections

{
compiledMdx,
categories,
entries,
title: `${title} | ReScript Language Manual`,
description,
filePath,
}
}

let default = () => {
let {pathname} = ReactRouter.useLocation()
let {compiledMdx, categories, entries, title, description, filePath} = ReactRouter.useLoaderData()

let breadcrumbs = list{
{Url.name: "Docs", href: "/docs/manual/introduction"},
{
Url.name: "Language Manual",
href: "/docs/manual/introduction",
},
}

let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master/${filePath}`

let sidebarContent =
<aside className="px-4 w-full block">
<div className="flex justify-between items-baseline">
<div className="flex flex-col text-fire font-medium">
<VersionSelect />
</div>
<button
className="flex items-center" onClick={_ => NavbarUtils.closeMobileTertiaryDrawer()}
>
<Icon.Close />
</button>
</div>
<div className="mb-56">
{categories
->Array.map(category => {
let isItemActive = (navItem: SidebarLayout.Sidebar.NavItem.t) =>
navItem.href === (pathname :> string)
let getActiveToc = (navItem: SidebarLayout.Sidebar.NavItem.t) =>
if navItem.href === (pathname :> string) {
Some({TableOfContents.title, entries})
} else {
None
}
<div key=category.name>
<SidebarLayout.Sidebar.Category
isItemActive
getActiveToc
category
onClick={_ => NavbarUtils.closeMobileTertiaryDrawer()}
/>
</div>
})
->React.array}
</div>
</aside>

<>
<Meta title description />
<NavbarSecondary />
<NavbarTertiary sidebar=sidebarContent>
<SidebarLayout.BreadCrumbs crumbs=breadcrumbs />
<a
href=editHref className="inline text-14 hover:underline text-fire" rel="noopener noreferrer"
>
{React.string("Edit")}
</a>
</NavbarTertiary>
<DocsLayout categories activeToc={title, entries}>
<div className="markdown-body">
<MdxContent compiledMdx />
</div>
</DocsLayout>
</>
}
12 changes: 12 additions & 0 deletions app/routes/DocsManualRoute.resi
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type loaderData = {
compiledMdx: CompiledMdx.t,
categories: array<SidebarLayout.Sidebar.Category.t>,
entries: array<TableOfContents.entry>,
title: string,
description: string,
filePath: string,
}

let loader: ReactRouter.Loader.t<loaderData>

let default: unit => React.element
32 changes: 31 additions & 1 deletion src/MdxFile.res
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ let resolveFilePath = (pathname, ~dir, ~alias) => {
} else {
path
}
relativePath ++ ".mdx"
relativePath->String.replaceAll("\\", "/") ++ ".mdx"
}

let loadFile = async filePath => {
Expand Down Expand Up @@ -74,3 +74,33 @@ let scanPaths = (~dir, ~alias) => {
alias ++ "/" ++ relativePath
})
}

// Convert frontmatter JSON dict to Mdx.attributes
// This is the same unsafe approach as react-router-mdx — frontmatter YAML
// becomes a JS object that we type as Mdx.attributes. Fields not present
// in the frontmatter (e.g. blog-specific `author`, `date`) are undefined at
// runtime, which is fine because docs/community code never accesses them.
external dictToAttributes: Dict.t<JSON.t> => Mdx.attributes = "%identity"

let loadAllAttributes = async (~dir) => {
let files = scanDir(dir, dir)
await Promise.all(
files->Array.map(async relativePath => {
let fullPath = Node.Path.join2(dir, relativePath ++ ".mdx")->String.replaceAll("\\", "/")
let raw = await Node.Fs.readFile(fullPath, "utf-8")
let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw)

let dict = switch frontmatter {
| Object(dict) => dict
| _ => Dict.make()
}

// Add path and slug fields (same as react-router-mdx does)
dict->Dict.set("path", JSON.String(fullPath))
let slug = Node.Path.basename(relativePath)
dict->Dict.set("slug", JSON.String(slug))

dictToAttributes(dict)
}),
)
}
6 changes: 6 additions & 0 deletions src/MdxFile.resi
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ let compileMdx: (
~filePath: string,
~remarkPlugins: array<Mdx.remarkPlugin>=?,
) => promise<CompiledMdx.t>

/** Scan all .mdx files in a directory, parse frontmatter only, and return
* as Mdx.attributes with `path` and `slug` fields populated.
* Replaces `react-router-mdx`'s `loadAllMdx`.
*/
let loadAllAttributes: (~dir: string) => promise<array<Mdx.attributes>>
23 changes: 23 additions & 0 deletions src/SidebarHelpers.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
let convertToNavItems = (items, rootPath) =>
Array.map(items, (item): SidebarLayout.Sidebar.NavItem.t => {
let href = switch item.Mdx.slug {
| Some(slug) => `${rootPath}/${slug}`
| None => rootPath
}
{
name: item.title,
href,
}
})

let getGroup = (groups, groupName): SidebarLayout.Sidebar.Category.t => {
{
name: groupName,
items: groups
->Dict.get(groupName)
->Option.getOr([]),
}
}

let getAllGroups = (groups, groupNames): array<SidebarLayout.Sidebar.Category.t> =>
groupNames->Array.map(item => getGroup(groups, item))
14 changes: 14 additions & 0 deletions src/SidebarHelpers.resi
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** Convert Mdx.attributes to sidebar nav items, building hrefs from rootPath + slug. */
let convertToNavItems: (array<Mdx.attributes>, string) => array<SidebarLayout.Sidebar.NavItem.t>

/** Get a single sidebar category by name from a dict of grouped nav items. */
let getGroup: (
Dict.t<array<SidebarLayout.Sidebar.NavItem.t>>,
string,
) => SidebarLayout.Sidebar.Category.t

/** Get multiple sidebar categories by name from a dict of grouped nav items. */
let getAllGroups: (
Dict.t<array<SidebarLayout.Sidebar.NavItem.t>>,
array<string>,
) => array<SidebarLayout.Sidebar.Category.t>
Loading