taylorchen_obsidian-press/scripts/verify-code-export.mjs

128 lines
3.3 KiB
JavaScript
Raw Normal View History

2026-04-28 23:36:23 +00:00
import { execFile } from "node:child_process";
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const root = path.resolve(import.meta.dirname, "..");
const outDir = path.join(os.tmpdir(), `obsidian-press-code-export-${Date.now()}`);
const inputPath = path.join(outDir, "code-format.md");
const pdfPath = path.join(outDir, "code-format.pdf");
const listingsHeaderPath = path.join(outDir, "listings-wrap.tex");
const pngPrefix = path.join(outDir, "code-format-page");
const pngPath = `${pngPrefix}.png`;
const env = {
...process.env,
PATH: `/Library/TeX/texbin:/opt/homebrew/bin:/usr/local/bin:${process.env.PATH || ""}`,
TMPDIR: outDir,
TMP: outDir,
TEMP: outDir,
LANG: process.env.LANG || "en_US.UTF-8",
LC_ALL: process.env.LC_ALL || "en_US.UTF-8",
};
const input = `# Code export verification
The fenced block below must keep Obsidian-like syntax untouched.
\`\`\`ts
const marker = "[[wikilink]] ==highlight== %%comment%%";
const longLine = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
function add(a: number, b: number) {
return a + b;
}
\`\`\`
Inline \`const inline = "==keep=="\` should remain inline code.
`;
await mkdir(outDir, { recursive: true });
await writeFile(inputPath, input, "utf8");
await writeFile(
listingsHeaderPath,
String.raw`\lstset{
breaklines=true,
breakatwhitespace=false,
columns=fullflexible,
keepspaces=true,
basicstyle=\ttfamily\footnotesize,
frame=single,
backgroundcolor=\color[HTML]{F6F8FA}
}
`,
"utf8"
);
await execFileAsync(
"pandoc",
[
inputPath,
"-o",
pdfPath,
"--from",
"markdown+fenced_code_blocks+fenced_code_attributes+backtick_code_blocks+pipe_tables+grid_tables+raw_html+tex_math_dollars",
"--to",
"pdf",
"--standalone",
"--highlight-style=tango",
"--listings",
"-H",
listingsHeaderPath,
"--pdf-engine=xelatex",
"-V",
"geometry:margin=25mm",
"-V",
"fontsize=11pt",
"-V",
"papersize=a4",
"-V",
"colorlinks=true",
],
{ cwd: root, env }
);
const { stdout: extractedText } = await execFileAsync(
"pdftotext",
[pdfPath, "-"],
{ cwd: root, env }
);
for (const expected of [
"[[wikilink]] ==highlight== %%comment%%",
"const longLine =",
"function add(a: number, b: number)",
"const inline = \"==keep==\"",
]) {
if (!extractedText.includes(expected)) {
throw new Error(`PDF text is missing expected code content: ${expected}`);
}
}
await execFileAsync("pdftoppm", ["-png", "-singlefile", pdfPath, pngPrefix], {
cwd: root,
env,
});
const [pdfStats, pngStats] = await Promise.all([stat(pdfPath), stat(pngPath)]);
if (pdfStats.size <= 0 || pngStats.size <= 0) {
throw new Error("PDF or rendered PNG is empty");
}
const pngHeader = await readFile(pngPath, { encoding: null });
if (
pngHeader.length < 8 ||
pngHeader[0] !== 0x89 ||
pngHeader[1] !== 0x50 ||
pngHeader[2] !== 0x4e ||
pngHeader[3] !== 0x47
) {
throw new Error("Rendered PDF preview is not a PNG file");
}
console.log("Code export verification passed");
console.log(`PDF: ${pdfPath}`);
console.log(`Preview: ${pngPath}`);