import { ensureDirSync, existsSync } from "../../deno_ral/fs.ts";
import { dirname, isAbsolute, join, relative } from "../../deno_ral/path.ts";
import { Document, parseHtml } from "../../core/deno-dom.ts";
import { mergeConfigs } from "../../core/config.ts";
import { resourcePath } from "../../core/resources.ts";
import { inputFilesDir } from "../../core/render.ts";
import {
normalizePath,
pathWithForwardSlashes,
safeExistsSync,
} from "../../core/path.ts";
import { FormatPandoc } from "../../config/types.ts";
import {
executionEngine,
executionEngineKeepMd,
} from "../../execute/engine.ts";
import {
HtmlPostProcessor,
HtmlPostProcessResult,
PandocInputTraits,
PandocOptions,
PandocRenderCompletion,
RenderedFormat,
} from "./types.ts";
import { runPandoc } from "./pandoc.ts";
import { renderCleanup } from "./cleanup.ts";
import { projectOffset } from "../../project/project-shared.ts";
import { ExecutedFile, RenderedFile, RenderResult } from "./types.ts";
import { PandocIncludes } from "../../execute/types.ts";
import { Metadata } from "../../config/types.ts";
import { isHtmlFileOutput } from "../../config/format.ts";
import { isSelfContainedOutput } from "./render-info.ts";
import {
pop as popTiming,
push as pushTiming,
withTiming,
withTimingAsync,
} from "../../core/timing.ts";
import { filesDirMediabagDir } from "./render-paths.ts";
import { replaceNotebookPlaceholders } from "../../core/jupyter/jupyter-embed.ts";
import {
kIncludeAfterBody,
kIncludeBeforeBody,
kIncludeInHeader,
kInlineIncludes,
kResourcePath,
} from "../../config/constants.ts";
import { pandocIngestSelfContainedContent } from "../../core/pandoc/self-contained.ts";
import { existsSync1 } from "../../core/file.ts";
import { projectType } from "../../project/types/project-types.ts";
export async function renderPandoc(
file: ExecutedFile,
quiet: boolean,
): Promise<PandocRenderCompletion> {
const { context, recipe, executeResult, resourceFiles } = file;
const format = recipe.format;
if (executeResult.includes) {
format.pandoc = mergePandocIncludes(
format.pandoc || {},
executeResult.includes,
);
}
if (executeResult.pandoc) {
format.pandoc = mergeConfigs(
format.pandoc || {},
executeResult.pandoc,
);
}
if (executeResult.engineDependencies) {
for (const engineName of Object.keys(executeResult.engineDependencies)) {
const engine = executionEngine(engineName)!;
const dependenciesResult = await engine.dependencies({
target: context.target,
format,
output: recipe.output,
resourceDir: resourcePath(),
tempDir: context.options.services.temp.createDir(),
projectDir: context.project?.dir,
libDir: context.libDir,
dependencies: executeResult.engineDependencies[engineName],
quiet: context.options.flags?.quiet,
});
format.pandoc = mergePandocIncludes(
format.pandoc,
dependenciesResult.includes,
);
}
}
const mediabagDir = filesDirMediabagDir(context.target.source);
ensureDirSync(join(dirname(context.target.source), mediabagDir));
const notebookResult = await replaceNotebookPlaceholders(
format.pandoc.to || "html",
context,
context.options.flags || {},
executeResult.markdown,
context.options.services,
);
const embedSupporting: string[] = [];
if (notebookResult.supporting.length) {
embedSupporting.push(...notebookResult.supporting);
}
const pandocIncludes: PandocIncludes = {
[kIncludeAfterBody]: notebookResult.includes?.afterBody
? [notebookResult.includes?.afterBody]
: undefined,
[kIncludeInHeader]: notebookResult.includes?.inHeader
? [notebookResult.includes?.inHeader]
: undefined,
};
format.pandoc = mergePandocIncludes(
format.pandoc,
pandocIncludes,
);
let markdownInput = notebookResult.markdown
? notebookResult.markdown
: executeResult.markdown;
if (format.render[kInlineIncludes]) {
const collectIncludes = (
location:
| "include-in-header"
| "include-before-body"
| "include-after-body",
) => {
const includes = format.pandoc[location];
if (includes) {
const append = location === "include-after-body";
for (const include of includes) {
const includeMd = Deno.readTextFileSync(include);
if (append) {
markdownInput = `${markdownInput}\n\n${includeMd}`;
} else {
markdownInput = `${includeMd}\n\n${markdownInput}`;
}
}
delete format.pandoc[location];
}
};
collectIncludes(kIncludeInHeader);
collectIncludes(kIncludeBeforeBody);
collectIncludes(kIncludeAfterBody);
}
const pandocOptions: PandocOptions = {
markdown: markdownInput,
source: context.target.source,
output: recipe.output,
keepYaml: recipe.keepYaml,
mediabagDir,
libDir: context.libDir,
format,
executionEngine: executeResult.engine,
project: context.project,
args: recipe.args,
services: context.options.services,
metadata: executeResult.metadata,
quiet,
flags: context.options.flags,
};
if (context.project) {
pandocOptions.offset = projectOffset(context.project, context.target.input);
}
const pandocResult = await runPandoc(pandocOptions, executeResult.filters);
if (!pandocResult) {
return Promise.reject();
}
return {
complete: async (renderedFormats: RenderedFormat[], cleanup?: boolean) => {
pushTiming("render-postprocessor");
if (executeResult.postProcess) {
await withTimingAsync("engine-postprocess", async () => {
return await context.engine.postprocess({
engine: context.engine,
target: context.target,
format,
output: recipe.output,
tempDir: context.options.services.temp.createDir(),
projectDir: context.project?.dir,
preserve: executeResult.preserve,
quiet: context.options.flags?.quiet,
});
});
}
const canHtmlPostProcess = isHtmlFileOutput(format.pandoc);
if (!canHtmlPostProcess && pandocResult.htmlPostprocessors.length > 0) {
const postProcessorNames = pandocResult.htmlPostprocessors.map((p) =>
p.name
).join(", ");
const msg =
`Attempt to HTML post process non HTML output using: ${postProcessorNames}`;
throw new Error(msg);
}
const htmlPostProcessors = canHtmlPostProcess
? pandocResult.htmlPostprocessors
: [];
const htmlFinalizers = canHtmlPostProcess
? pandocResult.htmlFinalizers || []
: [];
const htmlPostProcessResult = await runHtmlPostprocessors(
pandocResult.inputMetadata,
pandocResult.inputTraits,
pandocOptions,
htmlPostProcessors,
htmlFinalizers,
renderedFormats,
quiet,
);
const outputFile = isAbsolute(pandocOptions.output)
? pandocOptions.output
: join(dirname(pandocOptions.source), pandocOptions.output);
const postProcessSupporting: string[] = [];
const postProcessResources: string[] = [];
if (pandocResult.postprocessors) {
for (const postprocessor of pandocResult.postprocessors) {
const result = await postprocessor(outputFile);
if (result && result.supporting) {
postProcessSupporting.push(...result.supporting);
}
if (result && result.resources) {
postProcessResources.push(...result.resources);
}
}
}
let finalOutput: string;
let selfContained: boolean;
await withTimingAsync("postprocess-selfcontained", async () => {
const flags = context.options.flags || {};
finalOutput = recipe.output;
selfContained = isSelfContainedOutput(
flags,
format,
finalOutput,
);
if (selfContained && isHtmlFileOutput(format.pandoc)) {
await pandocIngestSelfContainedContent(
outputFile,
format.pandoc[kResourcePath],
);
}
finalOutput = (await recipe.complete(pandocOptions)) || recipe.output;
selfContained = isSelfContainedOutput(
flags,
format,
finalOutput,
);
});
let filesDir: string | undefined = inputFilesDir(context.target.source);
filesDir = existsSync(join(dirname(context.target.source), filesDir))
? filesDir
: undefined;
let supporting = filesDir ? executeResult.supporting : undefined;
if (filesDir && isHtmlFileOutput(format.pandoc)) {
const filesDirAbsolute = join(dirname(context.target.source), filesDir);
if (
existsSync(filesDirAbsolute) &&
(!supporting || !supporting.includes(filesDirAbsolute))
) {
const filesLibs = join(
dirname(context.target.source),
context.libDir,
);
if (
existsSync(filesLibs) &&
(!supporting || !supporting.includes(filesLibs))
) {
supporting = supporting || [];
supporting.push(filesLibs);
}
}
}
if (
htmlPostProcessResult.supporting &&
htmlPostProcessResult.supporting.length > 0
) {
supporting = supporting || [];
supporting.push(...htmlPostProcessResult.supporting);
}
if (embedSupporting && embedSupporting.length > 0) {
supporting = supporting || [];
supporting.push(...embedSupporting);
}
if (postProcessSupporting && postProcessSupporting.length > 0) {
supporting = supporting || [];
supporting.push(...postProcessSupporting);
}
let cleanupSelfContained: string[] | undefined = undefined;
if (selfContained! && supporting) {
cleanupSelfContained = [...supporting];
if (context.project!) {
const libDir = context.project?.config?.project["lib-dir"];
if (libDir) {
const absLibDir = join(context.project.dir, libDir);
cleanupSelfContained = cleanupSelfContained.filter((file) =>
!file.startsWith(absLibDir)
);
}
}
}
if (cleanup !== false) {
withTiming("render-cleanup", () =>
renderCleanup(
context.target.input,
finalOutput!,
format,
file.context.project,
cleanupSelfContained,
executionEngineKeepMd(context),
));
}
const projectPath = (path: string) => {
if (context.project) {
if (isAbsolute(path)) {
return relative(
normalizePath(context.project.dir),
normalizePath(path),
);
} else {
return relative(
normalizePath(context.project.dir),
normalizePath(join(dirname(context.target.source), path)),
);
}
} else {
return path;
}
};
popTiming();
const files = resourceFiles.concat(htmlPostProcessResult.resources)
.concat(postProcessResources);
const result: RenderedFile = {
isTransient: recipe.isOutputTransient,
input: projectPath(context.target.source),
markdown: executeResult.markdown,
format,
supporting: supporting
? supporting.filter(existsSync1).map((file: string) =>
context.project ? relative(context.project.dir, file) : file
)
: undefined,
file: recipe.isOutputTransient
? finalOutput!
: projectPath(finalOutput!),
resourceFiles: {
globs: pandocResult.resources,
files,
},
selfContained: selfContained!,
};
return result;
},
};
}
export function renderResultFinalOutput(
renderResults: RenderResult,
relativeToInputDir?: string,
) {
let result = renderResults.files.find((file) => {
return !file.supplemental;
});
if (!result) {
return undefined;
}
for (const fileResult of renderResults.files) {
if (fileResult.file === "index.html" && !fileResult.supplemental) {
result = fileResult;
break;
}
}
if (renderResults.context) {
const projType = projectType(renderResults.context.config?.project.type);
if (projType && projType.renderResultFinalOutput) {
const projectResult = projType.renderResultFinalOutput(
renderResults,
relativeToInputDir,
);
if (projectResult) {
result = projectResult;
}
}
}
let finalInput = result.input;
let finalOutput = result.file;
if (renderResults.baseDir) {
finalInput = join(renderResults.baseDir, finalInput);
if (renderResults.outputDir) {
finalOutput = join(
renderResults.baseDir,
renderResults.outputDir,
finalOutput,
);
} else {
finalOutput = join(renderResults.baseDir, finalOutput);
}
} else {
finalOutput = join(dirname(finalInput), finalOutput);
}
if (!safeExistsSync(finalOutput)) {
return undefined;
}
if (relativeToInputDir) {
const inputRealPath = normalizePath(relativeToInputDir);
const outputRealPath = normalizePath(finalOutput);
return relative(inputRealPath, outputRealPath);
} else {
return finalOutput;
}
}
export function renderResultUrlPath(
renderResult: RenderResult,
) {
if (renderResult.baseDir && renderResult.outputDir) {
const finalOutput = renderResultFinalOutput(
renderResult,
);
if (finalOutput) {
const targetPath = pathWithForwardSlashes(relative(
join(renderResult.baseDir, renderResult.outputDir),
finalOutput,
));
return targetPath;
}
}
return undefined;
}
function mergePandocIncludes(
format: FormatPandoc,
pandocIncludes: PandocIncludes,
) {
return mergeConfigs(format, pandocIncludes);
}
async function runHtmlPostprocessors(
inputMetadata: Metadata,
inputTraits: PandocInputTraits,
options: PandocOptions,
htmlPostprocessors: Array<HtmlPostProcessor>,
htmlFinalizers: Array<(doc: Document) => Promise<void>>,
renderedFormats: RenderedFormat[],
quiet?: boolean,
): Promise<HtmlPostProcessResult> {
const postProcessResult: HtmlPostProcessResult = {
resources: [],
supporting: [],
};
if (htmlPostprocessors.length > 0 || htmlFinalizers.length > 0) {
await withTimingAsync("htmlPostprocessors", async () => {
const outputFile = isAbsolute(options.output)
? options.output
: join(dirname(options.source), options.output);
const htmlInput = Deno.readTextFileSync(outputFile);
const doctypeMatch = htmlInput.match(/^<!DOCTYPE.*?>/);
const doc = await parseHtml(htmlInput);
for (let i = 0; i < htmlPostprocessors.length; i++) {
const postprocessor = htmlPostprocessors[i];
const result = await postprocessor(
doc,
{
inputMetadata,
inputTraits,
renderedFormats,
quiet,
},
);
postProcessResult.resources.push(...result.resources);
postProcessResult.supporting.push(...result.supporting);
}
for (let i = 0; i < htmlFinalizers.length; i++) {
const finalizer = htmlFinalizers[i];
await finalizer(doc);
}
const htmlOutput = (doctypeMatch ? doctypeMatch[0] + "\n" : "") +
doc.documentElement?.outerHTML!;
Deno.writeTextFileSync(outputFile, htmlOutput);
});
}
return postProcessResult;
}