|
| 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 | +} |
0 commit comments