Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion news/changelog-1.10.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ All changes included in 1.10:

## Other fixes and improvements

- ([#6092](https://github.com/quarto-dev/quarto-cli/issues/6092)): Fix the `default-image-extension` being appended to an extensionless URL ending in a slash (e.g. `![](https://example.com/)`), which broke the embed/iframe image syntax.
- ([#6651](https://github.com/quarto-dev/quarto-cli/issues/6651)): Fix dart-sass compilation failing in enterprise environments where `.bat` files are blocked by group policy.
- ([#14255](https://github.com/quarto-dev/quarto-cli/issues/14255)): Fix shortcodes inside inline and display math expressions not being resolved.
- ([#14342](https://github.com/quarto-dev/quarto-cli/issues/14342)): Work around TOCTOU race in Deno's `expandGlobSync` that can cause unexpected exceptions to be raised while traversing directories during project initialization.
Expand All @@ -98,4 +99,5 @@ All changes included in 1.10:
- ([#14472](https://github.com/quarto-dev/quarto-cli/issues/14472)): Add support for Kotlin in code annotations and YAML cell options. (author: @barendgehrels)
- ([#14529](https://github.com/quarto-dev/quarto-cli/issues/14529)): Fix bundled Julia engine path leaking into rendered YAML metadata and pandoc log output when running an installed Quarto. The internal subtree-engine filter only matched the source-tree share-path layout (`resources/extension-subtrees/`) and missed installed layouts where the path is `share/extension-subtrees/`.
- ([#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.
- ([#14595](https://github.com/quarto-dev/quarto-cli/issues/14595)): Fix reload preview in code-server environment.
- ([#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
22 changes: 22 additions & 0 deletions src/resources/filters/customnodes/shortcodes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,29 @@ function shortcodes_filter()
end,
Image = function(el)
el = attr_handler(el)
local before = el.src
el.src = apply_code_shortcode(el.src)
-- A shortcode used as an image source is parsed without an extension,
-- so Pandoc appends default_image_extension to it. Once the shortcode
-- resolves to a path that already carries its own extension, that
-- appended extension is spurious and doubles up -- e.g. diagram.png.png,
-- or diagram.png.svg in Typst where the default differs (#14583). Strip
-- the appended default extension, but only when the resolved path still
-- has an extension of its own, so the multi-format workflow (a shortcode
-- resolving to an extensionless path) keeps its appended extension.
if el.src ~= before then
local ext = PANDOC_READER_OPTIONS.default_image_extension
if ext and ext ~= "" then
local suffix = "." .. ext
if el.src:sub(-#suffix) == suffix then
local candidate = el.src:sub(1, -#suffix - 1)
local _, candidate_ext = pandoc.path.split_extension(candidate)
if candidate_ext ~= "" then
el.src = candidate
end
end
end
end
return el
end,
Link = function(el)
Expand Down
28 changes: 24 additions & 4 deletions src/resources/filters/normalize/fixupdatauri.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,36 @@
-- Copyright (C) 2023 Posit Software, PBC

-- https://github.com/quarto-dev/quarto-cli/issues/6568
function normalize_fixup_data_uri_image_extension()
-- https://github.com/quarto-dev/quarto-cli/issues/6092
function normalize_fixup_data_uri_image_extension()
return {
Image = function(img)
local ext = PANDOC_READER_OPTIONS.default_image_extension
-- Nothing was appended when there is no default extension.
if not ext or ext == "" then
return nil
end
local src = img.src
local suffix = "." .. ext

-- Data URIs never need an extension, but Pandoc appends one anyway (#6568).
if src:sub(1, 5) == "data:" then
local l = PANDOC_READER_OPTIONS.default_image_extension:len()
if src:sub(-l-1) == ("." .. PANDOC_READER_OPTIONS.default_image_extension) then
img.src = src:sub(1, -l - 2)
if src:sub(-#suffix) == suffix then
img.src = src:sub(1, -#suffix - 1)
return img
end
return nil
end

-- An http(s) URL whose path ends with a slash has no filename, so the
-- appended default extension is provably spurious -- e.g.
-- https://example.com/ is parsed as https://example.com/.png (#6092).
-- A URL with a non-empty last segment is indistinguishable from a real
-- file of that extension, so it is deliberately left untouched.
if src:sub(1, 4) == "http" and src:match("^https?://")
and src:sub(-#suffix - 1) == "/" .. suffix then
img.src = src:sub(1, -#suffix - 1)
return img
end
end
}
Expand Down
31 changes: 31 additions & 0 deletions tests/docs/smoke-all/2026/06/29/14583-custom-ext.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: default-image-extension custom value
logo: assets/diagram.png
default-image-extension: jpg
format: html
_quarto:
tests:
html:
ensureFileRegexMatches:
-
# Both fixes must read the configured extension, not a hardcoded "png".
# Fix A strips the appended .jpg off a whole-shortcode source
- 'src="assets/diagram\.png"'
# Local file gets the configured .jpg appended (multi-format workflow)
- 'src="images/screenshot\.jpg"'
# Fix B strips the appended .jpg off the empty-filename URL
- 'src="https://example\.com/"'
-
# Fix B must have stripped the spurious .jpg from the URL
- 'src="https://example\.com/\.jpg"'
# Fix A must not leave a doubled extension on the shortcode
- 'diagram\.png\.jpg'
---

A non-default extension must flow through both fixes.

![shortcode]({{< meta logo >}})

![local](images/screenshot)

![empty url](https://example.com/)
30 changes: 30 additions & 0 deletions tests/docs/smoke-all/2026/06/29/14583-explicit-empty.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: default-image-extension explicitly empty
logo: assets/diagram.png
default-image-extension: ""
format: html
_quarto:
tests:
html:
ensureFileRegexMatches:
-
# With an empty default, Pandoc appends nothing, so the fix is a no-op
# and a local file without extension stays extensionless (user's choice)
- 'src="images/screenshot"'
# Shortcode resolves to its own value untouched
- 'src="assets/diagram\.png"'
# URL stays exactly as authored
- 'src="https://example\.com/"'
-
# No extension was appended, so none should appear
- 'src="images/screenshot\.'
- 'diagram\.png\.png'
---

User explicitly disables the default extension. Behavior must be unchanged.

![shortcode]({{< meta logo >}})

![local](images/screenshot)

![empty url](https://example.com/)
22 changes: 22 additions & 0 deletions tests/docs/smoke-all/2026/06/29/14583-typst.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: default-image-extension shortcode in Typst
withext: assets/diagram.png
format: typst
keep-typ: true
_quarto:
tests:
typst:
ensureTypstFileRegexMatches:
-
# Typst's default-image-extension is svg. A shortcode resolving to a
# .png path becomes .png.svg; the spurious .svg must be stripped,
# leaving the real .png (issue #14583, mismatched-extension case).
- 'image\("assets/diagram\.png"\)'
-
- 'image\("assets/diagram\.png\.svg"\)'
---

Shortcode resolving to a `.png` path in a Typst document (default extension
`svg`) must not end up as `.png.svg`:

![diagram]({{< meta withext >}})
82 changes: 82 additions & 0 deletions tests/docs/smoke-all/2026/06/29/14583.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: default-image-extension spurious append
logo: assets/diagram.png
imgdir: assets
noext: figures/plot
svgfile: figures/chart.svg
format: html
_quarto:
tests:
html:
ensureFileRegexMatches:
-
# #14583: a shortcode resolving to a path that already has an extension
# must not end up with a doubled extension
- 'src="assets/diagram\.png"'
# The doubled extension may be mismatched: a shortcode resolving to a
# .svg path in an HTML doc (default png) becomes .svg.png; the spurious
# .png must be stripped, leaving the real .svg
- 'src="figures/chart\.svg"'
# A shortcode resolving to an extensionless path keeps the appended
# default extension (the multi-format workflow)
- 'src="figures/plot\.png"'
# Fix B: provably-spurious empty-filename URL has the extension stripped
- 'src="https://example\.com/"'
# Guard: local file without extension still gets default appended
- 'src="images/screenshot\.png"'
# Guard: user-typed URL with real extension is left unchanged
- 'src="https://example\.com/logo\.png"'
# Guard: data URI is left intact (no spurious extension)
- 'src="data:image/png;base64,[^"]+"'
# Guard: a shortcode mid-path with a real extension keeps that extension
- 'src="assets/logo\.png"'
-
# No doubled extension survives the shortcode cases
- 'diagram\.png\.png'
- 'chart\.svg\.png'
# Empty-filename URL must not be left with the appended extension
- 'src="https://example\.com/\.png"'
# Narrow URL fix must not strip a real extension off a user-typed URL
- 'src="https://example\.com/logo"'
# Mid-path shortcode source must not have its real extension stripped
- 'src="assets/logo"'
# Data URI must not have a spurious extension appended
- 'base64,[^"]*\.png"'
---

Shortcode resolving to an extensioned path — must not double (issue #14583):

![shortcode]({{< meta logo >}})

Shortcode resolving to a path whose extension differs from the format default
(here `.svg` in an HTML doc): the spurious default extension must be stripped:

![mismatched ext]({{< meta svgfile >}})

Shortcode resolving to an extensionless path — multi-format append is kept:

![extensionless]({{< meta noext >}})

User explicitly appends the default extension to a shortcode — preserved:

![user explicit]({{< meta noext >}}.png)

Empty-filename URL — embed/iframe syntax (issue #6092):

![empty url](https://example.com/)

Local file without extension — multi-format workflow must keep the append:

![local](images/screenshot)

User-typed URL with a real extension must stay untouched:

![user png](https://example.com/logo.png)

Data URI regression guard:

![data uri](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==)

Shortcode mid-path with a real extension must keep the extension:

![mid-path]({{< meta imgdir >}}/logo.png)
Binary file added tests/docs/smoke-all/2026/06/29/assets/diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading