diff --git a/src/command/preview/cmd.ts b/src/command/preview/cmd.ts index a39632befb..2d46db4de6 100644 --- a/src/command/preview/cmd.ts +++ b/src/command/preview/cmd.ts @@ -52,6 +52,7 @@ import { previewShiny } from "./preview-shiny.ts"; import { serve } from "../serve/serve.ts"; import { fileExecutionEngine } from "../../execute/engine.ts"; import { notebookContext } from "../../render/notebook/notebook-context.ts"; +import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts"; export const previewCommand = new Command() .name("preview") @@ -275,7 +276,8 @@ export const previewCommand = new Command() if (Deno.statSync(file).isFile) { // get project and preview format const nbContext = notebookContext(); - const project = await projectContext(dirname(file), nbContext); + const project = (await projectContext(dirname(file), nbContext)) || + singleFileProjectContext(file, nbContext); const formats = await (async () => { const services = renderServices(nbContext); try { diff --git a/src/command/preview/preview.ts b/src/command/preview/preview.ts index 65e3b7f48e..b72130287a 100644 --- a/src/command/preview/preview.ts +++ b/src/command/preview/preview.ts @@ -100,6 +100,7 @@ import { rswURL, } from "../../core/previewurl.ts"; import { notebookContext } from "../../render/notebook/notebook-context.ts"; +import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts"; export async function resolvePreviewOptions( options: ProjectPreview, @@ -365,11 +366,13 @@ export async function previewFormat( if (format) { return format; } + const nbContext = notebookContext(); + project = project || singleFileProjectContext(file, nbContext); formats = formats || await withRenderServices( - notebookContext(), + nbContext, (services: RenderServices) => - renderFormats(file, services, "all", project), + renderFormats(file, services, "all", project!), ); format = Object.keys(formats)[0] || "html"; return format; diff --git a/src/command/render/render-contexts.ts b/src/command/render/render-contexts.ts index a9da0fda1d..1a9c70c647 100644 --- a/src/command/render/render-contexts.ts +++ b/src/command/render/render-contexts.ts @@ -210,7 +210,7 @@ export async function renderContexts( options: RenderOptions, forExecute: boolean, notebookContext: NotebookContext, - project?: ProjectContext, + project: ProjectContext, cloneOptions: boolean = true, enforceProjectFormats: boolean = true, ): Promise> { @@ -304,11 +304,10 @@ export async function renderContexts( if (engineClaimReason === "markdown") { // since the content decided the engine, and the content now changed, // we need to re-evaluate the engine and target based on new content. - const { engine, target } = await fileExecutionEngineAndTarget( + const { engine, target } = await project.fileExecutionEngineAndTarget( file.path, - options.flags, markdown, - project, + true, ); context.engine = engine; context.target = target; @@ -327,7 +326,7 @@ export async function renderFormats( file: string, services: RenderServices, to = "all", - project?: ProjectContext, + project: ProjectContext, ): Promise> { const contexts = await renderContexts( { path: file }, diff --git a/src/command/render/render-files.ts b/src/command/render/render-files.ts index 11dd5857d2..35e7b49a77 100644 --- a/src/command/render/render-files.ts +++ b/src/command/render/render-files.ts @@ -288,9 +288,9 @@ export async function renderFiles( files: RenderFile[], options: RenderOptions, notebookContext: NotebookContext, - alwaysExecuteFiles?: string[], - pandocRenderer?: PandocRenderer, - project?: ProjectContext, + alwaysExecuteFiles: string[] | undefined, + pandocRenderer: PandocRenderer | undefined, + project: ProjectContext, ): Promise { // provide default renderer pandocRenderer = pandocRenderer || defaultPandocRenderer(options, project); @@ -368,7 +368,7 @@ export async function renderFile( file: RenderFile, options: RenderOptions, services: RenderServices, - project?: ProjectContext, + project: ProjectContext, enforceProjectFormats: boolean = true, ): Promise { // provide default renderer @@ -420,7 +420,7 @@ async function renderFileInternal( lifetime: Lifetime, file: RenderFile, options: RenderOptions, - project: ProjectContext | undefined, + project: ProjectContext, pandocRenderer: PandocRenderer, files: RenderFile[], tempContext: TempContext, @@ -456,6 +456,8 @@ async function renderFileInternal( const { engine, target } = await fileExecutionEngineAndTarget( file.path, options.flags, + undefined, + project, ); const validationResult = await validateDocumentFromSource( target.markdown, @@ -695,7 +697,7 @@ async function renderFileInternal( // default pandoc renderer immediately renders each execute result function defaultPandocRenderer( _options: RenderOptions, - _project?: ProjectContext, + _project: ProjectContext, ): PandocRenderer { const renderCompletions: PandocRenderCompletion[] = []; const renderedFiles: RenderedFile[] = []; diff --git a/src/command/render/render-shared.ts b/src/command/render/render-shared.ts index 8ae0f0c8e7..c4512f3a70 100644 --- a/src/command/render/render-shared.ts +++ b/src/command/render/render-shared.ts @@ -18,7 +18,10 @@ import { renderProject } from "./project.ts"; import { renderFiles } from "./render-files.ts"; import { resourceFilesFromRenderedFile } from "./resources.ts"; import { RenderFlags, RenderOptions, RenderResult } from "./types.ts"; -import { fileExecutionEngine } from "../../execute/engine.ts"; +import { + fileExecutionEngine, + fileExecutionEngineAndTarget, +} from "../../execute/engine.ts"; import { isProjectInputFile, @@ -33,6 +36,7 @@ import { initYamlIntelligenceResourcesFromFilesystem } from "../../core/schema/u import { kTextPlain } from "../../core/mime.ts"; import { normalizePath } from "../../core/path.ts"; import { notebookContext } from "../../render/notebook/notebook-context.ts"; +import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts"; export async function render( path: string, @@ -95,11 +99,24 @@ export async function render( // validate that we didn't get any project-only options validateDocumentRenderFlags(options.flags); + // NB: singleFileProjectContext is currently not fully-featured + context = singleFileProjectContext(path, nbContext, options.flags); + // otherwise it's just a file render - const result = await renderFiles([{ path }], options, nbContext); + const result = await renderFiles( + [{ path }], + options, + nbContext, + undefined, + undefined, + context, + ); // get partitioned markdown if we had result files - const engine = fileExecutionEngine(path); + const { engine } = await context.fileExecutionEngineAndTarget( + path, + undefined, + ); const partitioned = (engine && result.files.length > 0) ? await engine.partitionedMarkdown(path) : undefined; @@ -126,6 +143,7 @@ export async function render( }; })), error: result.error, + baseDir: normalizePath(dirname(path)), }; if (!renderResult.error && engine?.postRender) { diff --git a/src/command/render/types.ts b/src/command/render/types.ts index 05b2280ed1..cf097b4f4b 100644 --- a/src/command/render/types.ts +++ b/src/command/render/types.ts @@ -59,7 +59,7 @@ export interface RenderContext { engine: ExecutionEngine; format: Format; libDir: string; - project?: ProjectContext; + project: ProjectContext; active: boolean; } diff --git a/src/command/serve/cmd.ts b/src/command/serve/cmd.ts index 13b2d226fb..473c5abc79 100644 --- a/src/command/serve/cmd.ts +++ b/src/command/serve/cmd.ts @@ -18,6 +18,7 @@ import { previewFormat } from "../preview/preview.ts"; import { withRenderServices } from "../render/render-services.ts"; import { notebookContext } from "../../render/notebook/notebook-context.ts"; import { RenderServices } from "../render/types.ts"; +import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts"; export const serveCommand = new Command() .name("serve") @@ -65,7 +66,8 @@ export const serveCommand = new Command() const { host, port } = await resolveHostAndPort(options); const nbContext = notebookContext(); - const context = await projectContext(input, nbContext); + const context = (await projectContext(input, nbContext)) || + singleFileProjectContext(input, nbContext); const formats = await withRenderServices( nbContext, (services: RenderServices) => diff --git a/src/core/jupyter/jupyter-embed.ts b/src/core/jupyter/jupyter-embed.ts index 475dde1f6c..ae694da529 100644 --- a/src/core/jupyter/jupyter-embed.ts +++ b/src/core/jupyter/jupyter-embed.ts @@ -157,7 +157,7 @@ function unsupportedEmbed(path: string) { export async function ensureNotebookContext( markdown: string, services: RenderServices, - context?: ProjectContext, + context: ProjectContext, ) { const regex = placeholderRegex(); let match = regex.exec(markdown); @@ -353,9 +353,9 @@ export async function replaceNotebookPlaceholders( }; } -function resolveNbPath(input: string, path: string, context?: ProjectContext) { +function resolveNbPath(input: string, path: string, context: ProjectContext) { // If this is a project, absolute means project relative - if (context) { + if (!context.isSingleFile) { const projectMatch = path.match(/^[\\/](.*)/); if (projectMatch) { return join(context.dir, projectMatch[1]); @@ -368,8 +368,9 @@ function resolveNbPath(input: string, path: string, context?: ProjectContext) { if (isAbsolute(input)) { return join(dirname(input), path); } else { - const baseDir = context ? context.dir : Deno.cwd(); - return join(baseDir, dirname(input), path); + const baseDir = context.isSingleFile ? Deno.cwd() : context.dir; + const result = join(baseDir, dirname(input), path); + return result; } } } @@ -803,7 +804,7 @@ function resolveRange(rangeRaw?: string) { function jupyterFromNotebookOrQmd( nbAbsPath: string, services: RenderServices, - project?: ProjectContext, + project: ProjectContext, ) { // See if we can resolve non-notebooks. Note that this // requires that we have pre-rendered any notebooks that we discover diff --git a/src/execute/engine.ts b/src/execute/engine.ts index 338b7fe2c0..8075cc3381 100644 --- a/src/execute/engine.ts +++ b/src/execute/engine.ts @@ -4,7 +4,7 @@ * Copyright (C) 2020-2022 Posit Software, PBC */ -import { extname, join } from "path/mod.ts"; +import { extname, join, resolve } from "path/mod.ts"; import * as ld from "../core/lodash.ts"; @@ -30,23 +30,25 @@ import { ProjectContext } from "../project/types.ts"; import { pandocBuiltInFormats } from "../core/pandoc/pandoc-formats.ts"; import { gitignoreEntries } from "../project/project-gitignore.ts"; -const kEngines: ExecutionEngine[] = [ - knitrEngine, - jupyterEngine, - markdownEngine, -]; +const kEngines: Map = new Map(); -export function executionEngines() { - return kEngines.map((engine) => engine.name); +export function executionEngines(): ExecutionEngine[] { + return [...kEngines.values()]; } export function executionEngine(name: string) { - // try to find an engine - for (const engine of kEngines) { - if (engine.name === name) { - return engine; - } + return kEngines.get(name); +} + +for (const engine of [knitrEngine, jupyterEngine, markdownEngine]) { + registerExecutionEngine(engine); +} + +export function registerExecutionEngine(engine: ExecutionEngine) { + if (kEngines.has(engine.name)) { + throw new Error(`Execution engine ${engine.name} already registered`); } + kEngines.set(engine.name, engine); } export function executionEngineKeepMd(context: RenderContext) { @@ -85,7 +87,9 @@ export function executionEngineIntermediateFiles( } export function engineValidExtensions(): string[] { - return ld.uniq(kEngines.flatMap((engine) => engine.validExtensions())); + return ld.uniq( + executionEngines().flatMap((engine) => engine.validExtensions()), + ); } export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) { @@ -98,7 +102,7 @@ export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) { if (yaml) { // merge in command line fags yaml = mergeConfigs(yaml, flags?.metadata); - for (const engine of kEngines) { + for (const [_, engine] of kEngines) { if (yaml[engine.name]) { return engine; } @@ -115,7 +119,7 @@ export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) { // see if there is an engine that claims this language for (const language of languages) { - for (const engine of kEngines) { + for (const [_, engine] of kEngines) { if (engine.claimsLanguage(language)) { return engine; } @@ -146,12 +150,14 @@ export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) { export function fileEngineClaimReason(file: string) { // get the extension and validate that it can be handled by at least one of our engines const ext = extname(file).toLowerCase(); - if (!kEngines.some((engine) => engine.validExtensions().includes(ext))) { + if ( + !executionEngines().some((engine) => engine.validExtensions().includes(ext)) + ) { return "invalid"; } // try to find an engine that claims this extension outright - for (const engine of kEngines) { + for (const [_, engine] of kEngines) { if (engine.claimsFile(file, ext)) { return "extension"; } @@ -167,12 +173,16 @@ export function fileExecutionEngine( ) { // get the extension and validate that it can be handled by at least one of our engines const ext = extname(file).toLowerCase(); - if (!kEngines.some((engine) => engine.validExtensions().includes(ext))) { + if ( + !(executionEngines().some((engine) => + engine.validExtensions().includes(ext) + )) + ) { return undefined; } // try to find an engine that claims this extension outright - for (const engine of kEngines) { + for (const [_, engine] of kEngines) { if (engine.claimsFile(file, ext)) { return engine; } @@ -202,30 +212,37 @@ export function fileExecutionEngine( export async function fileExecutionEngineAndTarget( file: string, - flags?: RenderFlags, - markdown?: MappedString, - project?: ProjectContext, + flags: RenderFlags | undefined, + markdown: MappedString | undefined, + project: ProjectContext, + force?: boolean, ) { + const cached = force ? undefined : project.engineAndTargetCache?.get(file); + if (cached) { + return cached; + } + const engine = fileExecutionEngine(file, flags, markdown); if (!engine) { - throw new Error("Unable to render " + file); + throw new Error("Can't determine execution engine for " + file); } const target = await engine.target(file, flags?.quiet, markdown, project); if (!target) { - throw new Error("Unable to render " + file); + throw new Error("Can't determine execution target for " + file); } - return { - engine, - target, - }; + const result = { engine, target }; + if (!project.engineAndTargetCache) { + project.engineAndTargetCache = new Map(); + } + project.engineAndTargetCache.set(file, result); + return result; } export function engineIgnoreDirs() { const ignoreDirs: string[] = ["node_modules"]; - executionEngines().forEach((name) => { - const engine = executionEngine(name); + executionEngines().forEach((engine) => { if (engine && engine.ignoreDirs) { const ignores = engine.ignoreDirs(); if (ignores) { diff --git a/src/format/html/format-html-bootstrap.ts b/src/format/html/format-html-bootstrap.ts index 9e3aa1764b..61a6977aab 100644 --- a/src/format/html/format-html-bootstrap.ts +++ b/src/format/html/format-html-bootstrap.ts @@ -112,13 +112,13 @@ export function bootstrapFormatDependency() { }; } -export function boostrapExtras( +export function bootstrapExtras( input: string, flags: PandocFlags, format: Format, services: RenderServices, - offset?: string, - project?: ProjectContext, + offset: string | undefined, + project: ProjectContext, quiet?: boolean, ): FormatExtras { const toc = hasTableOfContents(flags, format); @@ -277,8 +277,8 @@ function bootstrapHtmlPostprocessor( format: Format, flags: PandocFlags, services: RenderServices, - offset?: string, - project?: ProjectContext, + offset: string | undefined, + project: ProjectContext, quiet?: boolean, ): HtmlPostProcessor { return async ( diff --git a/src/format/html/format-html-notebook-preview.ts b/src/format/html/format-html-notebook-preview.ts index 6da67fe012..0928e7347b 100644 --- a/src/format/html/format-html-notebook-preview.ts +++ b/src/format/html/format-html-notebook-preview.ts @@ -48,7 +48,7 @@ export const notebookPreviewer = ( nbView: boolean | NotebookPreviewDescriptor | NotebookPreviewDescriptor[], format: Format, services: RenderServices, - project?: ProjectContext, + project: ProjectContext, ) => { const isBook = projectIsBook(project); const previewQueue: NotebookPreviewTask[] = []; diff --git a/src/format/html/format-html-notebook.ts b/src/format/html/format-html-notebook.ts index 613b4c8b0c..ad6be3af6b 100644 --- a/src/format/html/format-html-notebook.ts +++ b/src/format/html/format-html-notebook.ts @@ -106,7 +106,7 @@ export async function emplaceNotebookPreviews( doc: Document, format: Format, services: RenderServices, - project?: ProjectContext, + project: ProjectContext, output?: string, quiet?: boolean, ) { diff --git a/src/format/html/format-html.ts b/src/format/html/format-html.ts index 3256801e90..e3a43a75d0 100644 --- a/src/format/html/format-html.ts +++ b/src/format/html/format-html.ts @@ -57,7 +57,7 @@ import { formatHasBootstrap, } from "./format-html-info.ts"; -import { boostrapExtras } from "./format-html-bootstrap.ts"; +import { bootstrapExtras } from "./format-html-bootstrap.ts"; import { clipboardDependency, @@ -991,8 +991,8 @@ function themeFormatExtras( flags: PandocFlags, format: Format, sevices: RenderServices, - offset?: string, - project?: ProjectContext, + offset: string | undefined, + project: ProjectContext, quiet?: boolean, ) { const theme = format.metadata[kTheme]; @@ -1005,7 +1005,7 @@ function themeFormatExtras( } else if (theme === "pandoc") { return pandocExtras(format); } else { - return boostrapExtras( + return bootstrapExtras( input, flags, format, diff --git a/src/format/jats/format-jats-postprocess.ts b/src/format/jats/format-jats-postprocess.ts index c5bb465838..ef4801af88 100644 --- a/src/format/jats/format-jats-postprocess.ts +++ b/src/format/jats/format-jats-postprocess.ts @@ -27,7 +27,7 @@ export const renderSubarticlePostProcessor = ( format: Format, subArticles: JatsRenderSubArticle[], services: RenderServices, - project?: ProjectContext, + project: ProjectContext, quiet?: boolean, ) => { return async (output: string) => { diff --git a/src/format/jats/format-jats.ts b/src/format/jats/format-jats.ts index e54bdc49a2..de26cc1451 100644 --- a/src/format/jats/format-jats.ts +++ b/src/format/jats/format-jats.ts @@ -56,8 +56,8 @@ export function jatsFormat(displayName: string, ext: string): Format { format: Format, _libDir: string, services: RenderServices, - _offset?: string, - project?: ProjectContext, + _offset: string | undefined, + project: ProjectContext, quiet?: boolean, ) => { // Provide a template and partials diff --git a/src/project/project-context.ts b/src/project/project-context.ts index 762b1b87ae..3bade988d8 100644 --- a/src/project/project-context.ts +++ b/src/project/project-context.ts @@ -55,6 +55,7 @@ import { engineIgnoreDirs, executionEngineIntermediateFiles, fileExecutionEngine, + fileExecutionEngineAndTarget, projectIgnoreGlobs, } from "../execute/engine.ts"; import { kMarkdownEngine } from "../execute/types.ts"; @@ -88,6 +89,7 @@ import { debug } from "log/mod.ts"; import { computeProjectEnvironment } from "./project-environment.ts"; import { ProjectEnvironment } from "./project-environment-types.ts"; import { NotebookContext } from "../render/notebook/notebook-types.ts"; +import { MappedString } from "../core/mapped-text.ts"; export async function projectContext( path: string, @@ -272,6 +274,20 @@ export async function projectContext( renderFormats, environment: () => environment(result), notebookContext, + fileExecutionEngineAndTarget: ( + file: string, + markdown?: MappedString, + force?: boolean, + ) => { + return fileExecutionEngineAndTarget( + file, + flags, + markdown, + result, + force, + ); + }, + isSingleFile: false, }; if (type.formatExtras) { result.formatExtras = async ( @@ -285,7 +301,7 @@ export async function projectContext( } else { const { files, engines } = projectInputFiles(dir); debug(`projectContext: Found Quarto project in ${dir}`); - const result = { + const result: ProjectContext = { dir, engines, config: projectConfig, @@ -297,7 +313,21 @@ export async function projectContext( }, renderFormats, environment: () => environment(result), + fileExecutionEngineAndTarget: ( + file: string, + markdown?: MappedString, + force?: boolean, + ) => { + return fileExecutionEngineAndTarget( + file, + flags, + markdown, + result, + force, + ); + }, notebookContext, + isSingleFile: false, }; return result; } @@ -323,6 +353,20 @@ export async function projectContext( renderFormats, environment: () => environment(context), notebookContext, + fileExecutionEngineAndTarget: ( + file: string, + markdown?: MappedString, + force?: boolean, + ) => { + return fileExecutionEngineAndTarget( + file, + flags, + markdown, + context, + force, + ); + }, + isSingleFile: false, }; if (Deno.statSync(path).isDirectory) { const { files, engines } = projectInputFiles(originalDir); diff --git a/src/project/project-environment.ts b/src/project/project-environment.ts index 19dec9db04..4b4b50b89f 100644 --- a/src/project/project-environment.ts +++ b/src/project/project-environment.ts @@ -23,6 +23,20 @@ import { NotebookContext } from "../render/notebook/notebook-types.ts"; const kDefaultContainerTitle = "Default Container"; +export const makeProjectEnvironmentMemoizer = ( + notebookContext: NotebookContext, +) => { + let cachedEnv: ProjectEnvironment | undefined = undefined; + return async (project: ProjectContext) => { + if (cachedEnv) { + return Promise.resolve(cachedEnv); + } else { + cachedEnv = await computeProjectEnvironment(notebookContext, project); + return cachedEnv; + } + }; +}; + export const computeProjectEnvironment = async ( notebookContext: NotebookContext, context: ProjectContext, diff --git a/src/project/types.ts b/src/project/types.ts index 22ff2af043..7217676601 100644 --- a/src/project/types.ts +++ b/src/project/types.ts @@ -4,10 +4,12 @@ * Copyright (C) 2020-2022 Posit Software, PBC */ -import { RenderServices } from "../command/render/types.ts"; +import { RenderFlags, RenderServices } from "../command/render/types.ts"; import { Metadata, PandocFlags } from "../config/types.ts"; import { Format, FormatExtras } from "../config/types.ts"; +import { MappedString } from "../core/mapped-text.ts"; import { PartitionedMarkdown } from "../core/pandoc/types.ts"; +import { ExecutionEngine, ExecutionTarget } from "../execute/types.ts"; import { NotebookContext } from "../render/notebook/notebook-types.ts"; import { NavigationItem as NavItem, @@ -44,6 +46,18 @@ export interface ProjectContext { notebookContext: NotebookContext; outputNameIndex?: Map; + // This is a cache of the engine and target for a given filename + engineAndTargetCache?: Map< + string, + { engine: ExecutionEngine; target: ExecutionTarget } + >; + + fileExecutionEngineAndTarget: ( + file: string, + markdown?: MappedString, + force?: boolean, + ) => Promise<{ engine: ExecutionEngine; target: ExecutionTarget }>; + formatExtras?: ( source: string, flags: PandocFlags, @@ -51,14 +65,18 @@ export interface ProjectContext { services: RenderServices, ) => Promise; + // declaring renderFormats here is a relatively ugly hack to avoid a circular import chain + // that causes a deno bundler bug renderFormats: ( file: string, services: RenderServices, - to?: string, - project?: ProjectContext, + to: string | undefined, + project: ProjectContext, ) => Promise>; environment: () => Promise; + + isSingleFile: boolean; } export interface ProjectFiles { diff --git a/src/project/types/single-file/single-file.ts b/src/project/types/single-file/single-file.ts new file mode 100644 index 0000000000..9a96b477cd --- /dev/null +++ b/src/project/types/single-file/single-file.ts @@ -0,0 +1,55 @@ +/* + * single-file.ts + * + * Copyright (C) 2024 Posit Software, PBC + */ + +// In the future, we will have a single-file project type +// that obeys exactly the same interface as a multi-file project. +// +// Currently, this file houses utilities to make the +// single-file path look closer to a project. + +import { dirname } from "path/dirname.ts"; +import { normalizePath } from "../../../core/path.ts"; +import { NotebookContext } from "../../../render/notebook/notebook-types.ts"; +import { makeProjectEnvironmentMemoizer } from "../../project-environment.ts"; +import { ProjectContext } from "../../types.ts"; +import { renderFormats } from "../../../command/render/render-contexts.ts"; +import { RenderFlags } from "../../../command/render/types.ts"; +import { MappedString } from "../../../core/mapped-text.ts"; +import { fileExecutionEngineAndTarget } from "../../../execute/engine.ts"; + +export function singleFileProjectContext( + source: string, + notebookContext: NotebookContext, + flags?: RenderFlags, +): ProjectContext { + const environmentMemoizer = makeProjectEnvironmentMemoizer(notebookContext); + + const result: ProjectContext = { + dir: normalizePath(dirname(source)), + engines: [], + files: { + input: [], + }, + notebookContext, + environment: () => environmentMemoizer(result), + renderFormats, + fileExecutionEngineAndTarget: ( + file: string, + markdown?: MappedString, + force?: boolean, + ) => { + return fileExecutionEngineAndTarget( + file, + flags, + markdown, + result, + force, + ); + }, + isSingleFile: true, + }; + return result; +} diff --git a/src/quarto-core/inspect.ts b/src/quarto-core/inspect.ts index 688d14a511..4303e30b20 100644 --- a/src/quarto-core/inspect.ts +++ b/src/quarto-core/inspect.ts @@ -31,6 +31,7 @@ import { extensionFilesFromDirs } from "../extension/extension.ts"; import { withRenderServices } from "../command/render/render-services.ts"; import { notebookContext } from "../render/notebook/notebook-context.ts"; import { RenderServices } from "../command/render/types.ts"; +import { singleFileProjectContext } from "../project/types/single-file/single-file.ts"; export interface InspectedConfig { quarto: { @@ -112,7 +113,8 @@ export async function inspectConfig(path?: string): Promise { const partitioned = await engine.partitionedMarkdown(path); // get formats - const context = await projectContext(path, nbContext); + const context = (await projectContext(path, nbContext)) || + singleFileProjectContext(path, nbContext); const formats = await withRenderServices( nbContext, (services: RenderServices) => diff --git a/src/render/notebook/notebook-context.ts b/src/render/notebook/notebook-context.ts index 0d335d91f1..97192094df 100644 --- a/src/render/notebook/notebook-context.ts +++ b/src/render/notebook/notebook-context.ts @@ -57,7 +57,7 @@ export function notebookContext(): NotebookContext { nbAbsPath: string, renderType: RenderType, result: NotebookRenderResult, - context?: ProjectContext, + context: ProjectContext, cached?: boolean, ) => { debug(`[NotebookContext]: Add Rendering (${renderType}):${nbAbsPath}`); @@ -146,7 +146,7 @@ export function notebookContext(): NotebookContext { function reviveOutput( nbAbsPath: string, renderType: RenderType, - context?: ProjectContext, + context: ProjectContext, ) { debug( `[NotebookContext]: Attempting to Revive Rendering (${renderType}):${nbAbsPath}`, @@ -174,7 +174,7 @@ export function notebookContext(): NotebookContext { files: [], }, }, - undefined, + context, true, ); } @@ -257,8 +257,8 @@ export function notebookContext(): NotebookContext { format: Format, renderType: RenderType, services: RenderServices, - notebookMetadata?: NotebookMetadata, - project?: ProjectContext, + notebookMetadata: NotebookMetadata | undefined, + project: ProjectContext, ) => { debug(`[NotebookContext]: Rendering (${renderType}):${nbAbsPath}`); diff --git a/src/render/notebook/notebook-contributor-html.ts b/src/render/notebook/notebook-contributor-html.ts index daeb5e2128..05a6ae89cd 100644 --- a/src/render/notebook/notebook-contributor-html.ts +++ b/src/render/notebook/notebook-contributor-html.ts @@ -144,8 +144,8 @@ async function renderHtmlNotebook( format: Format, _subArticleToken: string, services: RenderServices, - notebookMetadata?: NotebookMetadata, - project?: ProjectContext, + notebookMetadata: NotebookMetadata | undefined, + project: ProjectContext, ): Promise { // Use the special `embed` template for this render const template = formatResourcePath( diff --git a/src/render/notebook/notebook-contributor-ipynb.ts b/src/render/notebook/notebook-contributor-ipynb.ts index 04f7eb84f4..b4114adb88 100644 --- a/src/render/notebook/notebook-contributor-ipynb.ts +++ b/src/render/notebook/notebook-contributor-ipynb.ts @@ -99,8 +99,8 @@ async function renderOutputNotebook( _format: Format, _subArticleToken: string, services: RenderServices, - _notebookMetadata?: NotebookMetadata, - project?: ProjectContext, + _notebookMetadata: NotebookMetadata | undefined, + project: ProjectContext, ): Promise { const rendered = await renderFile( { path: nbPath, formats: ["ipynb"] }, diff --git a/src/render/notebook/notebook-contributor-jats.ts b/src/render/notebook/notebook-contributor-jats.ts index 6b6f13b646..f26e2c1f58 100644 --- a/src/render/notebook/notebook-contributor-jats.ts +++ b/src/render/notebook/notebook-contributor-jats.ts @@ -92,8 +92,8 @@ async function renderJats( format: Format, subArticleToken: string, services: RenderServices, - _notebookMetadata?: NotebookMetadata, - project?: ProjectContext, + _notebookMetadata: NotebookMetadata | undefined, + project: ProjectContext, ): Promise { const to = format.render[kVariant]?.includes("+element_citations") ? "jats+element_citations" diff --git a/src/render/notebook/notebook-contributor-qmd.ts b/src/render/notebook/notebook-contributor-qmd.ts index 3be4a1b5fe..f860a24975 100644 --- a/src/render/notebook/notebook-contributor-qmd.ts +++ b/src/render/notebook/notebook-contributor-qmd.ts @@ -115,8 +115,8 @@ async function renderOutputNotebook( _format: Format, _subArticleToken: string, services: RenderServices, - _notebookMetadata?: NotebookMetadata, - project?: ProjectContext, + _notebookMetadata: NotebookMetadata | undefined, + project: ProjectContext, ): Promise { const rendered = await renderFile( { path: nbPath, formats: ["ipynb"] }, diff --git a/src/render/notebook/notebook-types.ts b/src/render/notebook/notebook-types.ts index 633565f131..2899bf82e5 100644 --- a/src/render/notebook/notebook-types.ts +++ b/src/render/notebook/notebook-types.ts @@ -80,7 +80,7 @@ export interface NotebookTemplateMetadata extends NotebookMetadata { export interface NotebookContext { // Retrieves the notebook from the context. - get: (nbPath: string, context?: ProjectContext) => Notebook | undefined; + get: (nbPath: string, context: ProjectContext) => Notebook | undefined; all: () => Notebook[]; // Resolves the data on an executedFile into data that will // create a `renderType` output when rendered. @@ -96,7 +96,7 @@ export interface NotebookContext { nbPath: string, renderType: RenderType, result: NotebookRenderResult, - project?: ProjectContext, + project: ProjectContext, ) => void; removeRendering: ( nbAbsPath: string, @@ -109,8 +109,8 @@ export interface NotebookContext { format: Format, renderType: RenderType, renderServices: RenderServices, - notebookMetadata?: NotebookMetadata, - project?: ProjectContext, + notebookMetadata: NotebookMetadata | undefined, + project: ProjectContext, ) => Promise; // Previews are cleaned up when the notebook context is disposed, but // you can use this to mark specific notebook > rendertypes to not be cleaned up. @@ -132,12 +132,12 @@ export interface NotebookContributor { format: Format, token: string, services: RenderServices, - notebookMetadata?: NotebookMetadata, - project?: ProjectContext, + notebookMetadata: NotebookMetadata | undefined, + project: ProjectContext, ): Promise; - cache?: (output: NotebookOutput, project?: ProjectContext) => void; + cache?: (output: NotebookOutput, project: ProjectContext) => void; cachedPath?: ( nbAbsPath: string, - project?: ProjectContext, + project: ProjectContext, ) => string | undefined; }