diff --git a/bun.lockb b/bun.lockb index b7b6887..94bffbf 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index ac89b4a..ccfa50f 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@sveltejs/enhanced-img": "^0.3.9", "@sveltejs/kit": "^2.5.27", "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@types/node": "^22.8.1", "autoprefixer": "^10.4.20", "husky": "^9.1.6", "lint-staged": "^15.2.10", @@ -32,8 +33,19 @@ "type": "module", "dependencies": { "clsx": "^2.1.1", + "mdast": "^3.0.0", "moderndash": "^3.12.0", - "svelte-highlight": "^7.7.0" + "rehype-autolink-headings": "^7.1.0", + "rehype-sanitize": "^6.0.0", + "rehype-slug-custom-id": "^2.0.0", + "rehype-stringify": "^10.0.1", + "remark": "^15.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", + "svelte-highlight": "^7.7.0", + "to-vfile": "^8.0.0", + "unified": "^11.0.5", + "unist-util-flatmap": "^1.0.0" }, "lint-staged": { "{package.json,*.js,src/**.{svelte,ts,js,css}}": "prettier --write --ignore-unknown" diff --git a/src/lib/docs/index.ts b/src/lib/docs/index.ts new file mode 100644 index 0000000..e1d7baa --- /dev/null +++ b/src/lib/docs/index.ts @@ -0,0 +1,32 @@ +import { readFile } from "fs/promises"; +import rehypeStringify from "rehype-stringify"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import { unified } from "unified"; +import rehypeSlug from "rehype-slug-custom-id"; +import { join } from "path"; +import { remarkInclude } from "./remark-include"; +import rehypeAutolinkHeadings from "rehype-autolink-headings"; +// import rehypeSanitize from "rehype-sanitize"; + +export async function renderToHtml( + { base, path }: { + base: string, + path: string + } +) { + const md = await readFile(join(base, path + ".md"), { encoding: "utf-8" }); + + const html = await unified() + .use(remarkParse) + .use(remarkInclude, { resolveFrom: join(base, path.split("/").slice(0, -1).join("/")) }) + .use(remarkRehype) + .use(rehypeSlug, { enableCustomId: true }) + .use(rehypeAutolinkHeadings) + // .use(rehypeSanitize) TODO + .use(rehypeStringify) + .process(md) + .then(vfile => vfile.toString()); + + return html; +} diff --git a/src/lib/docs/remark-include.ts b/src/lib/docs/remark-include.ts new file mode 100644 index 0000000..0b54bc0 --- /dev/null +++ b/src/lib/docs/remark-include.ts @@ -0,0 +1,69 @@ +/** + * https://github.com/hashicorp/remark-plugins + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import path from "path"; +import { remark } from "remark"; +import flatMap from "unist-util-flatmap"; +import { readSync } from "to-vfile"; +import type { Node, Root, Code } from "mdast"; +import type { Plugin } from "unified"; + +export const remarkInclude: Plugin<[], Root> = ({ resolveFrom }: { resolveFrom: string }) => { + return (tree, file) => { + return flatMap(tree, (node: Code) => { + if (!(node.type == "code" + && node.lang == "{=include=}" + && node.meta !== "options")) + return [node]; + + // !== "options" because those are special, parsed from options.json: + // https://github.com/NixOS/nixpkgs/blob/d31617bedffa3e5fe067feba1c68b1a7f644cb4f/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py#L81-L102 + + console.log("awa", node); + + const includes = node.value.split("\n"); + + return includes.flatMap(included => { + // read the file contents + const includePath = path.join(resolveFrom ?? file.dirname, included); + let includeContents; + try { + includeContents = readSync(includePath, "utf8"); + } catch (err) { + throw new Error( + `The @include file path at ${includePath} was not found.\n\nInclude Location: ${file.path}:${node.position.start.line}:${node.position.start.column}` + ); + } + + // if we are including a ".md" or ".mdx" file, we add the contents as processed markdown + // if any other file type, they are embedded into a code block + if (includePath.match(/\.md(?:x)?$/)) { + // return the file contents in place of the @include + // (takes a couple steps because we're processing includes with remark) + const processor = remark(); + // use the includeMarkdown plugin to allow recursive includes + processor.use(remarkInclude, { resolveFrom }); + // Process the file contents, then return them + const ast = processor.parse(includeContents); + return processor.runSync(ast, includeContents).children; + } else { + // trim trailing newline + includeContents.contents = includeContents.contents.trim(); + + // return contents wrapped inside a "code" node + return [ + { + type: "code", + lang: includePath.match(/\.(\w+)$/)?.[1], + value: includeContents + } + ]; + } + + }) + }); + }; +}; diff --git a/src/routes/docs/+layout.svelte b/src/routes/docs/+layout.svelte index a943595..289023b 100644 --- a/src/routes/docs/+layout.svelte +++ b/src/routes/docs/+layout.svelte @@ -2,7 +2,7 @@ let { children } = $props(); -