Add semantic block parser and PDF export
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-25 12:35:40 +05:30
parent 632543db4d
commit 58310e1944
47 changed files with 3078 additions and 0 deletions

64
scripts/export-pdf.mjs Normal file
View file

@ -0,0 +1,64 @@
import { existsSync, mkdirSync } from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
const filterPath = path.join(repoRoot, "scripts", "pandoc-filters", "semantic-blocks.lua");
const preamblePath = path.join(repoRoot, "scripts", "pandoc-templates", "semantic-preamble.tex");
function usage() {
console.error("Usage: node scripts/export-pdf.mjs <post.md> [--out output.pdf]");
}
function parseArgs(argv) {
const args = [...argv];
const input = args.find((arg) => !arg.startsWith("--"));
const outIndex = args.indexOf("--out");
const output = outIndex >= 0 ? args[outIndex + 1] : "";
return { input, output };
}
const { input, output } = parseArgs(process.argv.slice(2));
if (!input) {
usage();
process.exit(2);
}
const inputPath = path.resolve(input);
if (!existsSync(inputPath)) {
console.error(`Input file not found: ${inputPath}`);
process.exit(2);
}
const outputPath = path.resolve(
output || path.join(path.dirname(inputPath), `${path.basename(inputPath, path.extname(inputPath))}.pdf`),
);
mkdirSync(path.dirname(outputPath), { recursive: true });
const pandocArgs = [
inputPath,
"--from",
"markdown+fenced_code_attributes+tex_math_dollars+tex_math_single_backslash+footnotes+raw_html",
"--pdf-engine=lualatex",
"--lua-filter",
filterPath,
"--include-in-header",
preamblePath,
"--output",
outputPath,
];
const result = spawnSync("pandoc", pandocArgs, { stdio: "inherit" });
if (result.error?.code === "ENOENT") {
console.error("pandoc is not installed or is not on PATH. Install pandoc and a LuaLaTeX distribution first.");
process.exit(127);
}
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
console.log(`Wrote ${outputPath}`);

View file

@ -0,0 +1,131 @@
local env_types = {
theorem = true,
lemma = true,
proposition = true,
corollary = true,
definition = true,
example = true,
remark = true,
proof = true,
}
local function has_class(el, class_name)
for _, value in ipairs(el.classes) do
if value == class_name then
return true
end
end
return false
end
local function first_env_class(el)
for _, value in ipairs(el.classes) do
if env_types[value] then
return value
end
end
return nil
end
local function latex_escape(value)
value = tostring(value or "")
value = value:gsub("\\", "\\textbackslash{}")
value = value:gsub("([{}%%$&#_])", "\\%1")
value = value:gsub("%^", "\\textasciicircum{}")
value = value:gsub("~", "\\textasciitilde{}")
return value
end
local function internal_link_to_cref(el)
if FORMAT:match("latex") and el.target:sub(1, 1) == "#" then
local label = el.target:sub(2)
if label ~= "" then
return pandoc.RawInline("latex", "\\cref{" .. label .. "}")
end
end
return nil
end
local function markdown_to_latex(markdown)
local doc = pandoc.read(
markdown or "",
"markdown+fenced_code_attributes+tex_math_dollars+tex_math_single_backslash+footnotes+raw_html"
)
doc = doc:walk({ Link = internal_link_to_cref })
return pandoc.write(doc, "latex")
end
local function label_line(label)
if label and label ~= "" then
return "\\label{" .. label .. "}\n"
end
return ""
end
local function interactive_to_latex(el)
local label = el.identifier
local caption = el.attributes.caption or el.attributes.title or ""
local fallback = el.attributes.pdf or el.attributes.fallback or el.text
local fallback_latex = markdown_to_latex(fallback)
local caption_line = ""
if caption ~= "" then
caption_line = "\\caption{" .. latex_escape(caption) .. "}\n"
end
return pandoc.RawBlock(
"latex",
table.concat({
"\\begin{figure}[htbp]\n",
"\\centering\n",
"\\fbox{\\begin{minipage}{0.86\\linewidth}\n",
fallback_latex,
"\n\\end{minipage}}\n",
caption_line,
label_line(label),
"\\end{figure}",
})
)
end
local function env_to_latex(el)
local env_type = el.attributes.type or first_env_class(el)
if not env_type or not env_types[env_type] then
return nil
end
local title = el.attributes.title or el.attributes.name or ""
local begin_env = "\\begin{" .. env_type .. "}"
if title ~= "" then
begin_env = begin_env .. "[" .. latex_escape(title) .. "]"
end
return pandoc.RawBlock(
"latex",
table.concat({
begin_env,
"\n",
label_line(el.identifier),
markdown_to_latex(el.text),
"\n\\end{",
env_type,
"}",
})
)
end
function CodeBlock(el)
if FORMAT:match("latex") and has_class(el, "interactive") then
return interactive_to_latex(el)
end
if FORMAT:match("latex") and (has_class(el, "env") or has_class(el, "latex-env") or first_env_class(el)) then
return env_to_latex(el)
end
return nil
end
function Link(el)
return internal_link_to_cref(el)
end

View file

@ -0,0 +1,44 @@
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{amsthm}
\theoremstyle{plain}
\newtheorem{theorem}{Theorem}
\newtheorem{lemma}[theorem]{Lemma}
\newtheorem{proposition}[theorem]{Proposition}
\newtheorem{corollary}[theorem]{Corollary}
\theoremstyle{definition}
\newtheorem{definition}[theorem]{Definition}
\newtheorem{example}[theorem]{Example}
\theoremstyle{remark}
\newtheorem{remark}[theorem]{Remark}
\newcommand{\SemanticLoadCleveref}{%
\usepackage{cleveref}%
\crefname{theorem}{theorem}{theorems}%
\Crefname{theorem}{Theorem}{Theorems}%
\crefname{lemma}{lemma}{lemmas}%
\Crefname{lemma}{Lemma}{Lemmas}%
\crefname{proposition}{proposition}{propositions}%
\Crefname{proposition}{Proposition}{Propositions}%
\crefname{corollary}{corollary}{corollaries}%
\Crefname{corollary}{Corollary}{Corollaries}%
\crefname{definition}{definition}{definitions}%
\Crefname{definition}{Definition}{Definitions}%
\crefname{example}{example}{examples}%
\Crefname{example}{Example}{Examples}%
\crefname{remark}{remark}{remarks}%
\Crefname{remark}{Remark}{Remarks}%
\crefname{figure}{figure}{figures}%
\Crefname{figure}{Figure}{Figures}%
}
\makeatletter
\@ifpackageloaded{hyperref}{%
\SemanticLoadCleveref
}{%
\AddToHook{package/hyperref/after}{\SemanticLoadCleveref}%
}
\makeatother