Add semantic block parser and PDF export
Some checks are pending
Build / build (push) Waiting to run
Some checks are pending
Build / build (push) Waiting to run
This commit is contained in:
parent
632543db4d
commit
58310e1944
47 changed files with 3078 additions and 0 deletions
64
scripts/export-pdf.mjs
Normal file
64
scripts/export-pdf.mjs
Normal 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}`);
|
||||
131
scripts/pandoc-filters/semantic-blocks.lua
Normal file
131
scripts/pandoc-filters/semantic-blocks.lua
Normal 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
|
||||
44
scripts/pandoc-templates/semantic-preamble.tex
Normal file
44
scripts/pandoc-templates/semantic-preamble.tex
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue