From a960649dae28e6bcdf95ae1dd6cf8e149eb406a1 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Fri, 10 Jul 2026 19:54:21 -0500 Subject: [PATCH 1/3] Fix keep-md cleanup deleting another format's markdown output (#14669) When output-file has an .html extension and an html format is paired with a markdown format (e.g. output-file: index.html with html + commonmark, as nbdev sets up for llms.txt workflows), the markdown output is named index.html.md -- the same path the keep-md intermediate convention (..md) assigns to the html format. The html render's cleanup removed the "stale intermediate", deleting the freshly written markdown output: website renders then failed at the output-move step, and other project types silently lost the file. Compute the projected output paths of the formats being rendered and attach them to each RenderContext; render cleanup now never deletes a path that a format owns, and a keep-md: true render whose intermediate location collides warns and skips saving the intermediate instead of overwriting the output. Co-Authored-By: Claude Fable 5 --- news/changelog-1.10.md | 1 + src/command/render/render-files.ts | 35 +++++- src/command/render/render.ts | 12 +- src/command/render/types.ts | 3 + src/core/render.ts | 17 ++- .../output-file-collision/default/.gitignore | 7 ++ .../output-file-collision/default/_quarto.yml | 6 + .../output-file-collision/default/index.qmd | 6 + .../output-file-collision/keepmd/.gitignore | 6 + .../output-file-collision/keepmd/_quarto.yml | 9 ++ .../output-file-collision/keepmd/index.qmd | 6 + .../output-file-collision/website/.gitignore | 6 + .../output-file-collision/website/_quarto.yml | 6 + .../output-file-collision/website/index.qmd | 6 + .../render-output-file-collision.test.ts | 114 ++++++++++++++++++ 15 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 tests/docs/output-file-collision/default/.gitignore create mode 100644 tests/docs/output-file-collision/default/_quarto.yml create mode 100644 tests/docs/output-file-collision/default/index.qmd create mode 100644 tests/docs/output-file-collision/keepmd/.gitignore create mode 100644 tests/docs/output-file-collision/keepmd/_quarto.yml create mode 100644 tests/docs/output-file-collision/keepmd/index.qmd create mode 100644 tests/docs/output-file-collision/website/.gitignore create mode 100644 tests/docs/output-file-collision/website/_quarto.yml create mode 100644 tests/docs/output-file-collision/website/index.qmd create mode 100644 tests/smoke/render/render-output-file-collision.test.ts diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index e10a2250d0a..efbd8a5346c 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -114,3 +114,4 @@ All changes included in 1.10: - ([#14582](https://github.com/quarto-dev/quarto-cli/issues/14582)): Fix format detection for extension formats (e.g. `acm-pdf`) in project preview, manuscript notebooks, MECA bundles, and website format ordering. - ([#14583](https://github.com/quarto-dev/quarto-cli/issues/14583)): Fix a shortcode used as an image source (e.g. `![]({{< meta logo >}})`) getting the `default-image-extension` appended, producing a doubled extension once the shortcode resolves. - ([#14595](https://github.com/quarto-dev/quarto-cli/issues/14595)): Fix reload preview in code-server environment +- ([#14669](https://github.com/quarto-dev/quarto-cli/issues/14669)): Fix the markdown output being deleted when `output-file` has an `.html` extension and an html format is paired with a markdown format (e.g. `output-file: index.html` with `format: html` + `commonmark`, as nbdev sets up for llms.txt workflows). The markdown output's name (`index.html.md`) coincides with the conventional `keep-md` intermediate location for the html format, and render cleanup deleted it — failing website renders at the output-move step and silently dropping the file in other project types. Renders now never clean up (or write a `keep-md` intermediate over) a path that belongs to another format's output; a `keep-md: true` render whose intermediate location collides warns and skips saving the intermediate. diff --git a/src/command/render/render-files.ts b/src/command/render/render-files.ts index 171cffec591..7423eec6253 100644 --- a/src/command/render/render-files.ts +++ b/src/command/render/render-files.ts @@ -62,7 +62,7 @@ import { RenderFlags, RenderOptions, } from "./types.ts"; -import { error, info } from "../../deno_ral/log.ts"; +import { error, info, warning } from "../../deno_ral/log.ts"; import * as ld from "../../core/lodash.ts"; import { basename, dirname, join, relative } from "../../deno_ral/path.ts"; import { Format } from "../../config/types.ts"; @@ -71,9 +71,11 @@ import { inputFilesDir, isServerShiny, isServerShinyKnitr, + projectedOutputFile, } from "../../core/render.ts"; import { normalizePath, + pathsEqual, removeIfEmptyDir, removeIfExists, } from "../../core/path.ts"; @@ -460,6 +462,20 @@ async function renderFileInternal( files, options, ); + + // let each context know the projected outputs of the formats being + // rendered: keep-md intermediate handling must not write to or delete + // a path that a format owns (e.g. output-file: index.html plus a + // markdown format yields index.html.md, which is also the conventional + // keep-md location for the html format) (#14669) + const formatOutputs = Object.values(contexts) + .filter((context) => context.active) + .map((context) => + projectedOutputFile(context.target.input, context.format) + ); + for (const context of Object.values(contexts)) { + context.siblingFormatOutputs = formatOutputs; + } } catch (e) { // bad YAML can cause failure before validation. We // reconstruct the context as best we can and try to validate. @@ -670,7 +686,22 @@ async function renderFileInternal( // keep md if requested const keepMd = executionEngineKeepMd(context); if (keepMd && context.format.execute[kKeepMd]) { - Deno.writeTextFileSync(keepMd, executeResult.markdown.value); + if ( + context.siblingFormatOutputs?.some((output) => + pathsEqual(output, keepMd) + ) + ) { + warning( + `${ + basename(context.target.input) + }: not saving the keep-md intermediate because its ` + + `conventional location (${ + basename(keepMd) + }) is the output file of another format`, + ); + } else { + Deno.writeTextFileSync(keepMd, executeResult.markdown.value); + } } // now get "unmapped" execute result back to send to pandoc diff --git a/src/command/render/render.ts b/src/command/render/render.ts index 3da6c73f04d..826316c608a 100644 --- a/src/command/render/render.ts +++ b/src/command/render/render.ts @@ -15,6 +15,7 @@ import { resourcePath } from "../../core/resources.ts"; import { inputFilesDir } from "../../core/render.ts"; import { normalizePath, + pathsEqual, pathWithForwardSlashes, safeExistsSync, } from "../../core/path.ts"; @@ -374,6 +375,15 @@ export async function renderPandoc( } if (cleanup !== false) { + // the conventional keep-md location can coincide with the declared + // output of another format (e.g. output-file: index.html plus a + // markdown format yields index.html.md) -- never clean up a path + // that a format owns (#14669) + const keepMd = executionEngineKeepMd(context); + const keepMdIsFormatOutput = keepMd !== undefined && + context.siblingFormatOutputs?.some((output) => + pathsEqual(output, keepMd) + ); withTiming("render-cleanup", () => renderCleanup( context.target.input, @@ -381,7 +391,7 @@ export async function renderPandoc( format, file.context.project, cleanupSelfContained, - executionEngineKeepMd(context), + keepMdIsFormatOutput ? undefined : keepMd, )); } diff --git a/src/command/render/types.ts b/src/command/render/types.ts index cdc576ea53c..2826cdf9f6e 100644 --- a/src/command/render/types.ts +++ b/src/command/render/types.ts @@ -61,6 +61,9 @@ export interface RenderContext { libDir: string; project: ProjectContext; active: boolean; + // projected output paths of the input's other formats; keep-md intermediate + // handling must not write to or delete paths a format owns (#14669) + siblingFormatOutputs?: string[]; } export interface RunPandocResult { diff --git a/src/core/render.ts b/src/core/render.ts index 313445eaed3..7c652f6e158 100644 --- a/src/core/render.ts +++ b/src/core/render.ts @@ -8,7 +8,7 @@ import { kOutputExt, kOutputFile, kServer } from "../config/constants.ts"; import { Format, Metadata } from "../config/types.ts"; import { kJupyterEngine, kKnitrEngine } from "../execute/types.ts"; import { dirAndStem } from "./path.ts"; -import { extname } from "../deno_ral/path.ts"; +import { dirname, extname, join } from "../deno_ral/path.ts"; export function inputFilesDir(input: string) { const [_, stem] = dirAndStem(input); @@ -44,6 +44,21 @@ export function isServerShinyKnitr( return isServerShiny(format) && engine === kKnitrEngine; } +// the path a format's rendered output is written to before any project +// output-dir relocation: an explicit output-file (with the format's extension +// appended by formatOutputFile when it differs), else .. +// used to detect collisions between declared outputs and the keep-md +// intermediate naming convention (#14669); doesn't cover recipe special cases +// (--output flag, md-to-md rename, stdout) which can't produce colliding names +export function projectedOutputFile(input: string, format: Format): string { + const outputFile = formatOutputFile(format); + if (outputFile) { + return join(dirname(input), outputFile); + } + const [dir, stem] = dirAndStem(input); + return join(dir, `${stem}.${format.render[kOutputExt] || "html"}`); +} + export function formatOutputFile(format: Format) { let outputFile = format.pandoc[kOutputFile]; if (outputFile) { diff --git a/tests/docs/output-file-collision/default/.gitignore b/tests/docs/output-file-collision/default/.gitignore new file mode 100644 index 00000000000..f9ce2b2db87 --- /dev/null +++ b/tests/docs/output-file-collision/default/.gitignore @@ -0,0 +1,7 @@ +/.quarto/ +/index.html +/index.html.md +/index.commonmark.md +/index_files/ +/site_libs/ +**/*.quarto_ipynb diff --git a/tests/docs/output-file-collision/default/_quarto.yml b/tests/docs/output-file-collision/default/_quarto.yml new file mode 100644 index 00000000000..a98fe5d28de --- /dev/null +++ b/tests/docs/output-file-collision/default/_quarto.yml @@ -0,0 +1,6 @@ +project: + type: default + +format: + html: default + commonmark: default diff --git a/tests/docs/output-file-collision/default/index.qmd b/tests/docs/output-file-collision/default/index.qmd new file mode 100644 index 00000000000..5cd735cefb7 --- /dev/null +++ b/tests/docs/output-file-collision/default/index.qmd @@ -0,0 +1,6 @@ +--- +title: Home +output-file: index.html +--- + +Hello. diff --git a/tests/docs/output-file-collision/keepmd/.gitignore b/tests/docs/output-file-collision/keepmd/.gitignore new file mode 100644 index 00000000000..b87288d2f4f --- /dev/null +++ b/tests/docs/output-file-collision/keepmd/.gitignore @@ -0,0 +1,6 @@ +/.quarto/ +/_site/ +/site_libs/ +/index.html +/index.commonmark.md +**/*.quarto_ipynb diff --git a/tests/docs/output-file-collision/keepmd/_quarto.yml b/tests/docs/output-file-collision/keepmd/_quarto.yml new file mode 100644 index 00000000000..3a5d272c7c2 --- /dev/null +++ b/tests/docs/output-file-collision/keepmd/_quarto.yml @@ -0,0 +1,9 @@ +project: + type: website + +format: + html: default + commonmark: default + +execute: + keep-md: true diff --git a/tests/docs/output-file-collision/keepmd/index.qmd b/tests/docs/output-file-collision/keepmd/index.qmd new file mode 100644 index 00000000000..5d906ee987f --- /dev/null +++ b/tests/docs/output-file-collision/keepmd/index.qmd @@ -0,0 +1,6 @@ +--- +title: Home +output-file: index.html +--- + +Keep md content. diff --git a/tests/docs/output-file-collision/website/.gitignore b/tests/docs/output-file-collision/website/.gitignore new file mode 100644 index 00000000000..b87288d2f4f --- /dev/null +++ b/tests/docs/output-file-collision/website/.gitignore @@ -0,0 +1,6 @@ +/.quarto/ +/_site/ +/site_libs/ +/index.html +/index.commonmark.md +**/*.quarto_ipynb diff --git a/tests/docs/output-file-collision/website/_quarto.yml b/tests/docs/output-file-collision/website/_quarto.yml new file mode 100644 index 00000000000..3c67c72b26d --- /dev/null +++ b/tests/docs/output-file-collision/website/_quarto.yml @@ -0,0 +1,6 @@ +project: + type: website + +format: + html: default + commonmark: default diff --git a/tests/docs/output-file-collision/website/index.qmd b/tests/docs/output-file-collision/website/index.qmd new file mode 100644 index 00000000000..5cd735cefb7 --- /dev/null +++ b/tests/docs/output-file-collision/website/index.qmd @@ -0,0 +1,6 @@ +--- +title: Home +output-file: index.html +--- + +Hello. diff --git a/tests/smoke/render/render-output-file-collision.test.ts b/tests/smoke/render/render-output-file-collision.test.ts new file mode 100644 index 00000000000..9e7826733a0 --- /dev/null +++ b/tests/smoke/render/render-output-file-collision.test.ts @@ -0,0 +1,114 @@ +/* + * render-output-file-collision.test.ts + * + * Regression tests for https://github.com/quarto-dev/quarto-cli/issues/14669: + * pairing an html format with a markdown format while output-file has an + * .html extension (e.g. output-file: index.html, as nbdev sets for llms.txt + * workflows) names the markdown output index.html.md — the same path as the + * keep-md intermediate convention for the html format. Render cleanup was + * deleting the markdown output, failing website renders at the output-move + * step and silently losing the file in default-type projects. + * + * Copyright (C) 2020-2026 Posit Software, PBC + * + */ +import { existsSync, safeRemoveSync } from "../../../src/deno_ral/fs.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { docs } from "../../utils.ts"; +import { testQuartoCmd } from "../../test.ts"; +import { + ensureFileRegexMatches, + fileExists, + pathDoNotExists, + printsMessage, +} from "../../verify.ts"; + +const cleanup = (dir: string, paths: string[]) => { + return () => { + for (const path of paths) { + const target = join(dir, path); + if (existsSync(target)) { + safeRemoveSync(target, { recursive: true }); + } + } + return Promise.resolve(); + }; +}; + +// paths a failed (pre-fix) render strands in the project root; clean them +// too so a regression doesn't leave debris behind +const websiteOutputs = [ + "_site", + ".quarto", + "index.html", + "site_libs", + "index.commonmark.md", +]; + +// website project: the render errored at the output-move step and left +// index.html + site_libs stranded in the project root +const websiteDir = docs("output-file-collision/website"); +testQuartoCmd( + "render", + [websiteDir], + [ + fileExists(join(websiteDir, "_site", "index.html")), + fileExists(join(websiteDir, "_site", "index.html.md")), + pathDoNotExists(join(websiteDir, "index.html")), + pathDoNotExists(join(websiteDir, "site_libs")), + ], + { + setup: cleanup(websiteDir, websiteOutputs), + teardown: cleanup(websiteDir, websiteOutputs), + }, + "#14669 website: output-file index.html + commonmark renders both outputs", +); + +// default-type project: the render exited 0 but the markdown output was +// silently missing +const defaultDir = docs("output-file-collision/default"); +const defaultOutputs = [ + "index.html", + "index.html.md", + "index_files", + "site_libs", + "index.commonmark.md", +]; +testQuartoCmd( + "render", + [defaultDir], + [ + fileExists(join(defaultDir, "index.html")), + fileExists(join(defaultDir, "index.html.md")), + ], + { + setup: cleanup(defaultDir, [...defaultOutputs, ".quarto"]), + teardown: cleanup(defaultDir, [...defaultOutputs, ".quarto"]), + }, + "#14669 default project: markdown output is not silently dropped", +); + +// keep-md: true wants to write its intermediate to the same path the +// markdown format owns: the intermediate write is skipped with a warning and +// the markdown output wins +const keepmdDir = docs("output-file-collision/keepmd"); +testQuartoCmd( + "render", + [keepmdDir], + [ + printsMessage({ level: "WARN", regex: /not saving the keep-md/ }), + fileExists(join(keepmdDir, "_site", "index.html.md")), + // the markdown twin holds the commonmark render of the document, not the + // executed-markdown intermediate (which carries a yaml header) + ensureFileRegexMatches(join(keepmdDir, "_site", "index.html.md"), [ + /Keep md content\./, + ], [ + /^---$/, + ]), + ], + { + setup: cleanup(keepmdDir, websiteOutputs), + teardown: cleanup(keepmdDir, websiteOutputs), + }, + "#14669 keep-md: intermediate write is skipped with a warning on collision", +); From 2ff3b6ff3c63c0cefd64caa36a4137db461a75ba Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Fri, 10 Jul 2026 19:56:54 -0500 Subject: [PATCH 2/3] Protect declared-but-unrendered format outputs from keep-md cleanup (#14669) A partial render (quarto render --to html) of a document whose other declared format owns the keep-md-conventional path (output-file: index.html + a markdown format -> index.html.md) was still deleting the markdown output a previous full render produced, since outputs live next to sources in default-type projects. renderContexts already resolves every declared format (inactive ones are simply marked active: false), so extending the protection is just including inactive contexts when collecting the owned output paths. Co-Authored-By: Claude Fable 5 --- src/command/render/render-files.ts | 12 +++++----- .../render-output-file-collision.test.ts | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/command/render/render-files.ts b/src/command/render/render-files.ts index 7423eec6253..213b3d85082 100644 --- a/src/command/render/render-files.ts +++ b/src/command/render/render-files.ts @@ -463,13 +463,13 @@ async function renderFileInternal( options, ); - // let each context know the projected outputs of the formats being - // rendered: keep-md intermediate handling must not write to or delete - // a path that a format owns (e.g. output-file: index.html plus a - // markdown format yields index.html.md, which is also the conventional - // keep-md location for the html format) (#14669) + // let each context know the projected outputs of every declared format, + // including formats not in this render (e.g. quarto render --to html): + // keep-md intermediate handling must not write to or delete a path that + // a format owns (e.g. output-file: index.html plus a markdown format + // yields index.html.md, which is also the conventional keep-md location + // for the html format) (#14669) const formatOutputs = Object.values(contexts) - .filter((context) => context.active) .map((context) => projectedOutputFile(context.target.input, context.format) ); diff --git a/tests/smoke/render/render-output-file-collision.test.ts b/tests/smoke/render/render-output-file-collision.test.ts index 9e7826733a0..f360ed0d54e 100644 --- a/tests/smoke/render/render-output-file-collision.test.ts +++ b/tests/smoke/render/render-output-file-collision.test.ts @@ -14,6 +14,7 @@ */ import { existsSync, safeRemoveSync } from "../../../src/deno_ral/fs.ts"; import { join } from "../../../src/deno_ral/path.ts"; +import { quarto } from "../../../src/quarto.ts"; import { docs } from "../../utils.ts"; import { testQuartoCmd } from "../../test.ts"; import { @@ -88,6 +89,27 @@ testQuartoCmd( "#14669 default project: markdown output is not silently dropped", ); +// partial render: a format that is declared but not being rendered still +// owns its output -- quarto render --to html must not delete the markdown +// twin a previous full render produced (outputs live in place in +// default-type projects) +testQuartoCmd( + "render", + [join(defaultDir, "index.qmd"), "--to", "html"], + [ + fileExists(join(defaultDir, "index.html")), + fileExists(join(defaultDir, "index.html.md")), + ], + { + setup: async () => { + await cleanup(defaultDir, [...defaultOutputs, ".quarto"])(); + await quarto(["render", defaultDir]); + }, + teardown: cleanup(defaultDir, [...defaultOutputs, ".quarto"]), + }, + "#14669 partial render: --to html preserves the existing markdown output", +); + // keep-md: true wants to write its intermediate to the same path the // markdown format owns: the intermediate write is skipped with a warning and // the markdown output wins From 7c288146b63380679e04d2b950ae518b0bd7e404 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 13 Jul 2026 17:12:58 +0200 Subject: [PATCH 3/3] Extract keep-md/format-output collision check into a helper The collision guard was inlined in both renderCleanup and the keep-md write path; keepMdCollidesWithFormatOutput keeps the invariant (never write to or clean up a path another format owns) in one place. Trim the changelog entry to the user-facing symptom -- the keep-md/output-move mechanism and the nbdev/commonmark example already live in the PR body. --- news/changelog-1.10.md | 2 +- src/command/render/render-files.ts | 7 ++++--- src/command/render/render.ts | 14 ++++++++------ src/core/render.ts | 13 ++++++++++++- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index efbd8a5346c..5f4abd24f67 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -114,4 +114,4 @@ All changes included in 1.10: - ([#14582](https://github.com/quarto-dev/quarto-cli/issues/14582)): Fix format detection for extension formats (e.g. `acm-pdf`) in project preview, manuscript notebooks, MECA bundles, and website format ordering. - ([#14583](https://github.com/quarto-dev/quarto-cli/issues/14583)): Fix a shortcode used as an image source (e.g. `![]({{< meta logo >}})`) getting the `default-image-extension` appended, producing a doubled extension once the shortcode resolves. - ([#14595](https://github.com/quarto-dev/quarto-cli/issues/14595)): Fix reload preview in code-server environment -- ([#14669](https://github.com/quarto-dev/quarto-cli/issues/14669)): Fix the markdown output being deleted when `output-file` has an `.html` extension and an html format is paired with a markdown format (e.g. `output-file: index.html` with `format: html` + `commonmark`, as nbdev sets up for llms.txt workflows). The markdown output's name (`index.html.md`) coincides with the conventional `keep-md` intermediate location for the html format, and render cleanup deleted it — failing website renders at the output-move step and silently dropping the file in other project types. Renders now never clean up (or write a `keep-md` intermediate over) a path that belongs to another format's output; a `keep-md: true` render whose intermediate location collides warns and skips saving the intermediate. +- ([#14669](https://github.com/quarto-dev/quarto-cli/issues/14669)): Fix markdown output being deleted when `output-file` has an `.html` extension and an html format is paired with a markdown format. diff --git a/src/command/render/render-files.ts b/src/command/render/render-files.ts index 213b3d85082..2c578c33c0f 100644 --- a/src/command/render/render-files.ts +++ b/src/command/render/render-files.ts @@ -71,11 +71,11 @@ import { inputFilesDir, isServerShiny, isServerShinyKnitr, + keepMdCollidesWithFormatOutput, projectedOutputFile, } from "../../core/render.ts"; import { normalizePath, - pathsEqual, removeIfEmptyDir, removeIfExists, } from "../../core/path.ts"; @@ -687,8 +687,9 @@ async function renderFileInternal( const keepMd = executionEngineKeepMd(context); if (keepMd && context.format.execute[kKeepMd]) { if ( - context.siblingFormatOutputs?.some((output) => - pathsEqual(output, keepMd) + keepMdCollidesWithFormatOutput( + keepMd, + context.siblingFormatOutputs, ) ) { warning( diff --git a/src/command/render/render.ts b/src/command/render/render.ts index 826316c608a..a7a8510b305 100644 --- a/src/command/render/render.ts +++ b/src/command/render/render.ts @@ -12,10 +12,12 @@ 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 { + inputFilesDir, + keepMdCollidesWithFormatOutput, +} from "../../core/render.ts"; import { normalizePath, - pathsEqual, pathWithForwardSlashes, safeExistsSync, } from "../../core/path.ts"; @@ -380,10 +382,10 @@ export async function renderPandoc( // markdown format yields index.html.md) -- never clean up a path // that a format owns (#14669) const keepMd = executionEngineKeepMd(context); - const keepMdIsFormatOutput = keepMd !== undefined && - context.siblingFormatOutputs?.some((output) => - pathsEqual(output, keepMd) - ); + const keepMdIsFormatOutput = keepMdCollidesWithFormatOutput( + keepMd, + context.siblingFormatOutputs, + ); withTiming("render-cleanup", () => renderCleanup( context.target.input, diff --git a/src/core/render.ts b/src/core/render.ts index 7c652f6e158..3f41947a5a8 100644 --- a/src/core/render.ts +++ b/src/core/render.ts @@ -7,7 +7,7 @@ import { kOutputExt, kOutputFile, kServer } from "../config/constants.ts"; import { Format, Metadata } from "../config/types.ts"; import { kJupyterEngine, kKnitrEngine } from "../execute/types.ts"; -import { dirAndStem } from "./path.ts"; +import { dirAndStem, pathsEqual } from "./path.ts"; import { dirname, extname, join } from "../deno_ral/path.ts"; export function inputFilesDir(input: string) { @@ -59,6 +59,17 @@ export function projectedOutputFile(input: string, format: Format): string { return join(dir, `${stem}.${format.render[kOutputExt] || "html"}`); } +// true when the keep-md intermediate location is a path another declared +// format owns, so it must not be written to or cleaned up (#14669) +export function keepMdCollidesWithFormatOutput( + keepMd: string | undefined, + siblingFormatOutputs: string[] | undefined, +): boolean { + return keepMd !== undefined && + (siblingFormatOutputs?.some((output) => pathsEqual(output, keepMd)) ?? + false); +} + export function formatOutputFile(format: Format) { let outputFile = format.pandoc[kOutputFile]; if (outputFile) {