diff --git a/news/changelog-1.8.md b/news/changelog-1.8.md index 2047ffe9e41..ec10b18d463 100644 --- a/news/changelog-1.8.md +++ b/news/changelog-1.8.md @@ -83,3 +83,4 @@ All changes included in 1.8: - ([#11321](https://github.com/quarto-dev/quarto-cli/issues/11321)): Follow [recommendation from LaTeX project](https://latex-project.org/news/latex2e-news/ltnews40.pdf) and use `lualatex` instead of `xelatex` as the default PDF engine. - ([#12782](https://github.com/quarto-dev/quarto-cli/pull/12782)): fix bug on `safeRemoveDirSync`'s detection of safe directory boundaries. +- ([#12853](https://github.com/quarto-dev/quarto-cli/issues/12853)): fix replaceAll() escaping issue with embedded notebooks containing `$` in their Markdown. \ No newline at end of file diff --git a/package/src/common/update-html-dependencies.ts b/package/src/common/update-html-dependencies.ts index 40aed8c6e84..ad7f0147722 100644 --- a/package/src/common/update-html-dependencies.ts +++ b/package/src/common/update-html-dependencies.ts @@ -820,47 +820,47 @@ async function updateBootstrapFromBslib( for (let line of varContents) { line = line.replaceAll( "var(--#{$prefix}font-sans-serif)", - "$font-family-sans-serif" + "$$font-family-sans-serif" ); line = line.replaceAll( "var(--#{$prefix}font-monospace)", - "$font-family-monospace" + "$$font-family-monospace" ); line = line.replaceAll( "var(--#{$prefix}success-rgb)", - "$success" + "$$success" ); line = line.replaceAll( "var(--#{$prefix}danger-rgb)", - "$danger" + "$$danger" ); line = line.replaceAll( "var(--#{$prefix}body-color-rgb)", - "$body-color" + "$$body-color" ); line = line.replaceAll( "var(--#{$prefix}body-bg-rgb)", - "$body-bg" + "$$body-bg" ); line = line.replaceAll( "var(--#{$prefix}emphasis-color-rgb)", - "$body-emphasis-color" + "$$body-emphasis-color" ); line = line.replaceAll( /RGBA?\(var\(--#\{\$prefix\}emphasis-color-rgb,(.*?)\).*?\)/gm, - "$body-emphasis-color" + "$$body-emphasis-color" ); line = line.replaceAll( "var(--#{$prefix}secondary-color)", - "$body-secondary-color" + "$$body-secondary-color" ); line = line.replaceAll( "var(--#{$prefix}secondary-bg)", - "$body-secondary-bg" + "$$body-secondary-bg" ); line = line.replaceAll( "var(--#{$prefix}tertiary-bg)", - "$body-tertiary-bg" + "$$body-tertiary-bg" ); line = line.replaceAll( "var(--#{$prefix}tertiary-color)", @@ -868,15 +868,15 @@ async function updateBootstrapFromBslib( ); line = line.replaceAll( "var(--#{$prefix}emphasis-bg)", - "$body-emphasis-bg" + "$$body-emphasis-bg" ); line = line.replaceAll( "var(--#{$prefix}emphasis-color)", - "$body-emphasis-color" + "$$body-emphasis-color" ); line = line.replaceAll( "$emphasis-color-rgb", - "$body-emphasis-color" + "$$body-emphasis-color" ); line = line.replaceAll(/var\(--#\{\$prefix\}(.*?)\)/gm, "$$$1"); diff --git a/src/core/handlers/embed.ts b/src/core/handlers/embed.ts index 61bd0e65c16..f8940514b28 100644 --- a/src/core/handlers/embed.ts +++ b/src/core/handlers/embed.ts @@ -1,9 +1,8 @@ /* -* embed.ts -* -* Copyright (C) 2022 by Posit, PBC -* -*/ + * embed.ts + * + * Copyright (C) 2022-2025 by Posit, PBC + */ import { LanguageCellHandlerContext, LanguageHandler } from "./types.ts"; import { baseHandler, install } from "./base.ts"; diff --git a/src/core/handlers/mermaid.ts b/src/core/handlers/mermaid.ts index 489879d9890..66aa8bbc9cc 100644 --- a/src/core/handlers/mermaid.ts +++ b/src/core/handlers/mermaid.ts @@ -258,7 +258,7 @@ mermaid.initialize(${JSON.stringify(mermaidOpts)}); const oldId = svg.getAttribute("id") as string; svg.setAttribute("id", newMermaidId); const style = svg.querySelector("style")!; - style.innerHTML = style.innerHTML.replaceAll(oldId, newMermaidId); + style.innerHTML = style.innerHTML.replaceAll(oldId, () => newMermaidId); for (const defNode of svg.querySelectorAll("defs")) { const defEl = defNode as Element; @@ -296,11 +296,11 @@ mermaid.initialize(${JSON.stringify(mermaidOpts)}); // this string substitution is fraught, but I don't know how else to fix the problem. oldSvgSrc = oldSvgSrc.replaceAll( `"${idToPatch}"`, - `"${to}"`, + () => `"${to}"`, ); oldSvgSrc = oldSvgSrc.replaceAll( `#${idToPatch}`, - `#${to}`, + () => `#${to}`, ); } svg = mappedDiff(svg, oldSvgSrc); diff --git a/src/core/jupyter/jupyter-embed.ts b/src/core/jupyter/jupyter-embed.ts index fc6d3402c11..16586e825de 100644 --- a/src/core/jupyter/jupyter-embed.ts +++ b/src/core/jupyter/jupyter-embed.ts @@ -339,7 +339,14 @@ export async function replaceNotebookPlaceholders( } // Replace the placeholders with the rendered markdown - markdown = markdown.replaceAll(match[0], nbMarkdown || ""); + markdown = markdown.replaceAll( + match[0], + // https://github.com/quarto-dev/quarto-cli/issues/12853 + // we use a function here to avoid + // escaping issues with $ in the markdown + // (e.g. $x$ in math mode) + () => nbMarkdown ?? "", + ); } match = regex.exec(markdown); } diff --git a/src/core/jupyter/jupyter.ts b/src/core/jupyter/jupyter.ts index 0d3cf4a3b7d..0281c144f67 100644 --- a/src/core/jupyter/jupyter.ts +++ b/src/core/jupyter/jupyter.ts @@ -1039,7 +1039,7 @@ export function mdFromContentCell( for (let i = 0; i < source.length; i++) { source[i] = source[i].replaceAll( `attachment:${file}`, - imageFile, + () => imageFile, ); } // only process one supported mime type diff --git a/src/execute/rmd.ts b/src/execute/rmd.ts index 0ee1b46c3fa..07589a60bdc 100644 --- a/src/execute/rmd.ts +++ b/src/execute/rmd.ts @@ -135,7 +135,10 @@ export const knitrEngine: ExecutionEngine = { options.quiet, // fixup .rmarkdown file references (output) => { - output = output.replaceAll(`${inputStem}.rmarkdown`, inputBasename); + output = output.replaceAll( + `${inputStem}.rmarkdown`, + () => inputBasename, + ); const m = output.match(/^Quitting from lines (\d+)-(\d+)/m); if (m) { diff --git a/src/format/jats/format-jats-postprocess.ts b/src/format/jats/format-jats-postprocess.ts index 46f63e19f5a..da9309f36af 100644 --- a/src/format/jats/format-jats-postprocess.ts +++ b/src/format/jats/format-jats-postprocess.ts @@ -88,7 +88,7 @@ export const renderSubarticlePostProcessor = ( // Replace the placeholder with the rendered subarticle outputContents = outputContents.replaceAll( placeholder, - subArtLines.join("\n"), + () => subArtLines.join("\n"), ); // Move supporting and resource files into place diff --git a/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/.gitignore b/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/.gitignore new file mode 100644 index 00000000000..9f3a92498a3 --- /dev/null +++ b/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/.gitignore @@ -0,0 +1,221 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints +.jupyter_cache + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# MACOS +.DS_Store + +# Files generated by invoking Julia with --code-coverage +*.jl.cov +*.jl.*.cov +*.jl.mem + +Manifest.toml + +.vscode + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# these are all temporary files that will be generated during Quarto render +/.quarto/ +/_freeze/ +/_site/ +/_output/ +/_book/ +/index_files/ +*.embed_files/ +*.html +*.gif +*.json +/site_libs/ +_tmp_fig.svg + +# Files generated from Binder +.TinyTeX +.code-server +.config +.local +.yarn +.jupyter-server-log.txt +.profile +.bashrc +.bash_logout +.wget-hsts diff --git a/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/index.qmd b/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/index.qmd new file mode 100644 index 00000000000..c9c02e65f9b --- /dev/null +++ b/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/index.qmd @@ -0,0 +1,15 @@ +--- +title: Home Page +format: + html: default + pdf: default +--- + +## From an embedded notebook + +{{< embed somefolder/somefile.qmd#plot-phase-space-density echo=true >}} + +## Still from an embedded notebook + +{{< embed somefolder/somefile.qmd#plot-final-density echo=true >}} + diff --git a/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/somefolder/somefile.qmd b/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/somefolder/somefile.qmd new file mode 100644 index 00000000000..36f2cd1a593 --- /dev/null +++ b/tests/docs/smoke-all/2025/06/11/issue-12853/quarto-test/somefolder/somefile.qmd @@ -0,0 +1,50 @@ +# Some File + +```{python} +#| label: import-libraries + +import numpy as np +import matplotlib.pyplot as plt +from IPython.display import SVG +``` + +Some text... + +```{python} +#| label: plot-phase-space-density + +from IPython.display import HTML +from matplotlib.animation import FuncAnimation + +N_x = 101 +N_px = 101 + +x_list = np.linspace(-2, 2, N_x) +px_list = np.linspace(-2, 2, N_px) +t_list = np.linspace(0, 2, 50000) + +rho_t = np.random.rand(N_x, N_px) + +fig, ax = plt.subplots() + +img = ax.pcolormesh(x_list, px_list, rho_t**2, shading='gouraud', rasterized=True) +ax.set_xlabel('Position $x$') +ax.set_ylabel('Momentum $p_x$') + +plt.show() +``` + +Other text... + +```{python} +#| label: plot-final-density + +fig, ax = plt.subplots() + +img = ax.pcolormesh(x_list, px_list, rho_t**2, shading='gouraud', rasterized=True) +ax.set_xlabel('Position $x$') +ax.set_ylabel('Momentum $p_x$') + +plt.show() +``` +