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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- feat: `get_project_metadata` returns the `QUARTO_EXECUTE_INFO` path as a third value with each reason, so a caller can name the file without reading the variable again. Its four reasons are now a stable API that callers can compare against. (#21)

## 2.7.0 (2026-10-29)

- feat: Add `get_project_metadata`, which returns the project's `format.metadata` table from the `QUARTO_EXECUTE_INFO` file. When the file is set but can't be used, it returns `nil` and a short reason, so each extension can stay silent or warn in its own words. (#19)
Expand Down
17 changes: 11 additions & 6 deletions modules/metadata.lua
Original file line number Diff line number Diff line change
Expand Up @@ -195,28 +195,33 @@ end
--- The file is read on every call; pass the result on rather than calling twice.
--- @return table|nil format_meta The format.metadata table, or nil if unavailable
--- @return string|nil reason Why the file could not be used when
--- QUARTO_EXECUTE_INFO is set; nil when it is unset or empty
--- @usage local format_meta, reason = M.get_project_metadata()
--- QUARTO_EXECUTE_INFO is set; nil when it is unset or empty.
--- The reasons are a stable API: callers can compare against them, and a
--- change to any of them is a breaking change. They are exactly
--- "file is unreadable", "file is empty", "invalid JSON" and
--- "no format.metadata table".
--- @return string|nil path The QUARTO_EXECUTE_INFO path, with every reason
--- @usage local format_meta, reason, path = M.get_project_metadata()
--- @note quarto-cli's decoder raises on bad JSON, while q2's (pandoc.json)
--- returns nil, so a nil result is also reported as invalid JSON.
function M.get_project_metadata()
local path = os.getenv("QUARTO_EXECUTE_INFO")
if str.is_empty(path) then return nil end

local file = io.open(path, "r")
if not file then return nil, "file is unreadable" end
if not file then return nil, "file is unreadable", path end

local content = file:read("*a")
file:close()

if str.is_empty(content) then return nil, "file is empty" end
if str.is_empty(content) then return nil, "file is empty", path end

local ok, info = pcall(quarto.json.decode, content)
if not ok or info == nil then return nil, "invalid JSON" end
if not ok or info == nil then return nil, "invalid JSON", path end

if type(info) ~= "table" or type(info["format"]) ~= "table"
or type(info["format"]["metadata"]) ~= "table" then
return nil, "no format.metadata table"
return nil, "no format.metadata table", path
end

return info["format"]["metadata"]
Expand Down
16 changes: 9 additions & 7 deletions tests/run.lua
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ do
end
return previous_getenv(name)
end
fn()
fn(value)
end

--- Point QUARTO_EXECUTE_INFO at a file holding `content`, or at a path that
Expand All @@ -394,16 +394,18 @@ do
end)
end

local function reports(description, expected_reason)
local ok, format_meta, reason = pcall(metadata.get_project_metadata)
check(ok and format_meta == nil and reason == expected_reason, description,
ok and string.format('expected nil, %s; got %s, %s',
tostring(expected_reason), tostring(format_meta), tostring(reason))
local function reports(description, expected_reason, expected_path)
local ok, format_meta, reason, path = pcall(metadata.get_project_metadata)
check(ok and format_meta == nil and reason == expected_reason and path == expected_path,
description,
ok and string.format('expected nil, %s, %s; got %s, %s, %s',
tostring(expected_reason), tostring(expected_path),
tostring(format_meta), tostring(reason), tostring(path))
or ('raised: ' .. tostring(format_meta)))
end

local function reports_for(content, description, expected_reason)
with_execute_info(content, function() reports(description, expected_reason) end)
with_execute_info(content, function(path) reports(description, expected_reason, path) end)
end

local function returns_blocks(content, description, predicate)
Expand Down