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: 4 additions & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ All changes included in 1.11:
- ([PumasAI/quarto-julia-engine#11](https://github.com/PumasAI/quarto-julia-engine/pull/11)): Support `fig-format: retina`, normalized to `png` with doubled `fig-dpi` as in the `jupyter` and `knitr` engines.
- ([PumasAI/quarto-julia-engine#7](https://github.com/PumasAI/quarto-julia-engine/pull/7)): Support `execute-dir`, shared worker processes across notebooks with matching configs (`share_worker_process: true`).

## Lua API

- ([#14894](https://github.com/quarto-dev/quarto-cli/pull/14894)): Fix `quarto.version` and `quarto.config.version()` crashing filters with `table expected, got string` when the version string contains semver build metadata (such as a distro packager's revision suffix) or otherwise does not start with a digit.

## Other fixes and improvements

- ([#14775](https://github.com/quarto-dev/quarto-cli/issues/14775)): Fix a crash when the `QUARTO_R` environment variable is set to a malformed path. Quarto now warns and falls back to other R lookup methods.
Expand Down
9 changes: 6 additions & 3 deletions src/command/editor-support/crossref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { encodeBase64 } from "../../deno_ral/encoding.ts";
import { Command } from "cliffy/command/mod.ts";

import { execProcess } from "../../core/process.ts";
import { pandocBinaryPath, resourcePath } from "../../core/resources.ts";
import {
pandocBinaryPath,
pandocDataDirArgs,
resourcePath,
} from "../../core/resources.ts";
import { globalTempContext } from "../../core/temp.ts";

function parseCrossrefFlags(options: any, args: string[]): {
Expand Down Expand Up @@ -99,8 +103,7 @@ const makeCrossrefCommand = () => {
resourcePath("filters/qmd-reader.lua"),
"--to",
"native",
"--data-dir",
resourcePath("pandoc/datadir"),
...pandocDataDirArgs(),
"--lua-filter",
resourcePath("filters/quarto-init/quarto-init.lua"),
"--lua-filter",
Expand Down
8 changes: 6 additions & 2 deletions src/command/render/pandoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ import {
isQuartoMetadata,
metadataGetDeep,
} from "../../config/metadata.ts";
import { pandocBinaryPath, resourcePath } from "../../core/resources.ts";
import {
pandocBinaryPath,
pandocDataDirArgs,
resourcePath,
} from "../../core/resources.ts";
import { getAvailableTypstFonts } from "../../core/typst.ts";
import { filterBundledSubtreeEngines } from "../../extension/extension.ts";
import { pandocAutoIdentifier } from "../../core/pandoc/pandoc-id.ts";
Expand Down Expand Up @@ -1071,7 +1075,7 @@ export async function runPandoc(
pandocArgs,
dataDirArgs,
);
pandocArgs.push("--data-dir", resourcePath("pandoc/datadir"));
pandocArgs.push(...pandocDataDirArgs());

// add any built-in syntax definition files
allDefaults[kSyntaxDefinitions] = allDefaults[kSyntaxDefinitions] || [];
Expand Down
8 changes: 8 additions & 0 deletions src/core/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ export function pandocBinaryPath(): string {
return isWindows ? toolsPath("pandoc") : architectureToolsPath("pandoc");
}

// Args selecting Quarto's own pandoc data-dir (init.lua and friends under
// src/resources/pandoc/datadir), never the user's default pandoc data
// directory. Not every pandoc invocation uses these -- e.g. `quarto run`
// (src/core/run/lua.ts) intentionally omits --data-dir.
export function pandocDataDirArgs(): string[] {
return ["--data-dir", resourcePath("pandoc/datadir")];
}

const _r_binary_path: Map<string, string> = new Map();
export async function rBinaryPath(
binary: string,
Expand Down
33 changes: 31 additions & 2 deletions src/resources/pandoc/datadir/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -675,13 +675,42 @@ local function outputFile()
end
end

-- Returns the leading sequence of dot-separated numeric components.
-- Lua patterns cannot match an arbitrary number of repeated groups.
local function leadingDottedNumber(s)
local parts = {}
local i = 1
while true do
local numStart, numEnd = s:find('^%d+', i)
if not numStart then break end
table.insert(parts, s:sub(numStart, numEnd))
i = numEnd + 1
if s:sub(i, i) == '.' then
i = i + 1
else
break
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, '.')
end

local function version()
local versionString = param('quarto-version', 'unknown')
local success, versionObject = pcall(pandoc.types.Version, versionString)
-- pandoc.types.Version's dotted-integer parser rejects semver build
-- metadata (e.g. "1.9.13+test.20260910") and any string without a
-- leading digit. Extract the leading dot-separated numeric component so
-- that construction always succeeds and this always returns a Version
-- object -- callers (e.g. table.concat(quarto.version, '.')) must not
-- have to handle a plain string fallback.
local numericVersion = leadingDottedNumber(versionString) or '0'
local success, versionObject = pcall(pandoc.types.Version, numericVersion)
if success then
return versionObject
else
return versionString
return pandoc.types.Version('0')
end
end

Expand Down
101 changes: 101 additions & 0 deletions tests/smoke/lua-unit/quarto-version-repr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* quarto-version-repr.test.ts
*
* Regression test ensuring that quarto.version / quarto.config.version()
* always return a pandoc Version object, even when the `quarto-version`
* filter parameter contains semver build metadata or does not begin with a
* digit. Pandoc's dotted-integer Version parser rejects both forms. Such
* strings can reach quarto-version via QUARTO_FORCE_VERSION, a
* packager-appended revision suffix, or CI build-metadata stamping
* (test-smokes-built.yml).
*
* Unlike tests/smoke/lua-unit/lua-unit.test.ts, this does not go through
* `quarto run` -- that invokes pandoc without --data-dir, so init.lua from
* src/resources/pandoc/datadir/ (which defines quarto.version /
* quarto.config.version()) never loads. Instead this invokes pandoc
* directly with pandocDataDirArgs() pointing at that directory, forcing the
* quarto-version filter param via QUARTO_FILTER_PARAMS -- the same
* mechanism a real quarto render uses (src/command/render/filters.ts,
* src/command/render/pandoc.ts).
*/

import { encodeBase64 } from "encoding/base64";
import { fromFileUrl, join } from "../../../src/deno_ral/path.ts";
import { assert } from "testing/asserts";
import { execProcess } from "../../../src/core/process.ts";
import {
pandocBinaryPath,
pandocDataDirArgs,
resourcePath,
} from "../../../src/core/resources.ts";
import { unitTest } from "../../test.ts";

const testsDir = fromFileUrl(new URL("../../", import.meta.url));
const unitLuaDir = join(testsDir, "unit-lua");
const luaScript = join(unitLuaDir, "quarto-version-repr.test.lua");

// The `;;` at the end preserves the default search path.
const LUA_PATH = [join(unitLuaDir, "?.lua"), ""].join(";") + ";";

// A plain dotted-integer version parses fine under pandoc.types.Version --
// this case must keep working (regression guard for the normal case). The
// four-component case guards against a fixed-arity extraction silently
// truncating a version with more components than it hard-codes. The other
// two are strings pandoc.types.Version's parser rejects: a build-metadata
// suffix (as CI's test-smokes-built.yml stamps via QUARTO_FORCE_VERSION, and
// as a distro packager might append) and a version with no leading digit at
// all.
const VERSION_CASES: Record<string, { input: string; expected: string }> = {
"plain numeric version": { input: "1.9.13", expected: "1.9.13" },
"four-component version": { input: "1.2.3.4", expected: "1.2.3.4" },
"build-metadata suffixed version": {
input: "1.9.13+test.20260910",
expected: "1.9.13",
},
"non-numeric version": { input: "unknown", expected: "0" },
};

for (
const [label, { input: versionString, expected }] of Object.entries(
VERSION_CASES,
)
) {
unitTest(`quarto-version-repr > ${label}`, async () => {
const filterParams = encodeBase64(
JSON.stringify({
"quarto-version": versionString,
"expected-version": expected,
}),
);
const result = await execProcess(
{
cmd: pandocBinaryPath(),
args: [
...pandocDataDirArgs(),
"--from",
"markdown",
"--to",
"plain",
"--lua-filter",
luaScript,
],
env: {
LUA_PATH,
QUARTO_FILTER_PARAMS: filterParams,
QUARTO_SHARE_PATH: resourcePath(),
},
},
"test\n",
undefined,
undefined,
true, // forward child output directly to stdout/stderr
);
assert(
result.success,
`quarto.version / quarto.config.version() were not a Version object ` +
`for quarto-version="${versionString}" (exit ${result.code}):\n` +
`--- stdout ---\n${result.stdout ?? ""}\n` +
`--- stderr ---\n${result.stderr ?? ""}`,
);
});
}
53 changes: 53 additions & 0 deletions tests/unit-lua/quarto-version-repr.test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- Regression test for quarto.version / quarto.config.version() always
-- supporting table.concat(), even when the `quarto-version` filter param is
-- a string pandoc.types.Version's dotted-integer parser rejects (e.g. a
-- semver build-metadata suffix like "1.9.13+test.20260910", or an entirely
-- non-numeric string). Note: pandoc.types.Version is actually lenient about
-- a trailing "-suffix" (e.g. "1.9.13-1" parses fine, silently dropping the
-- suffix) -- the "+" character is what breaks its parser, along with any
-- string that doesn't start with a digit.
--
-- On successful parsing, pandoc.types.Version returns a userdata object
-- (not a plain Lua table) that nonetheless supports table.concat() via its
-- metatable -- so this test checks table.concat() succeeds, not literal
-- Lua `type()`.
--
-- Unlike the other tests in this directory, this one is not run through
-- `quarto run` (which invokes pandoc without --data-dir, so init.lua from
-- src/resources/pandoc/datadir/ never loads). It is invoked directly against
-- pandoc with --data-dir pointing at that directory, so the real init.lua
-- runs and populates the real `quarto` global -- see
-- tests/smoke/lua-unit/quarto-version-repr.test.ts, which sets
-- QUARTO_FILTER_PARAMS to force a specific `quarto-version` filter param.
--
-- Reproduces the exact call made by the `{{< version >}}` shortcode
-- (src/resources/extensions/quarto/version/version.lua).
local lu = require('luaunit')

TestQuartoVersionRepr = {}

-- Optional: when the `expected-version` filter param is set, assert the
-- concatenated value exactly, not just that concat() didn't crash. Catches
-- silent truncation (e.g. a fixed-arity extraction pattern dropping a 4th
-- dotted component) that a crash-only check would miss.
local expectedVersion = param('expected-version', nil)

function TestQuartoVersionRepr:testVersionConcatWorks()
local ok, result = pcall(table.concat, quarto.version, '.')
lu.assertTrue(ok, 'table.concat(quarto.version, ".") failed: ' .. tostring(result))
if expectedVersion then
lu.assertEquals(result, expectedVersion)
end
end

function TestQuartoVersionRepr:testConfigVersionConcatWorks()
local ok, result = pcall(table.concat, quarto.config.version(), '.')
lu.assertTrue(ok, 'table.concat(quarto.config.version(), ".") failed: ' .. tostring(result))
if expectedVersion then
lu.assertEquals(result, expectedVersion)
end
end

function Pandoc(doc)
os.exit(lu.LuaUnit.run())
end
Loading