Path: blob/main/src/command/render/render-files.ts
3583 views
/*1* render-files.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56// ensures cell handlers are installed7import "../../core/handlers/handlers.ts";89import {10kExecuteEnabled,11kFreeze,12kIncludeInHeader,13kKeepMd,14kLang,15kQuartoRequired,16} from "../../config/constants.ts";17import { isHtmlCompatible } from "../../config/format.ts";18import { mergeConfigs } from "../../core/config.ts";19import { initDayJsPlugins, setDateLocale } from "../../core/date.ts";20import { initDenoDom } from "../../core/deno-dom.ts";21import { HandlerContextResults } from "../../core/handlers/types.ts";22import {23handleLanguageCells,24languages,25resetFigureCounter,26} from "../../core/handlers/base.ts";27import { LanguageCellHandlerOptions } from "../../core/handlers/types.ts";28import { asMappedString, mappedDiff } from "../../core/mapped-text.ts";29import {30validateDocument,31validateDocumentFromSource,32} from "../../core/schema/validate-document.ts";33import { createTempContext, TempContext } from "../../core/temp.ts";34import {35executionEngineKeepMd,36fileExecutionEngineAndTarget,37} from "../../execute/engine.ts";38import { annotateOjsLineNumbers } from "../../execute/ojs/annotate-source.ts";39import { ojsExecuteResult } from "../../execute/ojs/compile.ts";40import {41ExecuteOptions,42ExecuteResult,43MappedExecuteResult,44} from "../../execute/types.ts";45import { kProjectLibDir, ProjectContext } from "../../project/types.ts";46import { outputRecipe } from "./output.ts";4748import { renderPandoc } from "./render.ts";49import { PandocRenderCompletion, RenderServices } from "./types.ts";50import { renderContexts } from "./render-contexts.ts";51import { renderProgress } from "./render-info.ts";52import {53ExecutedFile,54PandocRenderer,55RenderContext,56RenderedFile,57RenderedFormat,58RenderExecuteOptions,59RenderFile,60RenderFilesResult,61RenderFlags,62RenderOptions,63} from "./types.ts";64import { error, info } from "../../deno_ral/log.ts";65import * as ld from "../../core/lodash.ts";66import { basename, dirname, join, relative } from "../../deno_ral/path.ts";67import { Format } from "../../config/types.ts";68import {69figuresDir,70inputFilesDir,71isServerShiny,72isServerShinyKnitr,73} from "../../core/render.ts";74import {75normalizePath,76removeIfEmptyDir,77removeIfExists,78} from "../../core/path.ts";79import { resourcePath } from "../../core/resources.ts";80import { YAMLValidationError } from "../../core/yaml.ts";81import { resolveParams } from "./flags.ts";82import {83copyFromProjectFreezer,84copyToProjectFreezer,85defrostExecuteResult,86freezeExecuteResult,87freezerFigsDir,88freezerFreezeFile,89kProjectFreezeDir,90removeFreezeResults,91} from "./freeze.ts";92import { isJupyterNotebook } from "../../core/jupyter/jupyter.ts";93import { MappedString } from "../../core/lib/text-types.ts";94import {95createNamedLifetime,96Lifetime,97waitUntilNamedLifetime,98} from "../../core/lifetimes.ts";99import { resolveDependencies } from "./pandoc-dependencies-html.ts";100import {101getData as getTimingData,102pop as popTiming,103push as pushTiming,104withTiming,105withTimingAsync,106} from "../../core/timing.ts";107import { satisfies } from "semver/mod.ts";108import { quartoConfig } from "../../core/quarto.ts";109import { ensureNotebookContext } from "../../core/jupyter/jupyter-embed.ts";110import {111projectIsWebsite,112projectOutputDir,113} from "../../project/project-shared.ts";114import { NotebookContext } from "../../render/notebook/notebook-types.ts";115import { setExecuteEnvironment } from "../../execute/environment.ts";116import { safeCloneDeep } from "../../core/safe-clone-deep.ts";117import { warn } from "log";118119export async function renderExecute(120context: RenderContext,121output: string,122options: RenderExecuteOptions,123): Promise<ExecuteResult> {124// are we running a compatible quarto version for this file?125const versionConstraint = context126.format.metadata[kQuartoRequired] as (string | undefined);127if (versionConstraint) {128const ourVersion = quartoConfig.version();129let result: boolean;130try {131result = satisfies(ourVersion, versionConstraint);132} catch (_e) {133throw new Error(134`In file ${context.target.source}:\nVersion constraint is invalid: ${versionConstraint}.`,135);136}137if (!result) {138throw new Error(139`in file ${context.target.source}:\nQuarto version ${ourVersion} does not satisfy version constraint ${versionConstraint}.`,140);141}142}143144// alias options145const { resolveDependencies = true, alwaysExecute = false } = options;146147// alias flags148const flags = context.options.flags || {};149150// compute filesDir151const filesDir = inputFilesDir(context.target.source);152153// compute project relative files dir (if we are in a project)154let projRelativeFilesDir: string | undefined;155if (context.project) {156const inputDir = relative(157context.project.dir,158dirname(context.target.source),159);160projRelativeFilesDir = join(inputDir, filesDir);161}162163// are we eligible to freeze?164const canFreeze = context.engine.canFreeze &&165(context.format.execute[kExecuteEnabled] !== false);166167// use previous frozen results if they are available168if (context.project && !context.project.isSingleFile && !alwaysExecute) {169// check if we are using the freezer170171const thaw = canFreeze &&172(context.format.execute[kFreeze] ||173(context.options.useFreezer ? "auto" : false));174175if (thaw) {176// copy from project freezer177const hidden = context.format.execute[kFreeze] === false;178copyFromProjectFreezer(179context.project,180projRelativeFilesDir!,181hidden,182);183184const thawedResult = defrostExecuteResult(185context.target.source,186output,187context.options.services.temp,188thaw === true,189);190if (thawedResult) {191// copy the site_libs dir from the freezer192const libDir = context.project?.config?.project[kProjectLibDir];193if (libDir) {194copyFromProjectFreezer(context.project, libDir, hidden);195}196197// remove the results dir198removeFreezeResults(join(context.project.dir, projRelativeFilesDir!));199200// notify engine that we skipped execute201if (context.engine.executeTargetSkipped) {202context.engine.executeTargetSkipped(203context.target,204context.format,205context.project,206);207}208209// return results210return thawedResult;211}212}213}214215// calculate figsDir216const figsDir = join(filesDir, figuresDir(context.format.pandoc.to));217218pushTiming("render-execute");219220const executeOptions: ExecuteOptions = {221target: context.target,222resourceDir: resourcePath(),223tempDir: context.options.services.temp.createDir(),224dependencies: resolveDependencies,225libDir: context.libDir,226format: context.format,227projectDir: context.project?.dir,228cwd: flags.executeDir || dirname(normalizePath(context.target.source)),229params: resolveParams(flags.params, flags.paramsFile),230quiet: flags.quiet,231previewServer: context.options.previewServer,232handledLanguages: languages(),233project: context.project,234};235// execute computations236setExecuteEnvironment(executeOptions);237const executeResult = await context.engine.execute(executeOptions);238popTiming();239240// write the freeze file if we are in a project241if (context.project && !context.project.isSingleFile && canFreeze) {242// write the freezer file243const freezeFile = freezeExecuteResult(244context.target.source,245output,246executeResult,247);248249// always copy to the hidden freezer250copyToProjectFreezer(context.project, projRelativeFilesDir!, true, true);251252// copy to the _freeze dir if explicit _freeze is requested253if (context.format.execute[kFreeze] !== false) {254copyToProjectFreezer(context.project, projRelativeFilesDir!, false, true);255} else {256// otherwise cleanup the _freeze subdir b/c we aren't explicitly freezing anymore257258// figs dir for this target format259const freezeFigsDir = freezerFigsDir(260context.project,261projRelativeFilesDir!,262basename(figsDir),263);264removeIfExists(freezeFigsDir);265266// freezer file267const projRelativeFreezeFile = relative(context.project.dir, freezeFile);268const freezerFile = freezerFreezeFile(269context.project,270projRelativeFreezeFile,271);272removeIfExists(freezerFile);273274// remove empty directories275removeIfEmptyDir(dirname(freezerFile));276removeIfEmptyDir(dirname(freezeFigsDir));277removeIfEmptyDir(join(context.project.dir, kProjectFreezeDir));278}279280// remove the freeze results file (now that it's safely in the freezer)281removeFreezeResults(join(context.project.dir, projRelativeFilesDir!));282}283284// return result285return executeResult;286}287288export async function renderFiles(289files: RenderFile[],290options: RenderOptions,291notebookContext: NotebookContext,292alwaysExecuteFiles: string[] | undefined,293pandocRenderer: PandocRenderer | undefined,294project: ProjectContext,295): Promise<RenderFilesResult> {296// provide default renderer297pandocRenderer = pandocRenderer || defaultPandocRenderer(options, project);298299// create temp context300const tempContext = createTempContext();301302try {303// make a copy of options so we don't mutate caller context304options = ld.cloneDeep(options);305306// see if we should be using file-by-file progress307const progress = options.progress ||308(project && (files.length > 1) && !options.flags?.quiet);309310// quiet pandoc output if we are doing file by file progress311const pandocQuiet = !!progress || !!options.quietPandoc;312313// calculate num width314const numWidth = String(files.length).length;315316for (let i = 0; i < files.length; i++) {317const file = files[i];318319if (progress) {320renderProgress(321`\r[${String(i + 1).padStart(numWidth)}/${files.length}] ${322relative(project!.dir, file.path)323}`,324);325}326327// get contexts328const fileLifetime = await waitUntilNamedLifetime("render-file");329try {330await renderFileInternal(331fileLifetime,332file,333options,334project,335pandocRenderer,336files,337tempContext,338alwaysExecuteFiles,339pandocQuiet,340notebookContext,341);342} finally {343fileLifetime.cleanup();344}345}346347if (progress) {348info("");349}350351return await pandocRenderer.onComplete(false, options.flags?.quiet);352} catch (error) {353if (!(error instanceof Error)) {354warn("Should not have arrived here:", error);355throw error;356}357return {358files: (await pandocRenderer.onComplete(true)).files,359error: error || new Error(),360};361} finally {362tempContext.cleanup();363if (Deno.env.get("QUARTO_PROFILER_OUTPUT")) {364Deno.writeTextFileSync(365Deno.env.get("QUARTO_PROFILER_OUTPUT")!,366JSON.stringify(getTimingData()),367);368}369}370}371372export async function renderFile(373file: RenderFile,374options: RenderOptions,375services: RenderServices,376project: ProjectContext,377enforceProjectFormats: boolean = true,378): Promise<RenderFilesResult> {379// provide default renderer380const pandocRenderer = defaultPandocRenderer(options, project);381382try {383// make a copy of options so we don't mutate caller context384options = ld.cloneDeep(options);385386// quiet pandoc output if we are doing file by file progress387const pandocQuiet = !!options.quietPandoc;388389// get contexts390const fileLifetime = createNamedLifetime("render-single-file");391try {392await renderFileInternal(393fileLifetime,394file,395options,396project,397pandocRenderer,398[file],399services.temp,400[],401pandocQuiet,402services.notebook,403enforceProjectFormats,404);405} finally {406fileLifetime.cleanup();407}408return await pandocRenderer.onComplete(false, options.flags?.quiet);409} catch (error) {410if (!(error instanceof Error)) {411warn("Should not have arrived here:", error);412throw error;413}414return {415files: (await pandocRenderer.onComplete(true)).files,416error: error || new Error(),417};418} finally {419if (Deno.env.get("QUARTO_PROFILER_OUTPUT")) {420Deno.writeTextFileSync(421Deno.env.get("QUARTO_PROFILER_OUTPUT")!,422JSON.stringify(getTimingData()),423);424}425}426}427428async function renderFileInternal(429lifetime: Lifetime,430file: RenderFile,431options: RenderOptions,432project: ProjectContext,433pandocRenderer: PandocRenderer,434files: RenderFile[],435tempContext: TempContext,436alwaysExecuteFiles: string[] | undefined,437pandocQuiet: boolean,438notebookContext: NotebookContext,439enforceProjectFormats: boolean = true,440) {441const outputs: Array<RenderedFormat> = [];442let contexts: Record<string, RenderContext> | undefined;443try {444contexts = await renderContexts(445file,446options,447true,448notebookContext,449project,450false,451enforceProjectFormats,452);453454// Allow renderers to filter the contexts455contexts = pandocRenderer.onFilterContexts(456file.path,457contexts,458files,459options,460);461} catch (e) {462// bad YAML can cause failure before validation. We463// reconstruct the context as best we can and try to validate.464// note that this ignores "validate-yaml: false"465const { engine, target } = await fileExecutionEngineAndTarget(466file.path,467options.flags,468project,469);470const validationResult = await validateDocumentFromSource(471target.markdown,472engine.name,473error,474);475if (validationResult.length) {476throw new RenderInvalidYAMLError();477} else {478// rethrow if no validation error happened.479throw e;480}481}482const mergeHandlerResults = (483results: HandlerContextResults | undefined,484executeResult: MappedExecuteResult,485context: RenderContext,486) => {487if (results === undefined) {488return;489}490if (executeResult.includes) {491executeResult.includes = mergeConfigs(492executeResult.includes,493results.includes,494);495} else {496executeResult.includes = results.includes;497}498const extras = resolveDependencies(499results.extras,500dirname(context.target.source),501context.libDir,502tempContext,503project,504);505if (extras[kIncludeInHeader]) {506// note that we merge engine execute results back into cell handler507// execute results so that jupyter widget dependencies appear at the508// end (so that they don't mess w/ other libs using require/define)509executeResult.includes[kIncludeInHeader] = [510...(extras[kIncludeInHeader] || []),511...(executeResult.includes[kIncludeInHeader] || []),512];513}514executeResult.supporting.push(...results.supporting);515};516517for (const format of Object.keys(contexts)) {518pushTiming("render-context");519const context = safeCloneDeep(contexts[format]); // since we're going to mutate it...520521// disquality some documents from server: shiny522if (isServerShiny(context.format) && context.project) {523const src = relative(context.project?.dir!, context.target.source);524if (projectIsWebsite(context.project)) {525error(526`${src} uses server: shiny so cannot be included in a website project ` +527`(shiny documents require a backend server and so can't be published as static web content).`,528);529throw new Error();530} else if (531(projectOutputDir(context.project) !==532normalizePath(context.project.dir)) &&533isServerShinyKnitr(context.format, context.engine.name)534) {535error(536`${src} is a knitr engine document that uses server: shiny so cannot be included in a project with an output-dir ` +537`(shiny document output must be rendered alongside its source document).`,538);539throw new Error();540}541}542543// get output recipe544const recipe = outputRecipe(context);545outputs.push({546path: recipe.finalOutput || recipe.output,547isTransient: recipe.isOutputTransient,548format: context.format,549});550551if (context.active) {552// Set the date locale for this render553// Used for date formatting554initDayJsPlugins();555const resolveLang = () => {556const lang = context.format.metadata[kLang] ||557options.flags?.pandocMetadata?.[kLang];558if (typeof lang === "string") {559return lang;560} else {561return undefined;562}563};564const dateFormatLang = resolveLang();565if (dateFormatLang) {566await setDateLocale(567dateFormatLang,568);569}570571lifetime.attach({572cleanup() {573resetFigureCounter();574},575});576try {577// one time denoDom init for html compatible formats578if (isHtmlCompatible(context.format)) {579await initDenoDom();580}581582// determine execute options583const executeOptions = mergeConfigs(584{585alwaysExecute: alwaysExecuteFiles?.includes(file.path),586},587pandocRenderer.onBeforeExecute(recipe.format),588);589590const validate = context.format.render?.["validate-yaml"];591if (validate !== false) {592const validationResult = await validateDocument(context);593if (validationResult.length) {594throw new RenderInvalidYAMLError();595}596}597598// FIXME it should be possible to infer this directly now599// based on the information in the mapped strings.600//601// collect line numbers to facilitate runtime error reporting602const { ojsBlockLineNumbers } = annotateOjsLineNumbers(context);603604// execute605const baseExecuteResult = await renderExecute(606context,607recipe.output,608executeOptions,609);610611// recover source map from diff and create a mappedExecuteResult612// for markdown processing pre-pandoc with mapped strings613let mappedMarkdown: MappedString;614615withTiming("diff-execute-result", () => {616if (!isJupyterNotebook(context.target.source)) {617mappedMarkdown = mappedDiff(618context.target.markdown,619baseExecuteResult.markdown,620);621} else {622mappedMarkdown = asMappedString(baseExecuteResult.markdown);623}624});625626const resourceFiles: string[] = [];627if (baseExecuteResult.resourceFiles) {628resourceFiles.push(...baseExecuteResult.resourceFiles);629}630631const languageCellHandlerOptions: LanguageCellHandlerOptions = {632name: "",633temp: tempContext,634format: recipe.format,635markdown: mappedMarkdown!,636context,637flags: options.flags || {} as RenderFlags,638stage: "post-engine",639};640641let unmappedExecuteResult: ExecuteResult;642await withTimingAsync("handle-language-cells", async () => {643// handle language cells644const { markdown, results } = await handleLanguageCells(645languageCellHandlerOptions,646);647const mappedExecuteResult: MappedExecuteResult = {648...baseExecuteResult,649markdown,650};651652mergeHandlerResults(653context.target.preEngineExecuteResults,654mappedExecuteResult,655context,656);657mergeHandlerResults(results, mappedExecuteResult, context);658659// process ojs660const { executeResult, resourceFiles: ojsResourceFiles } =661await ojsExecuteResult(662context,663mappedExecuteResult,664ojsBlockLineNumbers,665);666resourceFiles.push(...ojsResourceFiles);667668// keep md if requested669const keepMd = executionEngineKeepMd(context);670if (keepMd && context.format.execute[kKeepMd]) {671Deno.writeTextFileSync(keepMd, executeResult.markdown.value);672}673674// now get "unmapped" execute result back to send to pandoc675unmappedExecuteResult = {676...executeResult,677markdown: executeResult.markdown.value,678};679});680681// Ensure that we have rendered any notebooks682await ensureNotebookContext(683unmappedExecuteResult!.markdown,684context.options.services,685project,686);687688// callback689pushTiming("render-pandoc");690await pandocRenderer.onRender(format, {691context,692recipe,693executeResult: unmappedExecuteResult!,694resourceFiles,695}, pandocQuiet);696popTiming();697} finally {698popTiming();699}700}701}702await pandocRenderer.onPostProcess(outputs, project);703}704705// default pandoc renderer immediately renders each execute result706function defaultPandocRenderer(707_options: RenderOptions,708_project: ProjectContext,709): PandocRenderer {710const renderCompletions: PandocRenderCompletion[] = [];711const renderedFiles: RenderedFile[] = [];712713return {714onFilterContexts: (715_file: string,716contexts: Record<string, RenderContext>,717_files: RenderFile[],718_options: RenderOptions,719) => {720return contexts;721},722onBeforeExecute: (_format: Format) => ({}),723onRender: async (724_format: string,725executedFile: ExecutedFile,726quiet: boolean,727) => {728renderCompletions.push(await renderPandoc(executedFile, quiet));729},730onPostProcess: async (renderedFormats: RenderedFormat[]) => {731let completion = renderCompletions.pop();732while (completion) {733renderedFiles.push(await completion.complete(renderedFormats));734completion = renderCompletions.pop();735}736renderedFiles.reverse();737},738onComplete: async () => {739return {740files: await Promise.resolve(renderedFiles),741};742},743};744}745class RenderInvalidYAMLError extends YAMLValidationError {746constructor() {747super("Render failed due to invalid YAML.");748}749}750751752