feat($lib/docs): introduce new docs pipeline

This commit is contained in:
mei (ckie) 2024-10-27 04:04:00 +02:00
parent b9b3d2880c
commit abe45c4ade
Signed by: ckie
GPG key ID: 13E79449C0525215
8 changed files with 159 additions and 9 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -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"

32
src/lib/docs/index.ts Normal file
View file

@ -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;
}

View file

@ -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
}
];
}
})
});
};
};

View file

@ -2,7 +2,7 @@
let { children } = $props();
</script>
<div class="Layout-docs-root flex max-xl:flex-col lg:gap-2 justify-around">
<div class="Layout-docs-root flex max-xl:flex-col lg:gap-8 justify-around">
<div class="grow xl:relative">
<nav class="mt-12 xl:absolute lg:right-0">
<ul
@ -17,13 +17,13 @@
</ul>
</nav>
</div>
<div class="Layout-docs-content w-[700px] max-w-[700px] text-xl">{@render children?.()}</div>
<div class="Layout-docs-content max-w-[740px] text-lg">{@render children?.()}</div>
<div class="grow"></div>
</div>
<style>
:global(.Layout-docs-content p:not(.no-docs-style)) {
@apply tracking-tight mb-1;
@apply mb-1;
}
:global(.Layout-docs-content h1:not(.no-docs-style)) {
@ -33,4 +33,16 @@
:global(.Layout-docs-content h2:not(.no-docs-style)) {
@apply text-teal-600 text-2xl font-semibold my-1;
}
:global(.Layout-docs-content h3:not(.no-docs-style)) {
@apply text-teal-600 text-xl font-semibold my-1;
}
:global(.Layout-docs-content h4:not(.no-docs-style)) {
@apply text-teal-700 font-semibold my-1;
}
:global(.Layout-docs-content [id]:not(.no-docs-style) > a > .icon.icon-link) {
@apply after:opacity-10 after:hover:opacity-100 after:inline-block after:font-sans after:content-['¶'] after:-ms-[1em] after:me-2 after:align-middle after:text-3xl after:text-zinc-400;
}
</style>

View file

@ -0,0 +1,16 @@
import type { PageServerLoad } from "./$types";
import { join } from "path";
import { renderToHtml } from "$lib/docs";
export const load: PageServerLoad = async (event) => {
const userPath = event.params.rest;
// we shouldn't need this, but it doesn't hurt.
if (userPath.split("/").find(c => /^\.*$/.test(c)))
throw new Error("url is sus");
const base = join(process.cwd(), ".petalpkgs");
return {
html: await renderToHtml({ base, path: userPath })
};
};

View file

@ -0,0 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
</script>
{@html $page.data.html}

View file

@ -5,8 +5,12 @@ import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit(), enhancedImages()],
server: {
watch: {
// absurdly big
ignored: (file) => file.includes("/.petalpkgs")
},
fs: {
allow: ["./tailwind.config.js"]
allow: ["./tailwind.config.js", "./.petalpkgs"]
}
}
});