import { basename, extname, join } from "../../deno_ral/path.ts";
import { mergeConfigs } from "../../core/config.ts";
import { texSafeFilename } from "../../core/tex.ts";
import {
kBibliography,
kCapBottom,
kCapLoc,
kCapTop,
kCitationLocation,
kCiteMethod,
kClassOption,
kDefaultImageExtension,
kDocumentClass,
kEcho,
kFigCapLoc,
kFigDpi,
kFigFormat,
kFigHeight,
kFigWidth,
kHeaderIncludes,
kKeepTex,
kLang,
kNumberSections,
kPaperSize,
kPdfEngine,
kPdfStandard,
kPdfStandardApplied,
kReferenceLocation,
kShiftHeadingLevelBy,
kTblCapLoc,
kTopLevelDivision,
kWarning,
} from "../../config/constants.ts";
import { warning } from "../../deno_ral/log.ts";
import { asArray } from "../../core/array.ts";
import { Format, FormatExtras, PandocFlags } from "../../config/types.ts";
import { createFormat } from "../formats-shared.ts";
import { RenderedFile, RenderServices } from "../../command/render/types.ts";
import { ProjectConfig, ProjectContext } from "../../project/types.ts";
import { BookExtension } from "../../project/types/book/book-shared.ts";
import { readLines } from "io/read-lines";
import { TempContext } from "../../core/temp.ts";
import { isLatexPdfEngine, pdfEngine } from "../../config/pdf.ts";
import { formatResourcePath } from "../../core/resources.ts";
import { kTemplatePartials } from "../../command/render/template.ts";
import { copyTo } from "../../core/copy.ts";
import { kCodeAnnotations } from "../html/format-html-shared.ts";
import { safeModeFromFile } from "../../deno_ral/fs.ts";
import { hasLevelOneHeadings as hasL1Headings } from "../../core/lib/markdown-analysis/level-one-headings.ts";
export function pdfFormat(): Format {
return mergeConfigs(
createPdfFormat("PDF"),
{
extensions: {
book: pdfBookExtension,
},
},
);
}
export function beamerFormat(): Format {
return createFormat(
"Beamer",
"pdf",
createPdfFormat("Beamer", false, false),
{
execute: {
[kFigWidth]: 10,
[kFigHeight]: 7,
[kEcho]: false,
[kWarning]: false,
},
classoption: ["notheorems"],
},
);
}
export function latexFormat(displayName: string): Format {
return createFormat(
displayName,
"tex",
mergeConfigs(
createPdfFormat(displayName),
{
extensions: {
book: {
onSingleFilePreRender: (
format: Format,
_config?: ProjectConfig,
) => {
format.render[kKeepTex] = true;
return format;
},
formatOutputDirectory: () => {
return "book-latex";
},
},
},
},
),
);
}
function createPdfFormat(
displayName: string,
autoShiftHeadings = true,
koma = true,
): Format {
return createFormat(
displayName,
"pdf",
{
execute: {
[kFigWidth]: 5.5,
[kFigHeight]: 3.5,
[kFigFormat]: "pdf",
[kFigDpi]: 300,
},
pandoc: {
[kPdfEngine]: "lualatex",
standalone: true,
variables: {
graphics: true,
tables: true,
},
[kDefaultImageExtension]: "pdf",
},
metadata: {
["block-headings"]: true,
},
formatExtras: async (
_input: string,
markdown: string,
flags: PandocFlags,
format: Format,
_libDir: string,
services: RenderServices,
) => {
const extras: FormatExtras = {};
const engine = pdfEngine(format.pandoc, format.render, flags);
if (!isLatexPdfEngine(engine)) {
return extras;
}
extras.postprocessors = [
pdfLatexPostProcessor(flags, format, services.temp),
];
const documentclass = format.metadata[kDocumentClass] as
| string
| undefined;
const usingCustomTemplates = format.pandoc.template !== undefined ||
format.metadata[kTemplatePartials] !== undefined;
if (
usingCustomTemplates ||
(documentclass &&
![
"srcbook",
"scrreprt",
"scrreport",
"scrartcl",
"scrarticle",
].includes(
documentclass,
))
) {
koma = false;
}
if (koma) {
const captionOptions = [];
const tblCaploc = tblCapLocation(format);
captionOptions.push(
tblCaploc === kCapTop ? "tableheading" : "tablesignature",
);
if (figCapLocation(format) === kCapTop) {
captionOptions.push("figureheading");
}
const defaultClassOptions = ["DIV=11"];
if (format.metadata[kLang] !== "de") {
defaultClassOptions.push("numbers=noendperiod");
}
const userClassOptions = format.metadata[kClassOption] as
| string[]
| undefined;
const classOptions = defaultClassOptions.filter((option) => {
if (Array.isArray(userClassOptions)) {
const name = option.split("=")[0];
return !userClassOptions.some((userOption) =>
String(userOption).startsWith(name + "=")
);
} else {
return true;
}
});
const headerIncludes = [];
headerIncludes.push(
"\\KOMAoption{captions}{" + captionOptions.join(",") + "}",
);
extras.metadata = {
[kDocumentClass]: "scrartcl",
[kClassOption]: classOptions,
[kPaperSize]: "letter",
[kHeaderIncludes]: headerIncludes,
};
}
const partialNamesQuarto: string[] = [
"babel-lang",
"before-bib",
"biblio",
"biblio-config",
"citations",
"doc-class",
"graphics",
"after-body",
"before-body",
"pandoc",
"tables",
"tightlist",
"before-title",
"title",
"toc",
];
const partialNamesPandoc: string[] = [
"after-header-includes",
"common",
"document-metadata",
"font-settings",
"fonts",
"hypersetup",
"passoptions",
];
const createTemplateContext = function (
to: string,
partialNamesQuarto: string[],
partialNamesPandoc: string[],
) {
return {
template: formatResourcePath(to, "pandoc/template.tex"),
partials: [
...partialNamesQuarto.map((name) => {
return formatResourcePath(to, `pandoc/${name}.tex`);
}),
...partialNamesPandoc.map((name) => {
return formatResourcePath(to, `pandoc/${name}.latex`);
}),
],
};
};
const beamerPartialNamesPandoc = partialNamesPandoc.filter(
(name) => name !== "document-metadata",
);
extras.templateContext = createTemplateContext(
displayName === "Beamer" ? "beamer" : "pdf",
partialNamesQuarto,
displayName === "Beamer"
? beamerPartialNamesPandoc
: partialNamesPandoc,
);
const hasLevelOneHeadings = await hasL1Headings(markdown);
if (
!hasLevelOneHeadings &&
autoShiftHeadings &&
(flags?.[kNumberSections] === true ||
format.pandoc[kNumberSections] === true) &&
flags?.[kTopLevelDivision] === undefined &&
format.pandoc?.[kTopLevelDivision] === undefined &&
flags?.[kShiftHeadingLevelBy] === undefined &&
format.pandoc?.[kShiftHeadingLevelBy] === undefined
) {
extras.pandoc = {
[kShiftHeadingLevelBy]: -1,
};
}
extras.pandoc = extras.pandoc || {};
if (
documentclass === "scrbook" &&
format.pandoc[kNumberSections] !== false &&
flags[kNumberSections] !== false
) {
extras.pandoc[kNumberSections] = true;
}
const pdfStandard = asArray(
format.render?.[kPdfStandard] ?? format.metadata?.[kPdfStandard],
);
if (pdfStandard.length > 0) {
const { version, standards, needsTagging } =
normalizePdfStandardForLatex(pdfStandard);
if (standards.length > 0 || version) {
extras.pandoc.variables = extras.pandoc.variables || {};
const pdfstandardMap: Record<string, unknown> = {};
if (standards.length > 0) {
pdfstandardMap.standards = standards;
}
if (version) {
pdfstandardMap.version = version;
}
if (needsTagging) {
pdfstandardMap.tagging = true;
}
extras.pandoc.variables["pdfstandard"] = pdfstandardMap;
}
if (standards.length > 0) {
extras.metadata = extras.metadata || {};
extras.metadata[kPdfStandardApplied] = standards;
}
}
return extras;
},
},
);
}
const pdfBookExtension: BookExtension = {
selfContainedOutput: true,
onSingleFilePostRender: (
project: ProjectContext,
renderedFile: RenderedFile,
) => {
if (renderedFile.format.render[kKeepTex]) {
const finalOutputFile = renderedFile.file!;
const texOutputFile =
texSafeFilename(basename(finalOutputFile, extname(finalOutputFile))) +
".tex";
Deno.renameSync(
join(project.dir, "index.tex"),
join(project.dir, texOutputFile),
);
}
},
};
type LineProcessor = (line: string) => string | undefined;
function pdfLatexPostProcessor(
flags: PandocFlags,
format: Format,
temp: TempContext,
) {
return async (output: string) => {
const lineProcessors: LineProcessor[] = [
sidecaptionLineProcessor(),
calloutFloatHoldLineProcessor(),
tableColumnMarginLineProcessor(),
guidsProcessor(),
];
if (format.pandoc[kCiteMethod] === "biblatex") {
lineProcessors.push(bibLatexBibligraphyRefsDivProcessor());
} else if (format.pandoc[kCiteMethod] === "natbib") {
lineProcessors.push(
natbibBibligraphyRefsDivProcessor(
format.metadata[kBibliography] as string[] | undefined,
),
);
}
const marginCites = format.metadata[kCitationLocation] === "margin";
const renderedCites = {};
if (marginCites) {
if (format.pandoc[kCiteMethod] === "biblatex") {
lineProcessors.push(suppressBibLatexBibliographyLineProcessor());
lineProcessors.push(bibLatexCiteLineProcessor());
} else if (format.pandoc[kCiteMethod] === "natbib") {
lineProcessors.push(suppressNatbibBibliographyLineProcessor());
lineProcessors.push(natbibCiteLineProcessor());
} else {
lineProcessors.push(
indexAndSuppressPandocBibliography(renderedCites),
cleanReferencesChapter(),
);
}
}
if (tblCapLocation(format) === kCapBottom) {
lineProcessors.push(longtableBottomCaptionProcessor());
}
if (marginRefs(flags, format)) {
lineProcessors.push(sideNoteLineProcessor());
}
lineProcessors.push(captionFootnoteLineProcessor());
if (
format.metadata[kCodeAnnotations] as boolean !== false &&
format.metadata[kCodeAnnotations] as string !== "none"
) {
lineProcessors.push(codeAnnotationPostProcessor());
lineProcessors.push(codeListAnnotationPostProcessor());
}
lineProcessors.push(tableSidenoteProcessor());
await processLines(output, lineProcessors, temp);
const pass2Processors: LineProcessor[] = [
longTableSidenoteProcessor(),
];
if (Object.keys(renderedCites).length > 0) {
pass2Processors.push(placePandocBibliographyEntries(renderedCites));
}
await processLines(output, pass2Processors, temp);
};
}
function tblCapLocation(format: Format) {
return format.metadata[kTblCapLoc] || format.metadata[kCapLoc] || kCapTop;
}
function figCapLocation(format: Format) {
return format.metadata[kFigCapLoc] || format.metadata[kCapLoc] || kCapBottom;
}
function marginRefs(flags: PandocFlags, format: Format) {
return format.pandoc[kReferenceLocation] === "margin" ||
flags[kReferenceLocation] === "margin";
}
async function processLines(
inputFile: string,
lineProcessors: LineProcessor[],
temp: TempContext,
) {
const outputFile = temp.createFile({ suffix: ".tex" });
const file = await Deno.open(inputFile);
const mode = safeModeFromFile(inputFile);
try {
for await (const line of readLines(file)) {
let processedLine: string | undefined = line;
for (const processor of lineProcessors) {
if (processedLine !== undefined) {
processedLine = processor(processedLine);
}
}
if (processedLine !== undefined) {
Deno.writeTextFileSync(outputFile, processedLine + "\n", {
append: true,
mode,
});
}
}
} finally {
file.close();
copyTo(outputFile, inputFile);
}
}
const kBeginScanRegex = /^%quartopost-sidecaption-206BE349/;
const kEndScanRegex = /^%\/quartopost-sidecaption-206BE349/;
const sidecaptionLineProcessor = () => {
let state: "scanning" | "replacing" = "scanning";
return (line: string): string | undefined => {
switch (state) {
case "scanning":
if (line.match(kBeginScanRegex)) {
state = "replacing";
return kbeginLongTablesideCap;
} else {
return line;
}
case "replacing":
if (line.match(kEndScanRegex)) {
state = "scanning";
return kEndLongTableSideCap;
} else {
return line;
}
}
};
};
const readBalancedCommand = (latex: string) => {
let braceCount = 0;
let entered = false;
const chars: string[] = [];
for (let i = 0; i < latex.length; i++) {
const char = latex.charAt(i);
if (char === "{") {
braceCount++;
entered = true;
} else if (char === "}") {
braceCount--;
}
chars.push(char);
if (entered && braceCount === 0) {
break;
}
}
return chars.join("");
};
const processElementCaptionFootnotes = (latexFigure: string) => {
const footnoteMark = "\\footnote{";
const captionMark = "\\caption{";
const contents: string[] = [];
const captionIndex = latexFigure.indexOf(captionMark);
if (captionIndex > -1) {
contents.push(latexFigure.substring(0, captionIndex));
const captionStartStr = latexFigure.slice(captionIndex);
const captionLatex = readBalancedCommand(captionStartStr);
const figureSuffix = captionStartStr.slice(captionLatex.length);
let captionContents = captionLatex.slice(
captionMark.length,
captionLatex.length - 1,
);
let footNoteIndex = captionContents.indexOf(footnoteMark);
if (footNoteIndex > -1) {
const captionText: string[] = [];
const captionWithNote: string[] = [];
const footNotes: string[] = [];
while (footNoteIndex > -1) {
const prefix = captionContents.substring(0, footNoteIndex);
captionContents = captionContents.slice(footNoteIndex);
captionText.push(prefix);
captionWithNote.push(prefix);
const footnoteLatex = readBalancedCommand(captionContents);
captionContents = captionContents.slice(footnoteLatex.length);
footNoteIndex = captionContents.indexOf(footnoteMark);
captionWithNote.push("\\footnotemark{}");
footNotes.push(
footnoteLatex.slice(footnoteMark.length, footnoteLatex.length - 1),
);
}
captionText.push(captionContents);
captionWithNote.push(captionContents);
contents.push(
`\\caption[${captionText.join("")}]{${captionWithNote.join("")}}`,
);
contents.push(figureSuffix);
contents.push("\n");
if (footNotes.length > 1) {
contents.push(`\\addtocounter{footnote}{-${footNotes.length - 1}}`);
}
for (let i = 0; i < footNotes.length; i++) {
contents.push(`\\footnotetext{${footNotes[i]}}`);
if (footNotes.length > 1 && i < footNotes.length - 1) {
contents.push(`\\addtocounter{footnote}{1}`);
}
}
return contents.join("");
} else {
return latexFigure;
}
} else {
return latexFigure;
}
};
const kMatchLongTableSize = /^(.*)p{\(\\columnwidth - (\d+\\tabcolsep\).*$)/;
const kStartLongTable = /^\\begin{longtable}/;
const kEndLongTable = /^\\end{longtable}/;
const guidsProcessor = () => {
let state: "looking-for-definition-start" | "looking-for-definition-end" =
"looking-for-definition-start";
const guidDefinitions: [string, string][] = [];
let guidBeingProcessed: string | undefined;
let guidContents: string[] = [];
return (line: string): string | undefined => {
switch (state) {
case "looking-for-definition-start": {
if (line.startsWith("%quarto-define-uuid: ")) {
state = "looking-for-definition-end";
line = line.replace(/^%quarto-define-uuid:\s*/, "");
guidBeingProcessed = line.trim();
return undefined;
}
for (const [key, value] of guidDefinitions) {
line = line.replaceAll(key, value);
}
return line;
}
case "looking-for-definition-end": {
if (line === "%quarto-end-define-uuid") {
state = "looking-for-definition-start";
if (guidBeingProcessed === undefined) {
throw new Error("guidBeingProcessed is undefined");
}
guidDefinitions.push([
guidBeingProcessed,
guidContents.join("").trim(),
]);
guidContents = [];
guidBeingProcessed = undefined;
return undefined;
} else {
guidContents.push(line);
return undefined;
}
}
}
};
};
const tableColumnMarginLineProcessor = () => {
let state: "looking-for-boundaries" | "looking-for-tables" | "processing" =
"looking-for-boundaries";
return (line: string): string | undefined => {
switch (state) {
case "looking-for-boundaries": {
if (line === "% quarto-tables-in-margin-AB1927C9:begin") {
state = "looking-for-tables";
return undefined;
}
return line;
}
case "looking-for-tables": {
if (line.match(kStartLongTable)) {
state = "processing";
return line;
} else if (line === "% quarto-tables-in-margin-AB1927C9:end") {
state = "looking-for-boundaries";
return undefined;
}
return line;
}
case "processing": {
if (line.match(kEndLongTable)) {
state = "looking-for-tables";
return line;
} else {
const match = line.match(kMatchLongTableSize);
if (match) {
return `${
match[1]
}p{(\\marginparwidth + \\marginparsep + \\columnwidth - ${
match[2]
}`;
} else {
return line;
}
}
}
default: {
return line;
}
}
};
};
const captionFootnoteLineProcessor = () => {
let state: "scanning" | "capturing" = "scanning";
let capturedLines: string[] = [];
return (line: string): string | undefined => {
switch (state) {
case "scanning":
if (line.match(/^\\begin{figure}.*$/)) {
state = "capturing";
capturedLines = [line];
return undefined;
} else {
return line;
}
case "capturing":
capturedLines.push(line);
if (line.match(/^\\end{figure}%*$/)) {
state = "scanning";
const lines = capturedLines.join("\n");
capturedLines = [];
return processElementCaptionFootnotes(lines);
} else {
return undefined;
}
}
};
};
const processSideNotes = (endMarker: string) => {
return (latexLongTable: string) => {
const sideNoteMarker = "\\sidenote{\\footnotesize ";
let strProcessing = latexLongTable;
const strOutput: string[] = [];
const sidenotes: string[] = [];
let sidenotePos = strProcessing.indexOf(sideNoteMarker);
while (sidenotePos > -1) {
strOutput.push(strProcessing.substring(0, sidenotePos));
const remainingStr = strProcessing.substring(
sidenotePos + sideNoteMarker.length,
);
let escaped = false;
let sideNoteEnd = -1;
for (let i = 0; i < remainingStr.length; i++) {
const ch = remainingStr[i];
if (ch === "\\") {
escaped = true;
} else {
if (!escaped && ch === "}") {
sideNoteEnd = i;
break;
} else {
escaped = false;
}
}
}
if (sideNoteEnd > -1) {
strOutput.push("\\sidenotemark{}");
const contents = remainingStr.substring(0, sideNoteEnd);
sidenotes.push(contents);
strProcessing = remainingStr.substring(sideNoteEnd + 1);
sidenotePos = strProcessing.indexOf(sideNoteMarker);
} else {
strOutput.push(remainingStr);
}
}
const endTable = endMarker;
const endPos = strProcessing.indexOf(endTable);
const prefix = strProcessing.substring(0, endPos + endTable.length);
const suffix = strProcessing.substring(
endPos + endTable.length,
strProcessing.length,
);
strOutput.push(prefix);
for (const note of sidenotes) {
strOutput.push(`\\sidenotetext{${note}}\n`);
}
if (suffix) {
strOutput.push(suffix);
}
return strOutput.join("");
};
};
const processLongTableSidenotes = processSideNotes("\\end{longtable}");
const processTableSidenotes = processSideNotes("\\end{table}");
const sideNoteProcessor = (
beginRegex: RegExp,
endRegex: RegExp,
callback: (str: string) => string,
) => {
return () => {
let state: "scanning" | "capturing" = "scanning";
let capturedLines: string[] = [];
return (line: string): string | undefined => {
switch (state) {
case "scanning":
if (line.match(beginRegex)) {
state = "capturing";
capturedLines = [line];
return undefined;
} else {
return line;
}
case "capturing":
capturedLines.push(line);
if (line.match(endRegex)) {
state = "scanning";
const lines = capturedLines.join("\n");
capturedLines = [];
return callback(lines);
} else {
return undefined;
}
}
};
};
};
const longTableSidenoteProcessor = sideNoteProcessor(
/^\\begin{longtable}.*$/,
/^\\end{longtable}%*$/,
processLongTableSidenotes,
);
const tableSidenoteProcessor = sideNoteProcessor(
/^\\begin{table}.*$/,
/^\\end{table}%*$/,
processTableSidenotes,
);
const calloutFloatHoldLineProcessor = () => {
let state: "scanning" | "replacing" = "scanning";
return (line: string): string | undefined => {
switch (state) {
case "scanning":
if (line.match(/^\\begin{tcolorbox}/)) {
state = "replacing";
return line;
} else {
return line;
}
case "replacing":
if (line.match(/^\\end{tcolorbox}/)) {
state = "scanning";
return line;
} else if (line.match(/^\\begin{figure}$/)) {
return "\\begin{figure}[H]";
} else if (line.match(/^\\begin{codelisting}$/)) {
return "\\begin{codelisting}[H]";
} else {
return line;
}
}
};
};
const kQuartoBibPlaceholderRegex = "%bib-loc-124C8010";
const bibLatexBibligraphyRefsDivProcessor = () => {
let hasRefsDiv = false;
return (line: string): string | undefined => {
if (line === kQuartoBibPlaceholderRegex) {
if (!hasRefsDiv) {
hasRefsDiv = true;
return "\\printbibliography[heading=none]";
} else {
return undefined;
}
} else if (hasRefsDiv && line.match(/^\\printbibliography$/)) {
return undefined;
} else {
return line;
}
};
};
const natbibBibligraphyRefsDivProcessor = (bibs?: string[]) => {
let hasRefsDiv = false;
return (line: string): string | undefined => {
if (line === kQuartoBibPlaceholderRegex) {
if (bibs && !hasRefsDiv) {
hasRefsDiv = true;
return `\\renewcommand{\\bibsection}{}\n\\bibliography{${
bibs.join(",")
}}`;
} else {
return undefined;
}
} else if (hasRefsDiv && line.match(/^\s*\\bibliography{.*}$/)) {
return undefined;
} else {
return line;
}
};
};
const suppressBibLatexBibliographyLineProcessor = () => {
return (line: string): string | undefined => {
if (line.match(/^\\printbibliography$/)) {
return "";
}
return line;
};
};
const suppressNatbibBibliographyLineProcessor = () => {
return (line: string): string | undefined => {
return line.replace(/^\s*\\bibliography{(.*)}$/, (_match, bib) => {
return `\\newsavebox\\mytempbib
\\savebox\\mytempbib{\\parbox{\\textwidth}{\\bibliography{${bib}}}}`;
});
};
};
const kQuartoCiteRegex = /{\?quarto-cite:(.*?)}/g;
const bibLatexCiteLineProcessor = () => {
return (line: string): string | undefined => {
return line.replaceAll(kQuartoCiteRegex, (_match, citeKey) => {
return `\\fullcite{${citeKey}}`;
});
};
};
const natbibCiteLineProcessor = () => {
return (line: string): string | undefined => {
return line.replaceAll(kQuartoCiteRegex, (_match, citeKey) => {
return `\\bibentry{${citeKey}}`;
});
};
};
const sideNoteLineProcessor = () => {
return (line: string): string | undefined => {
return line.replaceAll(/\\footnote{/g, "\\sidenote{\\footnotesize ");
};
};
const longtableBottomCaptionProcessor = () => {
let scanning = false;
let capturing = false;
let caption: string | undefined;
return (line: string): string | undefined => {
const isEndOfDocument = !!line.match(/^\\end{document}/);
if (isEndOfDocument && caption) {
return `${caption}\n${line}`;
} else if (scanning) {
if (capturing) {
caption = `${caption}\n${line}`;
capturing = !line.match(/\\tabularnewline$/);
return undefined;
} else {
if (
line.match(/^\\caption.*?\\tabularnewline$/) ||
line.match(/^\\caption{.*}\\\\$/)
) {
caption = line;
return undefined;
} else if (line.match(/^\\caption.*?/)) {
caption = line;
capturing = true;
return undefined;
} else if (line.match(/^\\endlastfoot/) && caption) {
line = `\\tabularnewline\n${caption}\n${line}`;
caption = undefined;
return line;
} else if (line.match(/^\\end{longtable}$/)) {
scanning = false;
if (caption) {
line = caption + "\n" + line;
caption = undefined;
return line;
}
}
}
} else {
scanning = !!line.match(/^\\begin{longtable}/);
}
return line;
};
};
const kChapterRefNameRegex = /^\\chapter\*?{(.*?)}\\label{references.*?}$/;
const cleanReferencesChapter = () => {
let refChapterName: string | undefined;
let refChapterContentsRegex: RegExp | undefined;
let refChapterMarkRegex: RegExp | undefined;
return (line: string): string | undefined => {
const chapterRefMatch = line.match(kChapterRefNameRegex);
if (chapterRefMatch) {
refChapterName = chapterRefMatch[1];
refChapterContentsRegex = new RegExp(
`\\\\addcontentsline{toc}{chapter}{${refChapterName}}`,
);
refChapterMarkRegex = new RegExp(
`\\\\markboth{${refChapterName}}{${refChapterName}}`,
);
return undefined;
} else if (refChapterContentsRegex && line.match(refChapterContentsRegex)) {
return undefined;
} else if (refChapterMarkRegex && line.match(refChapterMarkRegex)) {
return undefined;
}
return line;
};
};
const indexAndSuppressPandocBibliography = (
renderedCites: Record<string, string[]>,
) => {
let readingBibliography = false;
let currentCiteKey: string | undefined = undefined;
return (line: string): string | undefined => {
if (
!readingBibliography &&
line.match(/^(\\protect)?\\phantomsection\\label{refs}$/)
) {
readingBibliography = true;
return undefined;
} else if (readingBibliography && line.match(/^\\end{CSLReferences}$/)) {
readingBibliography = false;
return undefined;
} else if (readingBibliography) {
const matches = line.match(/\\bibitem\[\\citeproctext\]{ref\-(.*?)}/);
if (matches && matches[1]) {
currentCiteKey = matches[1];
renderedCites[currentCiteKey] = [line];
} else if (line.length === 0) {
currentCiteKey = undefined;
} else if (currentCiteKey) {
renderedCites[currentCiteKey].push(line);
}
}
if (readingBibliography) {
return undefined;
} else {
return line;
}
};
};
const kInSideCaptionRegex = /^\\sidecaption{/;
const kBeginFigureRegex = /^\\begin{figure}\[.*?\]$/;
const kEndFigureRegex = /^\\end{figure}\%?$/;
const placePandocBibliographyEntries = (
renderedCites: Record<string, string[]>,
) => {
let biblioEntryState: "scanning" | "in-figure" | "in-sidecaption" =
"scanning";
let pendingCiteKeys: string[] = [];
return (line: string): string | undefined => {
switch (biblioEntryState) {
case "scanning": {
if (line.match(kBeginFigureRegex)) {
biblioEntryState = "in-figure";
}
break;
}
case "in-figure": {
if (line.match(kInSideCaptionRegex)) {
biblioEntryState = "in-sidecaption";
} else {
if (line.match(kEndFigureRegex)) {
biblioEntryState = "scanning";
}
}
break;
}
case "in-sidecaption": {
if (line.match(kEndFigureRegex)) {
biblioEntryState = "scanning";
}
break;
}
default:
break;
}
if (biblioEntryState === "scanning" && pendingCiteKeys.length > 0) {
const result = [
line,
"\n\\begin{CSLReferences}{2}{0}",
...pendingCiteKeys,
"\\end{CSLReferences}\n",
].join("\n");
pendingCiteKeys = [];
return result;
}
return line.replaceAll(kQuartoCiteRegex, (_match, citeKey) => {
const citeLines = renderedCites[citeKey];
if (citeLines) {
if (biblioEntryState === "in-sidecaption" && citeLines.length > 0) {
pendingCiteKeys.push(citeLines[0]);
return ["", ...citeLines.slice(1)].join("\n");
} else {
return [
"\n\\begin{CSLReferences}{2}{0}",
...citeLines,
"\\end{CSLReferences}\n",
].join("\n");
}
} else {
return citeKey;
}
});
};
};
const kCodeAnnotationRegex =
/(.*)\\CommentTok\{(.*?)[^\s]+? +\\textless\{\}(\d+)\\textgreater\{\}.*\}$/gm;
const kCodePlainAnnotationRegex = /(.*)% \((\d+)\)$/g;
const codeAnnotationPostProcessor = () => {
let lastAnnotation: string | undefined;
return (line: string): string | undefined => {
if (line === "\\begin{Shaded}") {
lastAnnotation = undefined;
}
line = line.replaceAll(
kCodeAnnotationRegex,
(_match, prefix: string, comment: string, annotationNumber: string) => {
if (annotationNumber !== lastAnnotation) {
lastAnnotation = annotationNumber;
if (comment.length > 0) {
prefix = `${prefix}\\CommentTok\{${comment}\}`;
}
return `${prefix}\\hspace*{\\fill}\\NormalTok{\\circled{${annotationNumber}}}`;
} else {
return `${prefix}`;
}
},
);
line = line.replaceAll(
kCodePlainAnnotationRegex,
(_match, prefix: string, annotationNumber: string) => {
if (annotationNumber !== lastAnnotation) {
lastAnnotation = annotationNumber;
const replaceValue = `(${annotationNumber})`;
const paddingNumber = Math.max(
0,
75 - prefix.length - replaceValue.length,
);
const padding = " ".repeat(paddingNumber);
return `${prefix}${padding}${replaceValue}`;
} else {
return `${prefix}`;
}
},
);
return line;
};
};
const kListAnnotationRegex = /(.*)5CB6E08D-list-annote-(\d+)(.*)/g;
const codeListAnnotationPostProcessor = () => {
return (line: string): string | undefined => {
return line.replaceAll(
kListAnnotationRegex,
(_match, prefix: string, annotationNumber: string, suffix: string) => {
return `${prefix}\\circled{${annotationNumber}}${suffix}`;
},
);
};
};
const kbeginLongTablesideCap = `{
\\makeatletter
\\def\\LT@makecaption#1#2#3{%
\\noalign{\\smash{\\hbox{\\kern\\textwidth\\rlap{\\kern\\marginparsep
\\parbox[t]{\\marginparwidth}{%
\\footnotesize{%
\\vspace{(1.1\\baselineskip)}
#1{#2: }\\ignorespaces #3}}}}}}%
}
\\makeatother`;
const kEndLongTableSideCap = "}";
const kLatexSupportedStandards = new Set([
"a-1b",
"a-2a",
"a-2b",
"a-2u",
"a-3a",
"a-3b",
"a-3u",
"a-4",
"a-4e",
"a-4f",
"x-4",
"x-4p",
"x-5g",
"x-5n",
"x-5pg",
"x-6",
"x-6n",
"x-6p",
"ua-2",
]);
const kTaggingRequiredStandards = new Set([
"a-2a",
"a-3a",
"ua-1",
"ua-2",
]);
const kVersionPattern = /^(1\.[4-7]|2\.0)$/;
const kStandardRequiredVersion: Record<string, string> = {
"a-1b": "1.4",
"a-2a": "1.7",
"a-2b": "1.7",
"a-2u": "1.7",
"a-3a": "1.7",
"a-3b": "1.7",
"a-3u": "1.7",
};
function normalizePdfStandardForLatex(
standards: unknown[],
): { version?: string; standards: string[]; needsTagging: boolean } {
let version: string | undefined;
const result: string[] = [];
let needsTagging = false;
for (const s of standards) {
let str: string;
if (typeof s === "number") {
str = Number.isInteger(s) ? `${s}.0` : String(s);
} else if (typeof s === "string") {
str = s;
} else {
continue;
}
const normalized = str.toLowerCase().replace(/^pdf[/-]?/, "");
if (kVersionPattern.test(normalized)) {
if (!version) {
version = normalized;
}
} else if (kLatexSupportedStandards.has(normalized)) {
result.push(normalized);
if (kTaggingRequiredStandards.has(normalized)) {
needsTagging = true;
}
if (!version && kStandardRequiredVersion[normalized]) {
version = kStandardRequiredVersion[normalized];
}
} else {
warning(
`PDF standard '${s}' is not supported by LaTeX and will be ignored`,
);
}
}
return { version, standards: result, needsTagging };
}