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
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,4 @@ All changes included in 1.11:
- ([#14878](https://github.com/quarto-dev/quarto-cli/pull/14878)): Add `az` (Azerbaijani) language translation. (author: @abdanar)
- ([#14983](https://github.com/quarto-dev/quarto-cli/issues/14983)): Fix `quarto render` treating a parent folder as a Hugo project when its `config/` folder can't be entered, which silently wrote `hugo-md` output or failed with a permission error. (author: @pinin4fjords)
- ([#15097](https://github.com/quarto-dev/quarto-cli/pull/15097)): Fix the `.content-hidden unless-profile` attribute completion inserting invalid syntax (its `=` was missing), and typos in three attribute completion descriptions. (author: @rkskmt)
- ([#15140](https://github.com/quarto-dev/quarto-cli/pull/15140)): Add support for definition lists written as a `definition-list` div holding a nested bullet list, the definition list syntax of Quarto 2.
91 changes: 91 additions & 0 deletions src/resources/filters/modules/definitionlist.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
-- definitionlist.lua
-- Copyright (C) 2026 Posit Software, PBC

-- Div-based definition lists, the syntax Quarto 2 uses in place of
-- Pandoc's `Term\n: Definition` syntax:
--
-- ::: {.definition-list}
-- * Term 1
-- - Definition 1a
-- - Definition 1b
-- * Term 2
-- - Definition 2
-- :::
--
-- A div with the `definition-list` class becomes a DefinitionList when
-- it contains exactly one bullet list, and each item of that list holds
-- exactly two blocks: the term (a Plain or Para), and a nested bullet
-- list whose items are the term's definitions.
--
-- This matches Quarto 2's rules (`is_valid_definition_list_div` and
-- `transform_definition_list_div` in pampa): a div that doesn't have this
-- shape is left alone, and the div's own attributes are dropped because
-- Pandoc's DefinitionList has no Attr.

-- Skip data-pos Divs inserted by the sourcepos extension
local function block_skip_data_pos(block)
if (block.t == "Div" and block.attr.attributes["data-pos"]) then
block = block.content[1]
end
return block
end

local function blocks_skip_data_pos(blocks)
local new_blocks = pandoc.Blocks({})
for _, block in ipairs(blocks) do
new_blocks:insert(block_skip_data_pos(block))
end
return new_blocks
end

-- Returns the outer bullet list when `div` has the definition-list
-- shape, nil otherwise.
local function definition_list_shape(div)
local content = blocks_skip_data_pos(div.content)
if #content ~= 1 or content[1] == nil or content[1].t ~= "BulletList" then
return nil
end
local list = content[1]
for _, item in ipairs(list.content) do
local blocks = blocks_skip_data_pos(item)
if #blocks ~= 2 then
return nil
end
local term = blocks[1]
if term == nil or (term.t ~= "Plain" and term.t ~= "Para") then
return nil
end
if blocks[2] == nil or blocks[2].t ~= "BulletList" then
return nil
end
end
return list
end

local function process(div)
if not div.attr.classes:includes("definition-list") then
return nil
end
local list = definition_list_shape(div)
if list == nil then
return nil
end

local items = pandoc.List({})
for _, item in ipairs(list.content) do
local blocks = blocks_skip_data_pos(item)
local term = blocks[1].content
local definitions = pandoc.List({})
for _, definition in ipairs(blocks[2].content) do
definitions:insert(blocks_skip_data_pos(definition))
end
items:insert({term, definitions})
end
return pandoc.DefinitionList(items)
end

return {
definition_list_filter = function()
return {Div = process}
end
}
1 change: 1 addition & 0 deletions src/resources/filters/modules/import_all.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ _quarto.modules = {
theorems = require("modules/theorems"),
typst = require("modules/typst"),
listtable = require("modules/listtable"),
definitionlist = require("modules/definitionlist"),
tableutils = require("modules/tableutils"),
}

Expand Down
5 changes: 5 additions & 0 deletions src/resources/filters/normalize/astpipeline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ function quarto_ast_pipeline()
filter = _quarto.modules.listtable.list_table_filter(),
traverser = 'jog',
},

{ name = "astpipeline-process-definition-lists",
filter = _quarto.modules.definitionlist.definition_list_filter(),
traverser = 'jog',
},

{ name = "astpipeline-process-tables",
filter = astpipeline_process_tables(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: Div syntax for definition lists
format:
html: default
latex: default
typst:
keep-typ: true
_quarto:
tests:
html:
ensureHtmlElements:
- - "div#tight > dl > dt > code"
- "div#loose > dl > dd > p"
- "div#nested > dl > dd > dl > dt"
- "div#list-table-in-definition > dl > dd > table"
- "div#not-a-list.definition-list > p"
- "div#no-definitions.definition-list > ul"
- - "div#tight div.definition-list"
- "div#loose div.definition-list"
- "div#nested div.definition-list"
- "div#list-table-in-definition div.list-table"
latex:
ensureFileRegexMatches:
- - '\\begin\{description\}'
- '\\item\[\\texttt\{time\}\]'
- '\\item\[Outer term\]'
- '\\item\[Table term\][\s\S]*?\\begin\{longtable\}'
- []
typst:
ensureTypstFileRegexMatches:
- - '/ Loose term: #block\['
- '/ Inner term: #block\['
- '/ Table term: #block\[\s*#table\('
- []
---

The `.definition-list` div is the definition list syntax of Quarto 2.
Each item of the outer bullet list is a term, and each item of its
nested bullet list is one of that term's definitions.

The `id`s below are only for the tests. A `DefinitionList` has no
attributes, so the div's `id` is dropped, and the tests locate each list
by the wrapper div added around it.

::: {#tight}
::: {.definition-list}
* `time`
- A timestamp that identifies when the request was initiated.
- Type: string
* `id`
- A unique identifier.
:::
:::

::: {#loose}
::: {.definition-list}
* Loose term

- A definition with two paragraphs.

The second paragraph.

- Another definition.
:::
:::

::: {#nested}
::: {.definition-list}
* Outer term
- ::: {.definition-list}
* Inner term
- Inner definition.
:::
:::
:::

::: {#list-table-in-definition}
::: {.definition-list}
* Table term
- ::: {.list-table}
* - Fruit
- Price
* - Apple
- 1.20
:::
:::
:::

A div that doesn't have the definition-list shape is left as is.

::: {#not-a-list .definition-list}
Not a list.
:::

::: {#no-definitions .definition-list}
* A term without a nested list of definitions.
:::
Loading