Skip to content
Closed
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
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
8 changes: 8 additions & 0 deletions app/routes.res
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ 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 communityRoutes =
MdxFile.scanPaths(~dir="markdown-pages/community", ~alias="community")->Array.map(path =>
route(path, "./routes/CommunityRoute.jsx", ~options={id: path})
Expand All @@ -44,6 +49,8 @@ let mdxRoutes = mdxRoutes("./routes/MdxRoute.jsx")->Array.filter(r =>
->Option.map(path =>
path === "blog" ||
String.startsWith(path, "blog/") ||
(path === "docs/manual" || String.startsWith(path, "docs/manual/")) &&
path !== "docs/manual/api" ||
path === "community" ||
String.startsWith(path, "community/")
)
Expand All @@ -66,6 +73,7 @@ let default = [
...stdlibRoutes,
...beltRoutes,
...blogArticleRoutes,
...docsManualRoutes,
...communityRoutes,
...mdxRoutes,
route("*", "./routes/NotFoundRoute.jsx"),
Expand Down
168 changes: 168 additions & 0 deletions app/routes/DocsManualRoute.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
type loaderData = {
compiledMdx: CompiledMdx.t,
categories: array<SidebarLayout.Sidebar.Category.t>,
entries: array<TableOfContents.entry>,
title: string,
description: string,
filePath: string,
}

// Module-level cache to avoid re-scanning frontmatter on every request
let manualTableOfContentsCache: ref<option<array<SidebarLayout.Sidebar.Category.t>>> = ref(None)

// Build sidebar categories from manual docs, sorted by their "order" field in frontmatter
let manualTableOfContents = async () => {
switch manualTableOfContentsCache.contents {
| Some(categories) => categories
| None =>
let groups =
(await MdxFile.loadAllAttributes(~dir="markdown-pages/docs/manual"))
->SidebarHelpers.groupBySection
->Dict.mapValues(values =>
values->SidebarHelpers.sortByOrder->SidebarHelpers.convertToNavItems("/docs/manual")
)

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

manualTableOfContentsCache := Some(categories)
categories
}
}

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
61 changes: 6 additions & 55 deletions app/routes/MdxRoute.res
Original file line number Diff line number Diff line change
Expand Up @@ -63,42 +63,6 @@ let convertToNavItems = (items, rootPath) =>
}
})

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))

// These are the pages for the language manual, sorted by their "order" field in the frontmatter
let manualTableOfContents = async () => {
let groups =
(await allMdx(~filterByPaths=["markdown-pages/docs"]))
->filterMdxPages("docs/manual")
->groupBySection
->Dict.mapValues(values => values->sortSection->convertToNavItems("/docs/manual"))

// these are the categories that appear in the sidebar
let categories: array<SidebarLayout.Sidebar.Category.t> = getAllGroups(
groups,
[
"Overview",
"Guides",
"Language Features",
"JavaScript Interop",
"Build System",
"Advanced Features",
],
)

categories
}

let reactTableOfContents = async () => {
let groups =
(await allMdx(~filterByPaths=["markdown-pages/docs"]))
Expand All @@ -107,7 +71,7 @@ let reactTableOfContents = async () => {
->Dict.mapValues(values => values->sortSection->convertToNavItems("/docs/react"))

// these are the categories that appear in the sidebar
let categories: array<SidebarLayout.Sidebar.Category.t> = getAllGroups(
let categories: array<SidebarLayout.Sidebar.Category.t> = SidebarHelpers.getAllGroups(
groups,
["Overview", "Main Concepts", "Hooks & State Management", "Guides"],
)
Expand Down Expand Up @@ -148,8 +112,6 @@ let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
let categories = {
if pathname == "/docs/manual/api" {
[]
} else if pathname->String.includes("docs/manual") {
await manualTableOfContents()
} else if pathname->String.includes("docs/react") {
await reactTableOfContents()
} else {
Expand Down Expand Up @@ -199,22 +161,14 @@ let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
->Array.slice(~start=2) // skip first two entries which are the document entry and the H1 title for the page, we just want the h2 sections

let breadcrumbs =
pathname->String.includes("docs/manual")
pathname->String.includes("docs/react")
? Some(list{
{Url.name: "Docs", href: "/docs/"},
{
Url.name: "Language Manual",
href: "/docs/manual/" ++ "introduction",
Url.name: "rescript-react",
href: "/docs/react/" ++ "introduction",
},
})
: pathname->String.includes("docs/react")
? Some(list{
{Url.name: "Docs", href: "/docs/"},
{
Url.name: "rescript-react",
href: "/docs/react/" ++ "introduction",
},
})
: None

let metaTitleCategory = {
Expand Down Expand Up @@ -304,9 +258,8 @@ let default = () => {
</ApiOverviewLayout.Docs>
</>
} else if (
(pathname :> string)->String.includes("docs/manual") ||
(pathname :> string)->String.includes("docs/react") ||
(pathname :> string)->String.includes("docs/guidelines")
(pathname :> string)->String.includes("docs/guidelines")
) {
<>
<Meta title=title description={attributes.description->Nullable.getOr("")} />
Expand All @@ -315,9 +268,7 @@ let default = () => {
let breadcrumbs = loaderData.breadcrumbs->Option.map(crumbs =>
List.mapWithIndex(crumbs, (item, index) => {
if index === 0 {
if (pathname :> string)->String.includes("docs/manual") {
{...item, href: "/docs/manual/introduction"}
} else if (pathname :> string)->String.includes("docs/react") {
if (pathname :> string)->String.includes("docs/react") {
{...item, href: "/docs/react/introduction"}
} else {
item
Expand Down
46 changes: 45 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,47 @@ let scanPaths = (~dir, ~alias) => {
alias ++ "/" ++ relativePath
})
}

type sidebarEntry = {
title: string,
slug: option<string>,
section: option<string>,
order: option<int>,
path: option<string>,
}

let jsonToString = json =>
switch json {
| JSON.String(s) => Some(s)
| _ => None
}

let jsonToInt = json =>
switch json {
| JSON.Number(n) => Some(Float.toInt(n))
| _ => None
}

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()
}

{
title: dict->Dict.get("title")->Option.flatMap(jsonToString)->Option.getOr(""),
slug: Some(Node.Path.basename(relativePath)),
section: dict->Dict.get("section")->Option.flatMap(jsonToString),
order: dict->Dict.get("order")->Option.flatMap(jsonToInt),
path: Some(fullPath),
}
}),
)
}
Loading
Loading