Skip to content

Commit ab22bb9

Browse files
authored
Merge pull request #12916 from quarto-dev/bugfix/12853
ensure replaceAll() is called with function parameter when str ing is not known to be free of $ (#12853)
2 parents 3cea512 + 9342a59 commit ab22bb9

File tree

11 files changed

+322
-26
lines changed

11 files changed

+322
-26
lines changed

news/changelog-1.8.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,4 @@ All changes included in 1.8:
8383

8484
- ([#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.
8585
- ([#12782](https://github.com/quarto-dev/quarto-cli/pull/12782)): fix bug on `safeRemoveDirSync`'s detection of safe directory boundaries.
86+
- ([#12853](https://github.com/quarto-dev/quarto-cli/issues/12853)): fix replaceAll() escaping issue with embedded notebooks containing `$` in their Markdown.

package/src/common/update-html-dependencies.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -820,63 +820,63 @@ async function updateBootstrapFromBslib(
820820
for (let line of varContents) {
821821
line = line.replaceAll(
822822
"var(--#{$prefix}font-sans-serif)",
823-
"$font-family-sans-serif"
823+
"$$font-family-sans-serif"
824824
);
825825
line = line.replaceAll(
826826
"var(--#{$prefix}font-monospace)",
827-
"$font-family-monospace"
827+
"$$font-family-monospace"
828828
);
829829
line = line.replaceAll(
830830
"var(--#{$prefix}success-rgb)",
831-
"$success"
831+
"$$success"
832832
);
833833
line = line.replaceAll(
834834
"var(--#{$prefix}danger-rgb)",
835-
"$danger"
835+
"$$danger"
836836
);
837837
line = line.replaceAll(
838838
"var(--#{$prefix}body-color-rgb)",
839-
"$body-color"
839+
"$$body-color"
840840
);
841841
line = line.replaceAll(
842842
"var(--#{$prefix}body-bg-rgb)",
843-
"$body-bg"
843+
"$$body-bg"
844844
);
845845
line = line.replaceAll(
846846
"var(--#{$prefix}emphasis-color-rgb)",
847-
"$body-emphasis-color"
847+
"$$body-emphasis-color"
848848
);
849849
line = line.replaceAll(
850850
/RGBA?\(var\(--#\{\$prefix\}emphasis-color-rgb,(.*?)\).*?\)/gm,
851-
"$body-emphasis-color"
851+
"$$body-emphasis-color"
852852
);
853853
line = line.replaceAll(
854854
"var(--#{$prefix}secondary-color)",
855-
"$body-secondary-color"
855+
"$$body-secondary-color"
856856
);
857857
line = line.replaceAll(
858858
"var(--#{$prefix}secondary-bg)",
859-
"$body-secondary-bg"
859+
"$$body-secondary-bg"
860860
);
861861
line = line.replaceAll(
862862
"var(--#{$prefix}tertiary-bg)",
863-
"$body-tertiary-bg"
863+
"$$body-tertiary-bg"
864864
);
865865
line = line.replaceAll(
866866
"var(--#{$prefix}tertiary-color)",
867867
"$body-tertiary-color"
868868
);
869869
line = line.replaceAll(
870870
"var(--#{$prefix}emphasis-bg)",
871-
"$body-emphasis-bg"
871+
"$$body-emphasis-bg"
872872
);
873873
line = line.replaceAll(
874874
"var(--#{$prefix}emphasis-color)",
875-
"$body-emphasis-color"
875+
"$$body-emphasis-color"
876876
);
877877
line = line.replaceAll(
878878
"$emphasis-color-rgb",
879-
"$body-emphasis-color"
879+
"$$body-emphasis-color"
880880
);
881881

882882
line = line.replaceAll(/var\(--#\{\$prefix\}(.*?)\)/gm, "$$$1");

src/core/handlers/embed.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
/*
2-
* embed.ts
3-
*
4-
* Copyright (C) 2022 by Posit, PBC
5-
*
6-
*/
2+
* embed.ts
3+
*
4+
* Copyright (C) 2022-2025 by Posit, PBC
5+
*/
76

87
import { LanguageCellHandlerContext, LanguageHandler } from "./types.ts";
98
import { baseHandler, install } from "./base.ts";

src/core/handlers/mermaid.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ mermaid.initialize(${JSON.stringify(mermaidOpts)});
258258
const oldId = svg.getAttribute("id") as string;
259259
svg.setAttribute("id", newMermaidId);
260260
const style = svg.querySelector("style")!;
261-
style.innerHTML = style.innerHTML.replaceAll(oldId, newMermaidId);
261+
style.innerHTML = style.innerHTML.replaceAll(oldId, () => newMermaidId);
262262

263263
for (const defNode of svg.querySelectorAll("defs")) {
264264
const defEl = defNode as Element;
@@ -296,11 +296,11 @@ mermaid.initialize(${JSON.stringify(mermaidOpts)});
296296
// this string substitution is fraught, but I don't know how else to fix the problem.
297297
oldSvgSrc = oldSvgSrc.replaceAll(
298298
`"${idToPatch}"`,
299-
`"${to}"`,
299+
() => `"${to}"`,
300300
);
301301
oldSvgSrc = oldSvgSrc.replaceAll(
302302
`#${idToPatch}`,
303-
`#${to}`,
303+
() => `#${to}`,
304304
);
305305
}
306306
svg = mappedDiff(svg, oldSvgSrc);

src/core/jupyter/jupyter-embed.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,14 @@ export async function replaceNotebookPlaceholders(
339339
}
340340

341341
// Replace the placeholders with the rendered markdown
342-
markdown = markdown.replaceAll(match[0], nbMarkdown || "");
342+
markdown = markdown.replaceAll(
343+
match[0],
344+
// https://github.com/quarto-dev/quarto-cli/issues/12853
345+
// we use a function here to avoid
346+
// escaping issues with $ in the markdown
347+
// (e.g. $x$ in math mode)
348+
() => nbMarkdown ?? "",
349+
);
343350
}
344351
match = regex.exec(markdown);
345352
}

src/core/jupyter/jupyter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ export function mdFromContentCell(
10391039
for (let i = 0; i < source.length; i++) {
10401040
source[i] = source[i].replaceAll(
10411041
`attachment:${file}`,
1042-
imageFile,
1042+
() => imageFile,
10431043
);
10441044
}
10451045
// only process one supported mime type

src/execute/rmd.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ export const knitrEngine: ExecutionEngine = {
135135
options.quiet,
136136
// fixup .rmarkdown file references
137137
(output) => {
138-
output = output.replaceAll(`${inputStem}.rmarkdown`, inputBasename);
138+
output = output.replaceAll(
139+
`${inputStem}.rmarkdown`,
140+
() => inputBasename,
141+
);
139142

140143
const m = output.match(/^Quitting from lines (\d+)-(\d+)/m);
141144
if (m) {

src/format/jats/format-jats-postprocess.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const renderSubarticlePostProcessor = (
8888
// Replace the placeholder with the rendered subarticle
8989
outputContents = outputContents.replaceAll(
9090
placeholder,
91-
subArtLines.join("\n"),
91+
() => subArtLines.join("\n"),
9292
);
9393

9494
// Move supporting and resource files into place
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
share/python-wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# PyInstaller
30+
# Usually these files are written by a python script from a template
31+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
32+
*.manifest
33+
*.spec
34+
35+
# Installer logs
36+
pip-log.txt
37+
pip-delete-this-directory.txt
38+
39+
# Unit test / coverage reports
40+
htmlcov/
41+
.tox/
42+
.nox/
43+
.coverage
44+
.coverage.*
45+
.cache
46+
nosetests.xml
47+
coverage.xml
48+
*.cover
49+
*.py,cover
50+
.hypothesis/
51+
.pytest_cache/
52+
cover/
53+
54+
# Translations
55+
*.mo
56+
*.pot
57+
58+
# Django stuff:
59+
*.log
60+
local_settings.py
61+
db.sqlite3
62+
db.sqlite3-journal
63+
64+
# Flask stuff:
65+
instance/
66+
.webassets-cache
67+
68+
# Scrapy stuff:
69+
.scrapy
70+
71+
# Sphinx documentation
72+
docs/_build/
73+
74+
# PyBuilder
75+
.pybuilder/
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
.jupyter_cache
81+
82+
# IPython
83+
profile_default/
84+
ipython_config.py
85+
86+
# pyenv
87+
# For a library or package, you might want to ignore these files since the code is
88+
# intended to run in multiple environments; otherwise, check them in:
89+
# .python-version
90+
91+
# pipenv
92+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
94+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
95+
# install all needed dependencies.
96+
#Pipfile.lock
97+
98+
# UV
99+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100+
# This is especially recommended for binary packages to ensure reproducibility, and is more
101+
# commonly ignored for libraries.
102+
#uv.lock
103+
104+
# poetry
105+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106+
# This is especially recommended for binary packages to ensure reproducibility, and is more
107+
# commonly ignored for libraries.
108+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109+
#poetry.lock
110+
111+
# pdm
112+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113+
#pdm.lock
114+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
115+
# in version control.
116+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
117+
.pdm.toml
118+
.pdm-python
119+
.pdm-build/
120+
121+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
122+
__pypackages__/
123+
124+
# Celery stuff
125+
celerybeat-schedule
126+
celerybeat.pid
127+
128+
# SageMath parsed files
129+
*.sage.py
130+
131+
# Environments
132+
.env
133+
.venv
134+
env/
135+
venv/
136+
ENV/
137+
env.bak/
138+
venv.bak/
139+
140+
# Spyder project settings
141+
.spyderproject
142+
.spyproject
143+
144+
# Rope project settings
145+
.ropeproject
146+
147+
# mkdocs documentation
148+
/site
149+
150+
# mypy
151+
.mypy_cache/
152+
.dmypy.json
153+
dmypy.json
154+
155+
# Pyre type checker
156+
.pyre/
157+
158+
# pytype static type analyzer
159+
.pytype/
160+
161+
# Cython debug symbols
162+
cython_debug/
163+
164+
# PyCharm
165+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
166+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
167+
# and can be added to the global gitignore or merged into this file. For a more nuclear
168+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
169+
#.idea/
170+
171+
# Ruff stuff:
172+
.ruff_cache/
173+
174+
# PyPI configuration file
175+
.pypirc
176+
177+
# MACOS
178+
.DS_Store
179+
180+
# Files generated by invoking Julia with --code-coverage
181+
*.jl.cov
182+
*.jl.*.cov
183+
*.jl.mem
184+
185+
Manifest.toml
186+
187+
.vscode
188+
189+
# System-specific files and directories generated by the BinaryProvider and BinDeps packages
190+
# They contain absolute paths specific to the host computer, and so should not be committed
191+
deps/deps.jl
192+
deps/build.log
193+
deps/downloads/
194+
deps/usr/
195+
deps/src/
196+
197+
# these are all temporary files that will be generated during Quarto render
198+
/.quarto/
199+
/_freeze/
200+
/_site/
201+
/_output/
202+
/_book/
203+
/index_files/
204+
*.embed_files/
205+
*.html
206+
*.gif
207+
*.json
208+
/site_libs/
209+
_tmp_fig.svg
210+
211+
# Files generated from Binder
212+
.TinyTeX
213+
.code-server
214+
.config
215+
.local
216+
.yarn
217+
.jupyter-server-log.txt
218+
.profile
219+
.bashrc
220+
.bash_logout
221+
.wget-hsts

0 commit comments

Comments
 (0)