Path: blob/main/src/command/render/output-tex.ts
12925 views
/*1* output-tex.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { dirname, join, normalize, relative } from "../../deno_ral/path.ts";7import { ensureDirSync, safeRemoveSync } from "../../deno_ral/fs.ts";89import { writeFileToStdout } from "../../core/console.ts";10import { dirAndStem, expandPath } from "../../core/path.ts";11import { texSafeFilename } from "../../core/tex.ts";1213import {14kKeepTex,15kOutputExt,16kOutputFile,17kPdfStandard,18kPdfStandardApplied,19kTargetFormat,20pdfStandardEnv,21} from "../../config/constants.ts";22import { Format } from "../../config/types.ts";23import { asArray } from "../../core/array.ts";24import { validatePdfStandards } from "../../core/verapdf.ts";2526import { PandocOptions, RenderFlags, RenderOptions } from "./types.ts";27import { kStdOut, replacePandocOutputArg } from "./flags.ts";28import { OutputRecipe } from "./types.ts";29import { pdfEngine } from "../../config/pdf.ts";30import { execProcess } from "../../core/process.ts";31import { parseFormatString } from "../../core/pandoc/pandoc-formats.ts";32import { normalizeOutputPath } from "./output-shared.ts";3334export interface PdfGenerator {35generate: (36input: string,37format: Format,38pandocOptions: PandocOptions,39) => Promise<string>;40computePath: (texStem: string, inputDir: string, format: Format) => string;41}4243export function texToPdfOutputRecipe(44input: string,45finalOutput: string,46options: RenderOptions,47format: Format,48pdfIntermediateTo: string,49pdfGenerator: PdfGenerator,50pdfOutputDir?: string | null,51): OutputRecipe {52// break apart input file53const [inputDir, inputStem] = dirAndStem(input);5455// there are many characters that give tex trouble in filenames, create56// a target stem that replaces them with the '-' character5758// include variants in the tex stem if they are present to avoid59// overwriting files60let fixupInputName = "";61if (format.identifier[kTargetFormat]) {62const formatDesc = parseFormatString(format.identifier[kTargetFormat]);63fixupInputName = `${formatDesc.variants.join("")}${64formatDesc.modifiers.join("")65}`;66}6768const texStem = texSafeFilename(`${inputStem}${fixupInputName}`);6970// calculate output and args for pandoc (this is an intermediate file71// which we will then compile to a pdf and rename to .tex)72const output = texStem + ".tex";73let args = options.pandocArgs || [];74const pandoc = { ...format.pandoc };75if (options.flags?.output) {76args = replacePandocOutputArg(args, output);77} else {78pandoc[kOutputFile] = output;79}8081// when pandoc is done, we need to run the pdf generator and then copy the82// ouptut to the user's requested destination83const complete = async (pandocOptions: PandocOptions) => {84const input = join(inputDir, output);85const pdfOutput = await pdfGenerator.generate(input, format, pandocOptions);8687// Validate PDF against applied standards using verapdf (if available)88// Use kPdfStandardApplied from pandocOptions.format.metadata (filtered by LaTeX support)89// if available, otherwise fall back to original kPdfStandard list90const pdfStandards = asArray(91pandocOptions.format.metadata?.[kPdfStandardApplied] ??92format.render?.[kPdfStandard] ??93format.metadata?.[kPdfStandard] ??94pdfStandardEnv(),95) as string[];96if (pdfStandards.length > 0) {97await validatePdfStandards(pdfOutput, pdfStandards, {98quiet: pandocOptions.flags?.quiet,99});100}101102// keep tex if requested103const compileTex = join(inputDir, output);104if (!format.render[kKeepTex]) {105safeRemoveSync(compileTex);106}107108// copy (or write for stdout) compiled pdf to final output location109if (finalOutput) {110if (finalOutput === kStdOut) {111writeFileToStdout(pdfOutput);112safeRemoveSync(pdfOutput);113} else {114const outputPdf = expandPath(finalOutput);115116if (normalize(pdfOutput) !== normalize(outputPdf)) {117// ensure the target directory exists118ensureDirSync(dirname(outputPdf));119120Deno.renameSync(pdfOutput, outputPdf);121}122}123124// Clean the output directory if it is empty125if (pdfOutputDir) {126console.log({ pdfOutputDir });127try {128// Remove the outputDir if it is empty129safeRemoveSync(pdfOutputDir, { recursive: false });130} catch {131// This is ok, just means the directory wasn't empty132}133}134135// final output needs to either absolute or input dir relative136// (however it may be working dir relative when it is passed in)137return normalizeOutputPath(input, finalOutput);138} else {139return normalizeOutputPath(input, pdfOutput);140}141};142143const pdfOutput = finalOutput144? finalOutput === kStdOut145? undefined146: normalizeOutputPath(input, finalOutput)147: normalizeOutputPath(148input,149pdfGenerator.computePath(texStem, dirname(input), format),150);151152// tweak writer if it's pdf153const to = format.pandoc.to === "pdf" ? pdfIntermediateTo : format.pandoc.to;154155// return recipe156return {157output,158keepYaml: false,159args,160format: {161...format,162pandoc: {163...pandoc,164to,165},166},167complete,168finalOutput: pdfOutput ? relative(inputDir, pdfOutput) : undefined,169};170}171172export function useContextPdfOutputRecipe(173format: Format,174flags?: RenderFlags,175) {176const kContextPdfEngine = "context";177if (format.pandoc.to === "pdf" && format.render[kOutputExt] === "pdf") {178const engine = pdfEngine(format.pandoc, format.render, flags);179return engine.pdfEngine === kContextPdfEngine;180} else {181return false;182}183}184185// based on: https://github.com/rstudio/rmarkdown/blob/main/R/context_document.R186187export function contextPdfOutputRecipe(188input: string,189finalOutput: string,190options: RenderOptions,191format: Format,192): OutputRecipe {193const computePath = (stem: string, dir: string, _format: Format) => {194return join(dir, stem + ".pdf");195};196197const generate = async (198input: string,199format: Format,200pandocOptions: PandocOptions,201): Promise<string> => {202// derive engine (parse opts, etc.)203const engine = pdfEngine(format.pandoc, format.render, pandocOptions.flags);204205// build context command206const cmd = "context";207const args = [input];208if (engine.pdfEngineOpts) {209args.push(...engine.pdfEngineOpts);210}211args.push(212// ConTeXt produces some auxiliary files:213// direct PDF generation by Pandoc never produces these auxiliary214// files because Pandoc runs ConTeXt in a temporary directory.215// Replicate Pandoc's behavior using "--purgeall" option216"--purgeall",217// Pandoc runs ConteXt with "--batchmode" option. Do the same.218"--batchmode",219);220221// run context222const result = await execProcess({223cmd,224args,225});226if (result.success) {227const [dir, stem] = dirAndStem(input);228return computePath(stem, dir, format);229} else {230throw new Error();231}232};233234return texToPdfOutputRecipe(235input,236finalOutput,237options,238format,239"context",240{241generate,242computePath,243},244);245}246247248