import {
basename,
dirname,
extname,
isAbsolute,
join,
relative,
SEP_PATTERN,
} from "../../deno_ral/path.ts";
import { writeFileToStdout } from "../../core/console.ts";
import { dirAndStem, expandPath, safeRemoveSync } from "../../core/path.ts";
import {
parse as parseYaml,
partitionYamlFrontMatter,
stringify as stringifyYaml,
} from "../../core/yaml.ts";
import {
kOutputExt,
kOutputFile,
kPreserveYaml,
kVariant,
} from "../../config/constants.ts";
import {
quartoLatexmkOutputRecipe,
useQuartoLatexmk,
} from "./latexmk/latexmk.ts";
import { kStdOut, replacePandocOutputArg } from "./flags.ts";
import { OutputRecipe, RenderContext } from "./types.ts";
import { resolveKeepSource } from "./codetools.ts";
import {
contextPdfOutputRecipe,
useContextPdfOutputRecipe,
} from "./output-tex.ts";
import { formatOutputFile } from "../../core/render.ts";
import { kYamlMetadataBlock } from "../../core/pandoc/pandoc-formats.ts";
import {
typstPdfOutputRecipe,
useTypstPdfOutputRecipe,
} from "./output-typst.ts";
export function outputRecipe(
context: RenderContext,
): OutputRecipe {
const input = context.target.input;
const options = context.options;
const format = context.format;
let output = options.flags?.output;
if (!output) {
const outputFile = formatOutputFile(format);
if (outputFile) {
if (outputFile.match(SEP_PATTERN)) {
throw new Error(
`\nIn file ${context.target.source}\n Invalid value for \`output-file\`: paths are not allowed`,
);
}
output = join(dirname(input), outputFile);
} else {
output = "";
}
}
if (useQuartoLatexmk(format, options.flags)) {
return quartoLatexmkOutputRecipe(input, output, options, format);
} else if (useContextPdfOutputRecipe(format, options.flags)) {
return contextPdfOutputRecipe(input, output, options, format);
} else if (useTypstPdfOutputRecipe(format)) {
return typstPdfOutputRecipe(
input,
output,
options,
format,
context.project,
);
} else {
const completeActions: VoidFunction[] = [];
const recipe: OutputRecipe = {
output,
keepYaml: false,
args: options.pandocArgs || [],
format: { ...format },
complete: (): Promise<string | void> => {
completeActions.forEach((action) => action());
return Promise.resolve();
},
};
resolveKeepSource(recipe.format, context.engine, context.target);
const updateOutput = (output: string) => {
recipe.output = output;
if (options.flags?.output) {
recipe.args = replacePandocOutputArg(recipe.args, output);
} else {
recipe.format.pandoc[kOutputFile] = output;
}
};
const ext = format.render[kOutputExt] || "html";
const [inputDir, inputStem] = dirAndStem(input);
if (format.render[kVariant]) {
const to = format.pandoc.to;
const variant = format.render[kVariant];
recipe.format = {
...recipe.format,
pandoc: {
...recipe.format.pandoc,
to: `${to}${variant}`,
},
};
if (recipe.format.pandoc.to?.includes(`+${kYamlMetadataBlock}`)) {
recipe.keepYaml = true;
}
}
if (recipe.keepYaml || recipe.format.render[kPreserveYaml]) {
completeActions.push(() => {
const inputMd = partitionYamlFrontMatter(context.target.markdown.value);
if (inputMd) {
const outputFile = isAbsolute(recipe.output)
? recipe.output
: join(dirname(context.target.input), recipe.output);
const output = Deno.readTextFileSync(outputFile);
const outputMd = partitionYamlFrontMatter(
Deno.readTextFileSync(outputFile),
);
const yaml = parseYaml(
inputMd.yaml.replace(/^---+\n/m, "").replace(/\n---+\n*$/m, "\n"),
) as Record<string, unknown>;
delete yaml._quarto;
const yamlString = `---\n${stringifyYaml(yaml)}---\n`;
const markdown = outputMd?.markdown || output;
Deno.writeTextFileSync(
outputFile,
yamlString + "\n\n" + markdown,
);
}
});
}
const deriveAutoOutput = () => {
let output = inputStem + "." + ext;
if (extname(input) === ".md" && ext === "md") {
output = `${inputStem}-${format.identifier["base-format"]}.md`;
}
if (output === basename(context.target.source)) {
output = inputStem + `.${kOutExt}.` + ext;
}
updateOutput(output);
};
if (!recipe.output) {
deriveAutoOutput();
} else if (recipe.output === kStdOut) {
deriveAutoOutput();
recipe.isOutputTransient = true;
completeActions.push(() => {
writeFileToStdout(join(inputDir, recipe.output));
safeRemoveSync(join(inputDir, recipe.output));
});
} else if (!isAbsolute(recipe.output)) {
updateOutput(relative(inputDir, recipe.output));
} else {
updateOutput(expandPath(recipe.output));
}
return recipe;
}
}
const kOutExt = "out";