From 2c8963a8e4d6bcec46d946e4d65ae5d40c5f7b6c Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 26 Mar 2025 10:20:41 -0400 Subject: [PATCH 1/5] manuscript - do not cloneDeep Kv. create safeCloneDeep --- src/core/cache/cache.ts | 7 +++- src/core/safe-clone-deep.ts | 36 +++++++++++++++++++ src/core/sass.ts | 3 ++ src/core/sass/add-css-vars.ts | 16 ++++++++- src/core/sass/cache.ts | 7 +++- .../notebook/notebook-contributor-html.ts | 3 +- .../notebook/notebook-contributor-ipynb.ts | 3 +- .../notebook/notebook-contributor-jats.ts | 3 +- .../notebook/notebook-contributor-qmd.ts | 3 +- 9 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/core/safe-clone-deep.ts diff --git a/src/core/cache/cache.ts b/src/core/cache/cache.ts index 8b447da3ceb..c32e6f42df0 100644 --- a/src/core/cache/cache.ts +++ b/src/core/cache/cache.ts @@ -19,6 +19,7 @@ import { type ImmediateBufferCacheEntry, type ImmediateStringCacheEntry, } from "./cache-types.ts"; +import { Cloneable } from "../safe-clone-deep.ts"; export { type ProjectCache } from "./cache-types.ts"; const currentCacheVersion = "1"; @@ -26,7 +27,7 @@ const requiredQuartoVersions: Record = { "1": ">1.7.0", }; -class ProjectCacheImpl { +class ProjectCacheImpl implements Cloneable { projectScratchDir: string; index: Deno.Kv | null; @@ -35,6 +36,10 @@ class ProjectCacheImpl { this.index = null; } + clone() { + return this; + } + close() { if (this.index) { this.index.close(); diff --git a/src/core/safe-clone-deep.ts b/src/core/safe-clone-deep.ts new file mode 100644 index 00000000000..c4b688dce0d --- /dev/null +++ b/src/core/safe-clone-deep.ts @@ -0,0 +1,36 @@ +/* + * safe-clone-deep.ts + * + * CloneDeep that uses object's own cloning methods when available + * + * Copyright (C) 2025 Posit Software, PBC + */ + +export interface Cloneable { + clone(): T; +} + +export function safeCloneDeep(obj: T): T { + if (obj === null || typeof obj !== "object") { + return obj; + } + + // Handle arrays + if (Array.isArray(obj)) { + return obj.map((item) => safeCloneDeep(item)) as T; + } + + if (obj && ("clone" in obj) && typeof obj.clone === "function") { + return obj.clone(); + } + + // Handle regular objects + const result = {} as T; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + result[key] = safeCloneDeep(obj[key]); + } + } + + return result; +} diff --git a/src/core/sass.ts b/src/core/sass.ts index 762a4d4937e..00589494654 100644 --- a/src/core/sass.ts +++ b/src/core/sass.ts @@ -384,6 +384,9 @@ export async function compileWithCache( const result = await memoizedGetVarsBlock(project, input); return input + "\n" + result; } catch (e) { + if (e.name !== "SCSSParsingError") { + throw e; + } console.warn("Error adding css vars block", e); console.warn( "The resulting CSS file will not have SCSS color variables exported as CSS.", diff --git a/src/core/sass/add-css-vars.ts b/src/core/sass/add-css-vars.ts index cac7db9fe98..6bda466533d 100644 --- a/src/core/sass/add-css-vars.ts +++ b/src/core/sass/add-css-vars.ts @@ -15,8 +15,22 @@ import { propagateDeclarationTypes } from "./analyzer/declaration-types.ts"; import { getVariableDependencies } from "./analyzer/get-dependencies.ts"; const { getSassAst } = makeParserModule(parse); + +export class SCSSParsingError extends Error { + constructor(message: string) { + super(`SCSS Parsing Error: ${message}`); + this.name = "SCSSParsingError"; + } +} + export const cssVarsBlock = (scssSource: string) => { - const ast = propagateDeclarationTypes(cleanSassAst(getSassAst(scssSource))); + let astOriginal; + try { + astOriginal = getSassAst(scssSource); + } catch (e) { + throw new SCSSParsingError(e.message); + } + const ast = propagateDeclarationTypes(cleanSassAst(astOriginal)); const deps = getVariableDependencies(ast); const output: string[] = [":root {"]; diff --git a/src/core/sass/cache.ts b/src/core/sass/cache.ts index 74e1117d216..ee3d7a80fee 100644 --- a/src/core/sass/cache.ts +++ b/src/core/sass/cache.ts @@ -14,11 +14,16 @@ import { TempContext } from "../temp.ts"; import { safeRemoveIfExists } from "../path.ts"; import * as log from "../../deno_ral/log.ts"; import { onCleanup } from "../cleanup.ts"; +import { Cloneable } from "../safe-clone-deep.ts"; -class SassCache { +class SassCache implements Cloneable { kv: Deno.Kv; path: string; + clone() { + return this; + } + constructor(kv: Deno.Kv, path: string) { this.kv = kv; this.path = path; diff --git a/src/render/notebook/notebook-contributor-html.ts b/src/render/notebook/notebook-contributor-html.ts index 448a1c61106..d5f9c2ad4ef 100644 --- a/src/render/notebook/notebook-contributor-html.ts +++ b/src/render/notebook/notebook-contributor-html.ts @@ -51,6 +51,7 @@ import { isQmdFile } from "../../execute/qmd.ts"; import { dirAndStem } from "../../core/path.ts"; import { projectOutputDir } from "../../project/project-shared.ts"; import { existsSync } from "../../deno_ral/fs.ts"; +import { safeCloneDeep } from "../../core/safe-clone-deep.ts"; export const htmlNotebookContributor: NotebookContributor = { resolve: resolveHtmlNotebook, @@ -85,7 +86,7 @@ function resolveHtmlNotebook( executedFile: ExecutedFile, notebookMetadata?: NotebookMetadata, ) { - const resolved = ld.cloneDeep(executedFile) as ExecutedFile; + const resolved = safeCloneDeep(executedFile) as ExecutedFile; // Set the output file resolved.recipe.format.pandoc[kOutputFile] = `${outputFile(nbAbsPath)}`; diff --git a/src/render/notebook/notebook-contributor-ipynb.ts b/src/render/notebook/notebook-contributor-ipynb.ts index 9a702a6456d..e021edc01e7 100644 --- a/src/render/notebook/notebook-contributor-ipynb.ts +++ b/src/render/notebook/notebook-contributor-ipynb.ts @@ -34,6 +34,7 @@ import { ipynbTitleTemplatePath } from "../../format/ipynb/format-ipynb.ts"; import { projectOutputDir } from "../../project/project-shared.ts"; import { existsSync } from "../../deno_ral/fs.ts"; import { dirname, join, relative } from "../../deno_ral/path.ts"; +import { safeCloneDeep } from "../../core/safe-clone-deep.ts"; export const outputNotebookContributor: NotebookContributor = { resolve: resolveOutputNotebook, @@ -67,7 +68,7 @@ function resolveOutputNotebook( executedFile: ExecutedFile, _notebookMetadata?: NotebookMetadata, ) { - const resolved = ld.cloneDeep(executedFile); + const resolved = safeCloneDeep(executedFile); resolved.recipe.format.pandoc[kOutputFile] = outputFile(nbAbsPath); resolved.recipe.output = resolved.recipe.format.pandoc[kOutputFile]; diff --git a/src/render/notebook/notebook-contributor-jats.ts b/src/render/notebook/notebook-contributor-jats.ts index c0338f34f5b..174f021a407 100644 --- a/src/render/notebook/notebook-contributor-jats.ts +++ b/src/render/notebook/notebook-contributor-jats.ts @@ -37,6 +37,7 @@ import * as ld from "../../core/lodash.ts"; import { error } from "../../deno_ral/log.ts"; import { Format } from "../../config/types.ts"; +import { safeCloneDeep } from "../../core/safe-clone-deep.ts"; export const jatsContributor: NotebookContributor = { resolve: resolveJats, @@ -56,7 +57,7 @@ function resolveJats( executedFile: ExecutedFile, _notebookMetadata?: NotebookMetadata, ) { - const resolved = ld.cloneDeep(executedFile); + const resolved = safeCloneDeep(executedFile); const to = resolved.recipe.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 ad9ff7d17af..9ad5a641863 100644 --- a/src/render/notebook/notebook-contributor-qmd.ts +++ b/src/render/notebook/notebook-contributor-qmd.ts @@ -38,6 +38,7 @@ import { ipynbTitleTemplatePath } from "../../format/ipynb/format-ipynb.ts"; import { projectScratchPath } from "../../project/project-scratch.ts"; import { ensureDirSync, existsSync } from "../../deno_ral/fs.ts"; import { dirname, join, relative } from "../../deno_ral/path.ts"; +import { safeCloneDeep } from "../../core/safe-clone-deep.ts"; export const qmdNotebookContributor: NotebookContributor = { resolve: resolveOutputNotebook, @@ -86,7 +87,7 @@ function resolveOutputNotebook( executedFile: ExecutedFile, _notebookMetadata?: NotebookMetadata, ) { - const resolved = ld.cloneDeep(executedFile); + const resolved = safeCloneDeep(executedFile); resolved.recipe.format.pandoc[kOutputFile] = ipynbOutputFile(nbAbsPath); resolved.recipe.output = resolved.recipe.format.pandoc[kOutputFile]; From bf818b3dba92be6c6f3241f8fd87ef353bee6a48 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Mar 2025 16:42:47 +0100 Subject: [PATCH 2/5] fix cloneDeep issue for epub book too regarding Deno.kv() --- src/command/render/render-contexts.ts | 15 --------------- src/command/render/render-files.ts | 5 +++-- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/command/render/render-contexts.ts b/src/command/render/render-contexts.ts index 0b3aec77acd..c6ca7dc7de4 100644 --- a/src/command/render/render-contexts.ts +++ b/src/command/render/render-contexts.ts @@ -89,21 +89,6 @@ import { import { ExtensionContext } from "../../extension/types.ts"; import { NotebookContext } from "../../render/notebook/notebook-types.ts"; -// we can't naively ld.cloneDeep everything -// because that destroys class instances -// with private members -// -// Currently, that's ProjectContext. -// -// TODO: Ideally, we shouldn't be copying the RenderContext at all. -export function copyRenderContext( - context: RenderContext, -): RenderContext { - return { - ...ld.cloneDeep(context), - project: context.project, - }; -} export async function resolveFormatsFromMetadata( metadata: Metadata, input: string, diff --git a/src/command/render/render-files.ts b/src/command/render/render-files.ts index 316f6cc899a..98527326688 100644 --- a/src/command/render/render-files.ts +++ b/src/command/render/render-files.ts @@ -48,7 +48,7 @@ import { outputRecipe } from "./output.ts"; import { renderPandoc } from "./render.ts"; import { PandocRenderCompletion, RenderServices } from "./types.ts"; -import { copyRenderContext, renderContexts } from "./render-contexts.ts"; +import { renderContexts } from "./render-contexts.ts"; import { renderProgress } from "./render-info.ts"; import { ExecutedFile, @@ -114,6 +114,7 @@ import { } from "../../project/project-shared.ts"; import { NotebookContext } from "../../render/notebook/notebook-types.ts"; import { setExecuteEnvironment } from "../../execute/environment.ts"; +import { safeCloneDeep } from "../../core/safe-clone-deep.ts"; export async function renderExecute( context: RenderContext, @@ -503,7 +504,7 @@ async function renderFileInternal( for (const format of Object.keys(contexts)) { pushTiming("render-context"); - const context = copyRenderContext(contexts[format]); // since we're going to mutate it... + const context = safeCloneDeep(contexts[format]); // since we're going to mutate it... // disquality some documents from server: shiny if (isServerShiny(context.format) && context.project) { From b77c30a17aad7cba91933197318f74a6a0755900 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Mar 2025 17:02:49 +0100 Subject: [PATCH 3/5] various cleaning - unused import - foundBrand is a const now --- src/command/render/pandoc-html.ts | 36 ++++++---- src/command/render/project.ts | 1 - src/core/sass.ts | 1 - src/execute/engine.ts | 3 +- src/project/project-context.ts | 2 +- src/project/project-shared.ts | 66 ++++++++++++------- .../listing/website-listing-template.ts | 2 +- .../notebook/notebook-contributor-html.ts | 2 - .../notebook/notebook-contributor-ipynb.ts | 3 - .../notebook/notebook-contributor-jats.ts | 3 - .../notebook/notebook-contributor-qmd.ts | 3 - 11 files changed, 68 insertions(+), 54 deletions(-) diff --git a/src/command/render/pandoc-html.ts b/src/command/render/pandoc-html.ts index 4d162adb803..86ade028aec 100644 --- a/src/command/render/pandoc-html.ts +++ b/src/command/render/pandoc-html.ts @@ -19,7 +19,6 @@ import { } from "../../config/types.ts"; import { ProjectContext } from "../../project/types.ts"; -import { TempContext } from "../../core/temp.ts"; import { cssImports, cssResources } from "../../core/css.ts"; import { cleanSourceMappingUrl, compileSass } from "../../core/sass.ts"; @@ -91,10 +90,12 @@ export async function resolveSassBundles( const maybeBrandBundle = bundlesWithBrand.find((bundle) => bundle.key === "brand" ); - assert(!maybeBrandBundle || - !maybeBrandBundle.user?.find((v) => v === "brand") && - !maybeBrandBundle.dark?.user?.find((v) => v === "brand")); - let foundBrand = {light: false, dark: false}; + assert( + !maybeBrandBundle || + !maybeBrandBundle.user?.find((v) => v === "brand") && + !maybeBrandBundle.dark?.user?.find((v) => v === "brand"), + ); + const foundBrand = { light: false, dark: false }; const bundles: SassBundle[] = bundlesWithBrand.filter((bundle) => bundle.key !== "brand" ).map((bundle) => { @@ -106,12 +107,18 @@ export async function resolveSassBundles( bundle.user!.splice(userBrand, 1, ...(maybeBrandBundle?.user || [])); foundBrand.light = true; } - const darkBrand = bundle.dark?.user?.findIndex((layer) => layer === "brand"); + const darkBrand = bundle.dark?.user?.findIndex((layer) => + layer === "brand" + ); if (darkBrand && darkBrand !== -1) { if (!cloned) { bundle = cloneDeep(bundle); } - bundle.dark!.user!.splice(darkBrand, 1, ...(maybeBrandBundle?.dark?.user || [])) + bundle.dark!.user!.splice( + darkBrand, + 1, + ...(maybeBrandBundle?.dark?.user || []), + ); foundBrand.dark = true; } return bundle as SassBundle; @@ -122,18 +129,19 @@ export async function resolveSassBundles( key: "brand", user: !foundBrand.light && maybeBrandBundle?.user as SassLayer[] || [], dark: !foundBrand.dark && maybeBrandBundle?.dark?.user && { - user: maybeBrandBundle.dark.user as SassLayer[], - default: maybeBrandBundle.dark.default - } || undefined + user: maybeBrandBundle.dark.user as SassLayer[], + default: maybeBrandBundle.dark.default, + } || undefined, }); } // See if any bundles are providing dark specific css const hasDark = bundles.some((bundle) => bundle.dark !== undefined); - defaultStyle = - bundles.some((bundle) => bundle.dark !== undefined && bundle.dark.default) - ? "dark" - : "light"; + defaultStyle = bundles.some((bundle) => + bundle.dark !== undefined && bundle.dark.default + ) + ? "dark" + : "light"; const targets: SassTarget[] = [{ name: `${dependency}.min.css`, bundles: (bundles as any), diff --git a/src/command/render/project.ts b/src/command/render/project.ts index 5ce9f59b96e..bc5730f46c8 100644 --- a/src/command/render/project.ts +++ b/src/command/render/project.ts @@ -81,7 +81,6 @@ import { fileExecutionEngine } from "../../execute/engine.ts"; import { projectContextForDirectory } from "../../project/project-context.ts"; import { ProjectType } from "../../project/types/types.ts"; import { ProjectConfig as ProjectConfig_Project } from "../../resources/types/schema-types.ts"; -import { Extension } from "../../extension/types.ts"; const noMutationValidations = ( projType: ProjectType, diff --git a/src/core/sass.ts b/src/core/sass.ts index 00589494654..85ca011a04d 100644 --- a/src/core/sass.ts +++ b/src/core/sass.ts @@ -8,7 +8,6 @@ import { existsSync } from "../deno_ral/fs.ts"; import { join } from "../deno_ral/path.ts"; import { quartoCacheDir } from "./appdirs.ts"; -import { TempContext } from "./temp.ts"; import { SassBundleLayers, SassLayer } from "../config/types.ts"; import { dartCompile } from "./dart-sass.ts"; diff --git a/src/execute/engine.ts b/src/execute/engine.ts index 9386b542e53..3b8f5d589a3 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, resolve } from "../deno_ral/path.ts"; +import { extname, join } from "../deno_ral/path.ts"; import * as ld from "../core/lodash.ts"; @@ -23,7 +23,6 @@ import { kMdExtensions, markdownEngine } from "./markdown.ts"; import { ExecutionEngine, kQmdExtensions } from "./types.ts"; import { languagesInMarkdown } from "./engine-shared.ts"; import { languages as handlerLanguages } from "../core/handlers/base.ts"; -import { MappedString } from "../core/lib/text-types.ts"; import { RenderContext, RenderFlags } from "../command/render/types.ts"; import { mergeConfigs } from "../core/config.ts"; import { ProjectContext } from "../project/types.ts"; diff --git a/src/project/project-context.ts b/src/project/project-context.ts index 2df6190f97e..1208a7f0d14 100644 --- a/src/project/project-context.ts +++ b/src/project/project-context.ts @@ -102,7 +102,7 @@ import { NotebookContext } from "../render/notebook/notebook-types.ts"; import { MappedString } from "../core/mapped-text.ts"; import { makeTimedFunctionAsync } from "../core/performance/function-times.ts"; import { createProjectCache } from "../core/cache/cache.ts"; -import { createTempContext, globalTempContext } from "../core/temp.ts"; +import { createTempContext } from "../core/temp.ts"; export async function projectContext( path: string, diff --git a/src/project/project-shared.ts b/src/project/project-shared.ts index 7bf97775906..a8dd1f43004 100644 --- a/src/project/project-shared.ts +++ b/src/project/project-shared.ts @@ -47,9 +47,11 @@ import { normalizeNewlines } from "../core/lib/text.ts"; import { DirectiveCell } from "../core/lib/break-quarto-md-types.ts"; import { QuartoJSONSchema, readYamlFromMarkdown } from "../core/yaml.ts"; import { refSchema } from "../core/lib/yaml-schema/common.ts"; -import { Brand as BrandJson, BrandPathBoolLightDark } from "../resources/types/schema-types.ts"; +import { + Brand as BrandJson, + BrandPathBoolLightDark, +} from "../resources/types/schema-types.ts"; import { Brand } from "../core/brand/brand.ts"; -import { warnOnce } from "../core/log.ts"; import { assert } from "testing/asserts"; export function projectExcludeDirs(context: ProjectContext): string[] { @@ -503,6 +505,10 @@ export const ensureFileInformationCache = ( if (!project.fileInformationCache) { project.fileInformationCache = new Map(); } + assert( + project.fileInformationCache instanceof Map, + JSON.stringify(project.fileInformationCache), + ); if (!project.fileInformationCache.has(file)) { project.fileInformationCache.set(file, {} as FileInformation); } @@ -512,8 +518,8 @@ export const ensureFileInformationCache = ( export async function projectResolveBrand( project: ProjectContext, fileName?: string, -) : Promise<{light?: Brand, dark?: Brand} | undefined> { - async function loadBrand(brandPath: string) : Promise { +): Promise<{ light?: Brand; dark?: Brand } | undefined> { + async function loadBrand(brandPath: string): Promise { const brand = await readAndValidateYamlFromFile( brandPath, refSchema("brand", "Format-independent brand configuration."), @@ -521,7 +527,10 @@ export async function projectResolveBrand( ) as BrandJson; return new Brand(brand, dirname(brandPath), project.dir); } - async function loadRelativeBrand(brandPath: string, dir: string = dirname(fileName!)) : Promise { + async function loadRelativeBrand( + brandPath: string, + dir: string = dirname(fileName!), + ): Promise { let resolved: string = ""; if (brandPath.startsWith("/")) { resolved = join(project.dir, brandPath); @@ -538,18 +547,27 @@ export async function projectResolveBrand( let fileNames = ["_brand.yml", "_brand.yaml"].map((file) => join(project.dir, file) ); - let brand = project?.config?.brand as Boolean | string | {light?: string, dark?: string}; + let brand = project?.config?.brand as Boolean | string | { + light?: string; + dark?: string; + }; if (brand === false) { project.brandCache.brand = undefined; return project.brandCache.brand; } - if (typeof brand === "object" && brand && - ("light" in brand || "dark" in brand)) { + if ( + typeof brand === "object" && brand && + ("light" in brand || "dark" in brand) + ) { project.brandCache.brand = { - light: brand.light ? await loadRelativeBrand(brand.light, project.dir) : undefined, - dark: brand.dark ? await loadRelativeBrand(brand.dark, project.dir) : undefined, + light: brand.light + ? await loadRelativeBrand(brand.light, project.dir) + : undefined, + dark: brand.dark + ? await loadRelativeBrand(brand.dark, project.dir) + : undefined, }; - return project.brandCache.brand; + return project.brandCache.brand; } if (typeof brand === "string") { fileNames = [join(project.dir, brand)]; @@ -559,7 +577,7 @@ export async function projectResolveBrand( if (!existsSync(brandPath)) { continue; } - project.brandCache.brand = {light: await loadBrand(brandPath)}; + project.brandCache.brand = { light: await loadBrand(brandPath) }; } return project.brandCache.brand; } else { @@ -576,37 +594,39 @@ export async function projectResolveBrand( return fileInformation.brand; } if (typeof brand === "string") { - fileInformation.brand = {light: await loadRelativeBrand(brand)}; + fileInformation.brand = { light: await loadRelativeBrand(brand) }; return fileInformation.brand; } else { assert(typeof brand === "object"); if ("light" in brand || "dark" in brand) { let light, dark; if (typeof brand.light === "string") { - light = await loadRelativeBrand(brand.light) + light = await loadRelativeBrand(brand.light); } else { light = new Brand( brand.light!, dirname(fileName), - project.dir + project.dir, ); } if (typeof brand.dark === "string") { - dark = await loadRelativeBrand(brand.dark) + dark = await loadRelativeBrand(brand.dark); } else { dark = new Brand( brand.dark!, dirname(fileName), - project.dir + project.dir, ); } - fileInformation.brand = {light, dark}; + fileInformation.brand = { light, dark }; } else { - fileInformation.brand = {light: new Brand( - brand as BrandJson, - dirname(fileName), - project.dir, - )}; + fileInformation.brand = { + light: new Brand( + brand as BrandJson, + dirname(fileName), + project.dir, + ), + }; } return fileInformation.brand; } diff --git a/src/project/types/website/listing/website-listing-template.ts b/src/project/types/website/listing/website-listing-template.ts index 4d4136a618a..50a538b23aa 100644 --- a/src/project/types/website/listing/website-listing-template.ts +++ b/src/project/types/website/listing/website-listing-template.ts @@ -6,7 +6,7 @@ * */ import { Document, Element } from "deno_dom/deno-dom-wasm-noinit.ts"; -import { cloneDeep, escape } from "../../../../core/lodash.ts"; +import { escape } from "../../../../core/lodash.ts"; import { kListingPageMinutesCompact, kListingPageOrderByDateAsc, diff --git a/src/render/notebook/notebook-contributor-html.ts b/src/render/notebook/notebook-contributor-html.ts index d5f9c2ad4ef..2505c52d29d 100644 --- a/src/render/notebook/notebook-contributor-html.ts +++ b/src/render/notebook/notebook-contributor-html.ts @@ -39,8 +39,6 @@ import { NotebookTemplateMetadata, } from "./notebook-types.ts"; -import * as ld from "../../core/lodash.ts"; - import { error } from "../../deno_ral/log.ts"; import { formatResourcePath } from "../../core/resources.ts"; import { kNotebookViewStyleNotebook } from "../../format/html/format-html-constants.ts"; diff --git a/src/render/notebook/notebook-contributor-ipynb.ts b/src/render/notebook/notebook-contributor-ipynb.ts index e021edc01e7..81df494d514 100644 --- a/src/render/notebook/notebook-contributor-ipynb.ts +++ b/src/render/notebook/notebook-contributor-ipynb.ts @@ -25,9 +25,6 @@ import { InternalError } from "../../core/lib/error.ts"; import { dirAndStem } from "../../core/path.ts"; import { ProjectContext } from "../../project/types.ts"; import { NotebookContributor, NotebookMetadata } from "./notebook-types.ts"; - -import * as ld from "../../core/lodash.ts"; - import { error } from "../../deno_ral/log.ts"; import { Format } from "../../config/types.ts"; import { ipynbTitleTemplatePath } from "../../format/ipynb/format-ipynb.ts"; diff --git a/src/render/notebook/notebook-contributor-jats.ts b/src/render/notebook/notebook-contributor-jats.ts index 174f021a407..7cdc2891599 100644 --- a/src/render/notebook/notebook-contributor-jats.ts +++ b/src/render/notebook/notebook-contributor-jats.ts @@ -32,9 +32,6 @@ import { import { subarticleTemplatePath } from "../../format/jats/format-jats-paths.ts"; import { ProjectContext } from "../../project/types.ts"; import { NotebookContributor, NotebookMetadata } from "./notebook-types.ts"; - -import * as ld from "../../core/lodash.ts"; - import { error } from "../../deno_ral/log.ts"; import { Format } from "../../config/types.ts"; import { safeCloneDeep } from "../../core/safe-clone-deep.ts"; diff --git a/src/render/notebook/notebook-contributor-qmd.ts b/src/render/notebook/notebook-contributor-qmd.ts index 9ad5a641863..9cee937017b 100644 --- a/src/render/notebook/notebook-contributor-qmd.ts +++ b/src/render/notebook/notebook-contributor-qmd.ts @@ -29,9 +29,6 @@ import { NotebookMetadata, NotebookOutput, } from "./notebook-types.ts"; - -import * as ld from "../../core/lodash.ts"; - import { error } from "../../deno_ral/log.ts"; import { Format } from "../../config/types.ts"; import { ipynbTitleTemplatePath } from "../../format/ipynb/format-ipynb.ts"; From c1805c3621b8dbc9fa47087226ea809c8bca537e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Mar 2025 18:39:33 +0100 Subject: [PATCH 4/5] safeCloneDeep shoud handle Maps and Sets too --- src/core/safe-clone-deep.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/core/safe-clone-deep.ts b/src/core/safe-clone-deep.ts index c4b688dce0d..c9fd00dd5c6 100644 --- a/src/core/safe-clone-deep.ts +++ b/src/core/safe-clone-deep.ts @@ -24,6 +24,24 @@ export function safeCloneDeep(obj: T): T { return obj.clone(); } + // Handle Maps + if (obj instanceof Map) { + const clonedMap = new Map(); + for (const [key, value] of obj.entries()) { + clonedMap.set(key, safeCloneDeep(value)); + } + return clonedMap as T; + } + + // Handle Sets + if (obj instanceof Set) { + const clonedSet = new Set(); + for (const value of obj.values()) { + clonedSet.add(safeCloneDeep(value)); + } + return clonedSet as T; + } + // Handle regular objects const result = {} as T; for (const key in obj) { From bca2d32697f210f7feae97e83c8f6ceb67000f5c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Mar 2025 19:38:57 +0100 Subject: [PATCH 5/5] use also safeCloneDeep when merging executed files for epub book output for example. --- src/project/types/book/book-render.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/project/types/book/book-render.ts b/src/project/types/book/book-render.ts index 7006aa9554e..d78d01e6683 100644 --- a/src/project/types/book/book-render.ts +++ b/src/project/types/book/book-render.ts @@ -92,6 +92,7 @@ import { resourcePath } from "../../../core/resources.ts"; import { PandocAttr, PartitionedMarkdown } from "../../../core/pandoc/types.ts"; import { stringify } from "../../../core/yaml.ts"; import { waitUntilNamedLifetime } from "../../../core/lifetimes.ts"; +import { safeCloneDeep } from "../../../core/safe-clone-deep.ts"; export function bookPandocRenderer( options: RenderOptions, @@ -381,7 +382,7 @@ async function mergeExecutedFiles( files: ExecutedFile[], ): Promise { // base context on the first file (which has to be index.md in the root) - const context = ld.cloneDeep(files[0].context) as RenderContext; + const context = safeCloneDeep(files[0].context); // use global render options context.options = removePandocTo(options);