diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 53b37f73b43..fd3d980f864 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -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. diff --git a/src/resources/filters/modules/definitionlist.lua b/src/resources/filters/modules/definitionlist.lua new file mode 100644 index 00000000000..8d661103e1e --- /dev/null +++ b/src/resources/filters/modules/definitionlist.lua @@ -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 +} diff --git a/src/resources/filters/modules/import_all.lua b/src/resources/filters/modules/import_all.lua index 78512ac6a15..4220af820e3 100644 --- a/src/resources/filters/modules/import_all.lua +++ b/src/resources/filters/modules/import_all.lua @@ -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"), } diff --git a/src/resources/filters/normalize/astpipeline.lua b/src/resources/filters/normalize/astpipeline.lua index 881d935dd98..11b148ded64 100644 --- a/src/resources/filters/normalize/astpipeline.lua +++ b/src/resources/filters/normalize/astpipeline.lua @@ -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(), diff --git a/tests/docs/smoke-all/markdown/definition-list/definition-list-div.qmd b/tests/docs/smoke-all/markdown/definition-list/definition-list-div.qmd new file mode 100644 index 00000000000..a750a0a6c89 --- /dev/null +++ b/tests/docs/smoke-all/markdown/definition-list/definition-list-div.qmd @@ -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. +:::