Path: blob/main/src/render/notebook/notebook-context.ts
6458 views
/*1* notebook-context.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { ExecutedFile, RenderServices } from "../../command/render/types.ts";7import { InternalError } from "../../core/lib/error.ts";8import { kJatsSubarticle } from "../../format/jats/format-jats-types.ts";9import { ProjectContext } from "../../project/types.ts";10import {11kHtmlPreview,12kQmdIPynb,13kRenderedIPynb,14Notebook,15NotebookContext,16NotebookContributor,17NotebookMetadata,18NotebookOutput,19NotebookRenderResult,20RenderType,21} from "./notebook-types.ts";2223import { basename, dirname, isAbsolute, join } from "../../deno_ral/path.ts";24import { jatsContributor } from "./notebook-contributor-jats.ts";25import { htmlNotebookContributor } from "./notebook-contributor-html.ts";26import { outputNotebookContributor } from "./notebook-contributor-ipynb.ts";27import { Format } from "../../config/types.ts";28import { safeExistsSync, safeRemoveIfExists } from "../../core/path.ts";29import { qmdNotebookContributor } from "./notebook-contributor-qmd.ts";30import { debug } from "../../deno_ral/log.ts";3132const contributors: Record<RenderType, NotebookContributor | undefined> = {33[kJatsSubarticle]: jatsContributor,34[kHtmlPreview]: htmlNotebookContributor,35[kRenderedIPynb]: outputNotebookContributor,36[kQmdIPynb]: qmdNotebookContributor,37};3839export function notebookContext(): NotebookContext {40const notebooks: Record<string, Notebook> = {};41const preserveNotebooks: Record<string, RenderType[]> = {};42let nbCount = 0;4344const token = () => {45return `nb-${++nbCount}`;46};4748const emptyNotebook = (nbAbsPath: string): Notebook => {49return {50source: nbAbsPath,51};52};5354// Adds a rendering of a notebook to the notebook context55const addRendering = (56nbAbsPath: string,57renderType: RenderType,58result: NotebookRenderResult,59context: ProjectContext,60cached?: boolean,61) => {62debug(`[NotebookContext]: Add Rendering (${renderType}):${nbAbsPath}`);6364const hrefPath = join(dirname(nbAbsPath), basename(result.file));65const absPath = isAbsolute(result.file) ? result.file : hrefPath;66const output: NotebookOutput = {67path: absPath,68hrefPath,69supporting: result.supporting || [],70resourceFiles: result.resourceFiles,71cached,72};7374const nb: Notebook = notebooks[nbAbsPath] || emptyNotebook(nbAbsPath);75nb[renderType] = output;76notebooks[nbAbsPath] = nb;77needRewrite = true;7879if (context) {80const contrib = contributor(renderType);81if (contrib.cache) {82contrib.cache(output, context);83}84}85};8687// Removes a rendering of a notebook from the notebook context88// which includes cleaning up the files. This should only be89// used when the caller knows other callers will not need the90// notebook.91const removeRendering = (92nbAbsPath: string,93renderType: RenderType,94preserveFiles: string[],95) => {96debug(`[NotebookContext]: Remove Rendering (${renderType}):${nbAbsPath}`);97if (98preserveNotebooks[nbAbsPath] &&99preserveNotebooks[nbAbsPath].includes(renderType)100) {101// Someone asked to preserve this, don't clean it up102return;103}104const nb: Notebook = notebooks[nbAbsPath];105if (nb) {106const rendering = nb[renderType];107108if (rendering) {109safeRemoveIfExists(rendering.path);110const filteredSupporting = rendering.supporting.filter(111(file) => {112const absPath = join(dirname(nbAbsPath), file);113return !preserveFiles.includes(absPath);114},115);116for (const supporting of filteredSupporting) {117safeRemoveIfExists(supporting);118}119}120}121};122123// Get a contribute for a render type124function contributor(renderType: RenderType) {125const contributor = contributors[renderType];126if (contributor) {127return contributor;128} else {129throw new InternalError(130`Missing contributor ${renderType} when resolving`,131);132}133}134135// Add metadata to a given notebook rendering136function addMetadata(137nbAbsPath: string,138nbMeta: NotebookMetadata,139) {140debug(`[NotebookContext]: Add Notebook Metadata:${nbAbsPath}`);141const nb: Notebook = notebooks[nbAbsPath] || emptyNotebook(nbAbsPath);142nb.metadata = nbMeta;143notebooks[nbAbsPath] = nb;144}145146function reviveOutput(147nbAbsPath: string,148renderType: RenderType,149context: ProjectContext,150) {151debug(152`[NotebookContext]: Attempting to Revive Rendering (${renderType}):${nbAbsPath}`,153);154const contrib = contributor(renderType);155156if (contrib.cachedPath) {157const existingPath = contrib.cachedPath(nbAbsPath, context);158if (existingPath) {159if (safeExistsSync(existingPath)) {160const inputTime = Deno.statSync(nbAbsPath).mtime?.valueOf() || 0;161const outputTime = Deno.statSync(existingPath).mtime?.valueOf() || 0;162if (inputTime <= outputTime) {163debug(164`[NotebookContext]: Revived Rendering (${renderType}):${nbAbsPath}`,165);166addRendering(167nbAbsPath,168renderType,169{170file: existingPath,171supporting: [],172resourceFiles: {173globs: [],174files: [],175},176},177context,178true,179);180}181}182}183}184}185186function preserve(nbAbsPath: string, renderType: RenderType) {187debug(`[NotebookContext]: Preserving (${renderType}):${nbAbsPath}`);188preserveNotebooks[nbAbsPath] = preserveNotebooks[nbAbsPath] || [];189if (!preserveNotebooks[nbAbsPath].includes(renderType)) {190preserveNotebooks[nbAbsPath].push(renderType);191}192}193194let allNotebooksTempFilename: string | undefined;195let needRewrite = true;196197return {198all: (context: ProjectContext) => {199if (!allNotebooksTempFilename) {200allNotebooksTempFilename = context.temp.createFile({201suffix: ".json",202});203}204if (needRewrite) {205debug(206`[NotebookContext]: Writing all notebooks to ${allNotebooksTempFilename}`,207);208const objs = Object.values(notebooks);209Deno.writeTextFileSync(210allNotebooksTempFilename,211JSON.stringify(objs),212);213needRewrite = false;214}215return allNotebooksTempFilename;216},217get: (nbAbsPath: string, context?: ProjectContext) => {218debug(`[NotebookContext]: Get Notebook:${nbAbsPath}`);219const notebook = notebooks[nbAbsPath];220const reviveRenders: RenderType[] = [];221if (notebook) {222// We already have a notebook, try to complete its renderings223// by reviving any outputs that are valid224[kJatsSubarticle, kHtmlPreview, kRenderedIPynb].forEach(225(renderTypeStr) => {226const renderType = renderTypeStr as RenderType;227if (!notebook[renderType]) {228reviveRenders.push(renderType);229}230},231);232} else {233reviveRenders.push(kHtmlPreview);234reviveRenders.push(kJatsSubarticle);235reviveRenders.push(kRenderedIPynb);236reviveRenders.push(kQmdIPynb);237}238239if (context) {240// See if an up to date rendered result exists for each contributor241// TODO: consider doing this check only when a render type is requested242// or at some other time to reduce the frequency (currently revive is being243// attempted anytime a notebook `get` is called)244for (const renderType of reviveRenders) {245reviveOutput(nbAbsPath, renderType, context);246}247}248return notebooks[nbAbsPath];249},250addMetadata: (nbAbsPath: string, notebookMetadata: NotebookMetadata) => {251addMetadata(nbAbsPath, notebookMetadata);252},253resolve: (254nbAbsPath: string,255renderType: RenderType,256executedFile: ExecutedFile,257notebookMetadata?: NotebookMetadata,258) => {259debug(260`[NotebookContext]: Resolving ExecutedFile (${renderType}):${nbAbsPath}`,261);262if (notebookMetadata) {263addMetadata(nbAbsPath, notebookMetadata);264}265return contributor(renderType).resolve(266nbAbsPath,267token(),268executedFile,269notebookMetadata,270);271},272addRendering,273removeRendering,274render: async (275nbAbsPath: string,276format: Format,277renderType: RenderType,278services: RenderServices,279notebookMetadata: NotebookMetadata | undefined,280project: ProjectContext,281) => {282debug(`[NotebookContext]: Rendering (${renderType}):${nbAbsPath}`);283284if (notebookMetadata) {285addMetadata(nbAbsPath, notebookMetadata);286}287288// If there is a source representation of the qmd file289// we should use that, which will prevent rexecution of the290// QMD291const notebook = notebooks[nbAbsPath];292const toRenderPath = notebook293? notebook[kQmdIPynb] ? notebook[kQmdIPynb].path : nbAbsPath294: nbAbsPath;295296const renderedFile = await contributor(renderType).render(297toRenderPath,298format,299token(),300services,301notebookMetadata,302project,303);304305addRendering(nbAbsPath, renderType, renderedFile, project);306if (!notebooks[nbAbsPath][renderType]) {307throw new InternalError(308"We just rendered and contributed a notebook, but it isn't present in the notebook context.",309);310}311return notebooks[nbAbsPath][renderType]!;312},313preserve,314cleanup: () => {315debug(`[NotebookContext]: Starting Cleanup`);316const hasNotebooks = Object.keys(notebooks).length > 0;317if (hasNotebooks) {318Object.keys(contributors).forEach((renderTypeStr) => {319Object.values(notebooks).forEach((notebook) => {320const renderType = renderTypeStr as RenderType;321// Check to see if this is preserved, if it is322// skip clean up for this notebook and render type323if (324!preserveNotebooks[notebook.source] ||325!preserveNotebooks[notebook.source].includes(renderType)326) {327const notebookOutput = notebook[renderType];328if (notebookOutput && notebookOutput.cached !== true) {329debug(330`[NotebookContext]: Cleanup (${renderType}):${notebook.source}`,331);332debug(333`[NotebookContext]: Deleting (${notebookOutput.path}`,334);335safeRemoveIfExists(notebookOutput.path);336for (const supporting of notebookOutput.supporting) {337safeRemoveIfExists(supporting);338}339}340}341});342});343}344},345};346}347348349