Skip to content

Commit 4b29a10

Browse files
authored
Support Quarto 2's div syntax for definition lists (#15140)
A `.definition-list` div holding one bullet list of terms, each followed by a nested bullet list of definitions, now renders as a DefinitionList. Quarto 2 uses this syntax in place of Pandoc's `term` / `: definition` form, so authors can adopt it before upgrading to Quarto 2. Divs with any other shape are left untouched, and the div's attributes are dropped because DefinitionList has no Attr. - Add a `definitionlist` module, registered in `_quarto.modules` - Run it as an astpipeline pass right after the list-table pass, so it runs before crossrefs and user filters See #13528 for the list-table precedent See quarto-dev/quarto-web#2305 for the documentation
1 parent b74bec2 commit 4b29a10

5 files changed

Lines changed: 195 additions & 0 deletions

File tree

‎news/changelog-1.11.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,4 @@ All changes included in 1.11:
185185
- ([#14878](https://github.com/quarto-dev/quarto-cli/pull/14878)): Add `az` (Azerbaijani) language translation. (author: @abdanar)
186186
- ([#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)
187187
- ([#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)
188+
- ([#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.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
-- definitionlist.lua
2+
-- Copyright (C) 2026 Posit Software, PBC
3+
4+
-- Div-based definition lists, the syntax Quarto 2 uses in place of
5+
-- Pandoc's `Term\n: Definition` syntax:
6+
--
7+
-- ::: {.definition-list}
8+
-- * Term 1
9+
-- - Definition 1a
10+
-- - Definition 1b
11+
-- * Term 2
12+
-- - Definition 2
13+
-- :::
14+
--
15+
-- A div with the `definition-list` class becomes a DefinitionList when
16+
-- it contains exactly one bullet list, and each item of that list holds
17+
-- exactly two blocks: the term (a Plain or Para), and a nested bullet
18+
-- list whose items are the term's definitions.
19+
--
20+
-- This matches Quarto 2's rules (`is_valid_definition_list_div` and
21+
-- `transform_definition_list_div` in pampa): a div that doesn't have this
22+
-- shape is left alone, and the div's own attributes are dropped because
23+
-- Pandoc's DefinitionList has no Attr.
24+
25+
-- Skip data-pos Divs inserted by the sourcepos extension
26+
local function block_skip_data_pos(block)
27+
if (block.t == "Div" and block.attr.attributes["data-pos"]) then
28+
block = block.content[1]
29+
end
30+
return block
31+
end
32+
33+
local function blocks_skip_data_pos(blocks)
34+
local new_blocks = pandoc.Blocks({})
35+
for _, block in ipairs(blocks) do
36+
new_blocks:insert(block_skip_data_pos(block))
37+
end
38+
return new_blocks
39+
end
40+
41+
-- Returns the outer bullet list when `div` has the definition-list
42+
-- shape, nil otherwise.
43+
local function definition_list_shape(div)
44+
local content = blocks_skip_data_pos(div.content)
45+
if #content ~= 1 or content[1] == nil or content[1].t ~= "BulletList" then
46+
return nil
47+
end
48+
local list = content[1]
49+
for _, item in ipairs(list.content) do
50+
local blocks = blocks_skip_data_pos(item)
51+
if #blocks ~= 2 then
52+
return nil
53+
end
54+
local term = blocks[1]
55+
if term == nil or (term.t ~= "Plain" and term.t ~= "Para") then
56+
return nil
57+
end
58+
if blocks[2] == nil or blocks[2].t ~= "BulletList" then
59+
return nil
60+
end
61+
end
62+
return list
63+
end
64+
65+
local function process(div)
66+
if not div.attr.classes:includes("definition-list") then
67+
return nil
68+
end
69+
local list = definition_list_shape(div)
70+
if list == nil then
71+
return nil
72+
end
73+
74+
local items = pandoc.List({})
75+
for _, item in ipairs(list.content) do
76+
local blocks = blocks_skip_data_pos(item)
77+
local term = blocks[1].content
78+
local definitions = pandoc.List({})
79+
for _, definition in ipairs(blocks[2].content) do
80+
definitions:insert(blocks_skip_data_pos(definition))
81+
end
82+
items:insert({term, definitions})
83+
end
84+
return pandoc.DefinitionList(items)
85+
end
86+
87+
return {
88+
definition_list_filter = function()
89+
return {Div = process}
90+
end
91+
}

‎src/resources/filters/modules/import_all.lua‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ _quarto.modules = {
2424
theorems = require("modules/theorems"),
2525
typst = require("modules/typst"),
2626
listtable = require("modules/listtable"),
27+
definitionlist = require("modules/definitionlist"),
2728
tableutils = require("modules/tableutils"),
2829
}
2930

‎src/resources/filters/normalize/astpipeline.lua‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,11 @@ function quarto_ast_pipeline()
318318
filter = _quarto.modules.listtable.list_table_filter(),
319319
traverser = 'jog',
320320
},
321+
322+
{ name = "astpipeline-process-definition-lists",
323+
filter = _quarto.modules.definitionlist.definition_list_filter(),
324+
traverser = 'jog',
325+
},
321326

322327
{ name = "astpipeline-process-tables",
323328
filter = astpipeline_process_tables(),
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
title: Div syntax for definition lists
3+
format:
4+
html: default
5+
latex: default
6+
typst:
7+
keep-typ: true
8+
_quarto:
9+
tests:
10+
html:
11+
ensureHtmlElements:
12+
- - "div#tight > dl > dt > code"
13+
- "div#loose > dl > dd > p"
14+
- "div#nested > dl > dd > dl > dt"
15+
- "div#list-table-in-definition > dl > dd > table"
16+
- "div#not-a-list.definition-list > p"
17+
- "div#no-definitions.definition-list > ul"
18+
- - "div#tight div.definition-list"
19+
- "div#loose div.definition-list"
20+
- "div#nested div.definition-list"
21+
- "div#list-table-in-definition div.list-table"
22+
latex:
23+
ensureFileRegexMatches:
24+
- - '\\begin\{description\}'
25+
- '\\item\[\\texttt\{time\}\]'
26+
- '\\item\[Outer term\]'
27+
- '\\item\[Table term\][\s\S]*?\\begin\{longtable\}'
28+
- []
29+
typst:
30+
ensureTypstFileRegexMatches:
31+
- - '/ Loose term: #block\['
32+
- '/ Inner term: #block\['
33+
- '/ Table term: #block\[\s*#table\('
34+
- []
35+
---
36+
37+
The `.definition-list` div is the definition list syntax of Quarto 2.
38+
Each item of the outer bullet list is a term, and each item of its
39+
nested bullet list is one of that term's definitions.
40+
41+
The `id`s below are only for the tests. A `DefinitionList` has no
42+
attributes, so the div's `id` is dropped, and the tests locate each list
43+
by the wrapper div added around it.
44+
45+
::: {#tight}
46+
::: {.definition-list}
47+
* `time`
48+
- A timestamp that identifies when the request was initiated.
49+
- Type: string
50+
* `id`
51+
- A unique identifier.
52+
:::
53+
:::
54+
55+
::: {#loose}
56+
::: {.definition-list}
57+
* Loose term
58+
59+
- A definition with two paragraphs.
60+
61+
The second paragraph.
62+
63+
- Another definition.
64+
:::
65+
:::
66+
67+
::: {#nested}
68+
::: {.definition-list}
69+
* Outer term
70+
- ::: {.definition-list}
71+
* Inner term
72+
- Inner definition.
73+
:::
74+
:::
75+
:::
76+
77+
::: {#list-table-in-definition}
78+
::: {.definition-list}
79+
* Table term
80+
- ::: {.list-table}
81+
* - Fruit
82+
- Price
83+
* - Apple
84+
- 1.20
85+
:::
86+
:::
87+
:::
88+
89+
A div that doesn't have the definition-list shape is left as is.
90+
91+
::: {#not-a-list .definition-list}
92+
Not a list.
93+
:::
94+
95+
::: {#no-definitions .definition-list}
96+
* A term without a nested list of definitions.
97+
:::

0 commit comments

Comments
 (0)