blog/plugin/remark-agda.ts

175 lines
4.7 KiB
TypeScript
Raw Permalink Normal View History

2024-06-26 23:18:47 +00:00
import type { RootContent } from "hast";
2023-10-11 16:48:34 +00:00
import { fromMarkdown } from "mdast-util-from-markdown";
2023-10-11 17:47:18 +00:00
import { fromHtml } from "hast-util-from-html";
import { toHtml } from "hast-util-to-html";
2023-10-11 16:48:34 +00:00
import { spawnSync } from "node:child_process";
2024-06-27 05:22:46 +00:00
import {
mkdirSync,
mkdtempSync,
readFileSync,
2024-06-29 01:48:21 +00:00
existsSync,
2024-06-27 05:22:46 +00:00
readdirSync,
copyFileSync,
writeFileSync,
} from "node:fs";
2023-10-11 16:48:34 +00:00
import { tmpdir } from "node:os";
2024-09-18 21:22:37 +00:00
import { join, parse, resolve, basename } from "node:path";
2023-10-11 17:47:18 +00:00
import { visit } from "unist-util-visit";
2024-06-27 05:22:46 +00:00
import type { RemarkPlugin } from "@astrojs/markdown-remark";
interface Options {
base: string;
outDir: string;
publicDir: string;
}
const remarkAgda: RemarkPlugin = ({ base, publicDir }: Options) => {
const destDir = join(publicDir, "generated", "agda");
mkdirSync(destDir, { recursive: true });
2023-10-11 16:48:34 +00:00
2024-04-05 15:34:32 +00:00
return (tree, { history }) => {
2024-06-26 23:18:47 +00:00
const path: string = history[history.length - 1]!;
2023-10-11 16:48:34 +00:00
if (!(path.endsWith(".lagda.md") || path.endsWith(".agda"))) return;
2024-11-27 22:38:34 +00:00
// console.log("AGDA:processing path", path);
2024-06-26 23:18:47 +00:00
2023-10-11 16:48:34 +00:00
const tempDir = mkdtempSync(join(tmpdir(), "agdaRender."));
2024-06-27 05:22:46 +00:00
const agdaOutDir = join(tempDir, "output");
mkdirSync(agdaOutDir, { recursive: true });
2023-10-11 16:48:34 +00:00
const childOutput = spawnSync(
"agda",
2024-04-05 15:34:32 +00:00
[
"--html",
2024-06-27 05:22:46 +00:00
`--html-dir=${agdaOutDir}`,
2024-04-05 15:34:32 +00:00
"--highlight-occurrences",
"--html-highlight=code",
path,
],
2023-10-11 16:48:34 +00:00
{},
);
2024-09-16 00:38:05 +00:00
// Locate output file
const directory = readdirSync(agdaOutDir);
const outputFilename = directory.find((name) => name.endsWith(".md"));
if (childOutput.status !== 0 || outputFilename === undefined) {
2024-06-29 01:48:21 +00:00
throw new Error(
2024-09-16 00:38:05 +00:00
`Agda output:
2024-06-29 01:48:21 +00:00
Stdout:
${childOutput.stdout}
Stderr:
2024-09-16 00:38:05 +00:00
${childOutput.stderr}
`,
2024-06-29 01:48:21 +00:00
{
cause: childOutput.error,
},
);
}
2024-09-16 00:38:05 +00:00
const outputFile = join(agdaOutDir, outputFilename);
2024-06-29 01:48:21 +00:00
// // TODO: Handle child output
// console.error("--AGDA OUTPUT--");
// console.error(childOutput);
// console.error(childOutput.stdout?.toString());
// console.error(childOutput.stderr?.toString());
// console.error("--AGDA OUTPUT--");
2024-06-27 05:22:46 +00:00
const referencedFiles = new Set();
for (const file of readdirSync(agdaOutDir)) {
referencedFiles.add(file);
const fullPath = join(agdaOutDir, file);
const fullDestPath = join(destDir, file);
if (file.endsWith(".html")) {
const src = readFileSync(fullPath);
writeFileSync(
fullDestPath,
`
<!DOCTYPE html>
<html>
<head>
2024-12-17 22:50:57 +00:00
<meta charset="utf-8" />
2024-06-27 05:22:46 +00:00
<link rel="stylesheet" href="${base}generated/agda/Agda.css" />
</head>
<body>
<pre class="Agda">
${src}
</pre>
</body>
</html>
`,
);
} else {
copyFileSync(fullPath, fullDestPath);
}
}
2024-06-27 00:17:01 +00:00
2024-11-27 22:38:34 +00:00
const htmlname = basename(resolve(outputFile)).replace(/(\.lagda)?\.md/, ".html");
2023-10-11 16:48:34 +00:00
2024-09-16 00:38:05 +00:00
const doc = readFileSync(outputFile);
2024-06-26 23:18:47 +00:00
// This is the post-processed markdown with HTML code blocks replacing the Agda code blocks
2023-10-11 16:48:34 +00:00
const tree2 = fromMarkdown(doc);
2023-10-11 17:47:18 +00:00
const collectedCodeBlocks: RootContent[] = [];
visit(tree2, "html", (node) => {
const html = fromHtml(node.value, { fragment: true });
2023-10-11 16:48:34 +00:00
2023-10-11 17:47:18 +00:00
const firstChild: RootContent = html.children[0]!;
visit(html, "element", (node) => {
if (node.tagName !== "a") return;
2024-06-27 05:22:46 +00:00
if (node.properties.href) {
// Trim off end
const [href, hash, ...rest] = node.properties.href.split("#");
if (rest.length > 0) throw new Error("come look at this");
2024-09-18 21:22:37 +00:00
if (href === htmlname) {
node.properties.href = `#${hash}`;
}
2024-06-27 05:22:46 +00:00
if (referencedFiles.has(href)) {
node.properties.href = `${base}generated/agda/${href}${hash ? `#${hash}` : ""}`;
node.properties.target = "_blank";
}
2023-10-11 17:47:18 +00:00
}
});
if (!firstChild?.properties?.className?.includes("Agda")) return;
const stringContents = toHtml(firstChild);
collectedCodeBlocks.push({
contents: stringContents,
});
});
let idx = 0;
2024-09-18 09:47:52 +00:00
try {
visit(tree, "code", (node) => {
if (!(node.lang === null || node.lang === "agda")) return;
2023-10-11 17:47:18 +00:00
2024-09-18 09:47:52 +00:00
if (idx > collectedCodeBlocks.length) {
throw new Error("failed");
}
node.type = "html";
node.value = collectedCodeBlocks[idx].contents;
idx += 1;
});
} catch (e) {
2024-09-18 11:01:37 +00:00
// TODO: Figure out a way to handle this correctly
// Possibly by diffing?
2024-11-27 22:38:34 +00:00
console.log("Mismatch in number of args. Perhaps there was an empty block?");
2024-09-18 09:47:52 +00:00
}
2023-10-11 16:48:34 +00:00
};
};
export default remarkAgda;