Labelled lists in Pandoc and Quarto

Filter to create labelled lists in Pandoc and Quarto.

Introduction

This filter provides custom labelled lists in Pandoc’s markdown for outputs in LaTeX/PDF, HTML and JATS XML. Instead of bullets or numbers, list items are given custom text labels. The text labels can include markdown formatting.

View the filter source on GitHub.

Installation

Plain pandoc

Get labelled-lists.lua from the Releases page and save it somewhere Pandoc can find (see PandocMan for details).

Pass the filter to Pandoc via the --lua-filter (or -L) command line option.

pandoc --lua-filter imagify.lua ...

Quarto

Install this filter as a Quarto extension with

quarto install extension dialoa/labelled-lists

and use it by adding labelled-lists to the filters entry in their YAML header:

---
filters:
- labelled-lists
---

See Quarto’s Extensions guide for more details on updating and version-controlling filters.

R Markdown

Use pandoc_args to invoke the filter. See the R Markdown Cookbook for details.

---
output:
  word_document:
    pandoc_args: ['--lua-filter=labelled-lists.lua']
---

Usage

Markdown syntax

A simple illustration of the custom label syntax:

* [Premise 1]{} This is the first claim.
* [Premise 2]{} This is the second claim.
* [Conclusion]{} This is the conclusion.

This generates the following list (process this file with the filter to see the result):

(Premise 1) This is the first claim.

(Premise 2) This is the second claim.

(Conclusion) This is the conclusion.

In general, the filter will turn a bullet list into a custom label list provided that every item starts with a Span element.

Customizing the label delimiters

By default the custom lable is put between two parentheses. You can change this globally by setting a delimiter key within a labelled-lists key in your document’s metadata.

labelled-lists:
  delimiter: )

Possible values:

This can be set for a specific list by using a delimiter attribute on the first span element of your list (same possible values as above):

* [Premise 1]{delimiter='**%1**'} This is the first claim.
* [Premise 2]{} This is the second claim.
* [Conclusion]{} This is the conclusion.

**Premise 1** This is the first claim.

**Premise 2** This is the second claim.

**Conclusion** This is the conclusion.

Cross-referencing custom-label items

Custom labels can be given internal identifiers. The syntax is [label]{#identifier}. In the list below, A1ref, A2ref and Cref identify the item:

* [**A1**]{#A1ref} This is the first claim.
* [A2]{#A2ref} This is the second claim.
* [*C*]{#Cref} This is the conclusion.

Note that # is not part of the identifier. Identifiers should start with a letter and contain only letters, digits, colons :, dots ., dashes - and underscores _.

Labels with identifiers can be crossreferenced using Pandoc’s citations or internal links.

Cross-referencing with citations

The basic syntax is:

You can crossrefer to several custom labels at a time: [@A1ref; @A2ref]. But mixing references to a custom label with bibliographic ones in a same citation won’t work: if Smith2003 is a key in your bibliography [@A1ref; Smith2003] will only output “(A1; Smith, 2003)”.

Because this syntax overlaps with Pandoc’s citation syntax, conflicts should be avoided:

Alternatively, the citation syntax for crossreferencing custom label items can be deactivated. See Customization below.

In Pandoc markdown internal links are created with the syntax [link text](#target_identifier). (Note the rounded brackets instead of curly ones for Span element identifiers.) You can use internal links to cross-refer to custom label items that have a identifier. If your link has no text, the label with its formatting will be printed out; otherwise whichever text you give for the link. For instance, given the custom label list above, the following:

The claim [](#A1ref) together with [the next claim](#A2ref) 
entail ([](#Cref)).

will output:

The claim A1 together with the next claim entail (C).

where the links point to the corresponding items in the list.

Adding am opposite side comment

You can add a side comment to any item, e.g. a citation, crossreference, longer label. This is done by placing a Span element with class side:

(MP) If it’s raining, the streets are wet. It’s raining. Therefore, the streets are wet.

(modus ponens)

(MT) If it’s raining, the streets are wet. The streets aren’t wet. Therefore, it’s not raining.

(modus tollens)

* [MP]{} If it's raining, the streets are wet. It's raining.
  Therefore, the streets are wet.
  [(*modus ponens*)]{.side}
* [MT]{} If it's raining, the streets are wet. The streets aren't wet.  Therefore, it's not raining.
  [(*modus tollens*)]{.side}

If an item consists of several blocks, the side comment must be a Span at the very end of the last block.

Side comments are only typeset in HTML and LaTeX. In other output formats, they appear at the end of the item’s text separated by a single space.

They are typeset slightly differently in HTML and LaTeX output. In HTML output, they appear next to the top of the item (by default, this can be changed with a custom CSS). In LaTeX, they appear at the right of the last line of the item.

Customization

Filter options can be specified in the document’s metadata (YAML block) as follows:

---
title: My document
author: John Doe
labelled-lists:
  disable-citations: true
  delimiter: Period

That is the metadata field labelled-lists contains the filter options as a map. Presently the filter has just one option:

Examples and tests

math formulas

(p1) This list uses

(p2) math formulas as labels.

LaTeX code

() This list uses

() latex code as labels.

Ignored: these are not treated as labels.

Small caps

(All) This list uses

(Some) latex code as labels.

List with Para items

(A1) F(x) > G(x)

(A2) G(x) > H(x)

items with several blocks

(B1) This list’s items

consist of several blocks

iFi > ∑iGi

(B2) Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec et massa ut eros volutpat gravida ut vel lacus. Proin turpis eros, imperdiet sed quam eget, bibendum aliquam massa. Phasellus pellentesque egestas dapibus. Proin porta tellus id orci consectetur bibendum. Nam eu cursus quam. Etiam vehicula in mi sed interdum. Duis rutrum eleifend consectetur. Phasellus ullamcorper, urna at vestibulum venenatis, tellus erat luctus nibh, eget hendrerit justo enim nec magna. Duis mollis ac felis ac tristique.

Pellentesque malesuada arcu ac orci scelerisque vulputate. Aenean at ex suscipit, ultricies tellus sit amet, luctus lectus. Duis ut viverra sapien. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras consequat nisi at ex finibus, in condimentum erat auctor. In at nulla at est iaculis pulvinar sed id diam. Cras malesuada sit amet tellus id molestie.

cross-reference with citation syntax

(B1) This is the first claim.

(B2) This is the second claim.

(D) This is the conclusion.

The claim B1 together with the claim B2 entail (D).

(A1) This is the first claim.

(A2) This is the second claim.

(C) This is the conclusion.

The claim A1 together with the claim A2 entail (C).

Details

LaTeX output

\begin{itemize}
\tightlist

\item[(Premise 1)] This is the first claim.

\item[(Premise 2)] This is the second claim.

\item[(Conclusion)] This is the conclusion.

\end{itemize}

With a side comment:

\begin{itemize}
\tightlist

\item[MP)] If it's raining, the streets are wet. It's raining.
Therefore, the streets are wet. \hfill modus ponens

\item[MT)] If it's raining, the streets are wet. The streets aren't wet.
Therefore, it's not raining. \hfill modus tollens

\end{itemize}

HTML output

HTML output is placed in a <div>.

Currently, the list is recreated as a set of paragraphs. Each item is a <p> if it’s one block long, a <div> if longer. The label itself is contained in a <span>.

<div class="labelled-lists-list">
  <p class="labelled-lists-item"><span class="labelled-lists-label">(Premise 1)</span> This is the first claim.</p>
  <p class="labelled-lists-item"><span class="labelled-lists-label">(Premise 2)</span> This is the second claim.</p>
  <div class="labelled-lists-item">
    <p><span class="labelled-lists-label">(<strong>Conclusion</strong>)</span> This third item consists of</p>
    <p>two blocks.</p>
  </div>
</div>

With side comments:

<div class="labelled-lists-list">
<div class="labelled-lists-item"
style="display:flex; flex-direction: row;">
<div>
<span class="labelled-lists-label">MP)</span> If it’s raining, the
streets are wet. It’s raining. Therefore, the streets are wet.
</div>
<p class="labelled-lists-side">modus ponens</p>
</div>
<div class="labelled-lists-item"
style="display:flex; flex-direction: row;">
<div>
<span class="labelled-lists-label">MT)</span> If it’s raining, the
streets are wet. The streets aren’t wet. Therefore, it’s not raining.
</div>
<p class="labelled-lists-side">modus tollens</p>
</div>
</div>

In the future, we’ll output a <ol> list within a Div:

<div class="labelled-lists-list">
  <ul>
  <li><span class="labelled-lists-label">(Premise 1) </span>This is the first claim.</li>
  <li><span class="labelled-lists-label">(Premise 1) </span>This is the first claim.</li>
  <li>
    <p><span class="labelled-lists-label">(<strong>Conclusion</strong>)</span> This third item consists of</p>
    <p>two blocks.</p>
  </li>
  </ul>
</div>

And style it via the CSS (see “Css” global variable in the code).

List structures

Example

example.md

---
title: Labelled lists examples
labelled-lists:
  delimiter: )
  disable-citations: false
---

# List labels and delimiters

Default delimiter format set to  "...)".

Labelled list

* [Premise 1]{} This is the first claim. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
* [Premise 2]{} This is the second claim. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 
* [Conclusion]{} This is the conclusion. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 

Setting the delimiter for an individual list

* [Label 1]{delimiter='**%1**'} This is the first item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
* [Label 2]{} This is the second item. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Empty list

* []{delimiter=''} This is the first item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
* []{} This is the second item. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

# Cross-referencing

Assigning identifiers to list items. Arbitrary markdown formatting on
labels will be preserved in crossreferencing.

* [**A1**]{#A1ref} This is the first claim.
* [A2]{#A2ref} This is the second claim.
* [*C*]{#Cref} This is the conclusion.

Crossreferencing items with the citation syntax

Normal citation [@A1ref]. In-text reference, see @A2ref. Year-only
citations are treated as normal ones [-@Cref]. Referencing several
items [@A1ref; @A2ref].

Crossreferencing items with links

The claim [](#A1ref) together with [the next claim](#A2ref) 
entail ([](#Cref)).

# Side comments on list items

We can add a side comment (inlines) to selected items in the list.

* [A1]{} This is the text of a list item. We add a comment to the right side. The comment must be a span element with class `side` at the very end of the item. [*demonstration*]{.side}
* [A2]{} Side comments can be added to list items consisting of several blocks. They should be the last span of the last paragraph, with class `side`. They are shown next the the topmost paragraph in HTML, but at the right of the last line in LaTeX. 

  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec et
  massa ut eros volutpat gravida ut vel lacus. Proin turpis eros, imperdiet sed
  quam eget, bibendum aliquam massa. Phasellus pellentesque egestas dapibus.
  Proin porta tellus id orci consectetur bibendum. Nam eu cursus quam. Etiam
  vehicula in mi sed interdum. Duis rutrum eleifend consectetur. Phasellus
  ullamcorper, urna at vestibulum venenatis, tellus erat luctus nibh, eget
  hendrerit justo enim nec magna. Duis mollis ac felis ac tristique.
  [*very long demonstration spanning multiple lines*]{.side}

* [A3]{} This items has no side content. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec et
  massa ut eros volutpat gravida ut vel lacus. Proin turpis eros, imperdiet sed
  quam eget, bibendum aliquam massa. Phasellus pellentesque egestas dapibus.
  Proin porta tellus id orci consectetur bibendum. Nam eu cursus quam. Etiam
  vehicula in mi sed interdum. Duis rutrum eleifend consectetur. Phasellus
  ullamcorper, urna at vestibulum venenatis, tellus erat luctus nibh, eget
  hendrerit justo enim nec magna. Duis mollis ac felis ac tristique.

This final block is outside of the list. 

# More examples and tests

## Math formulas

* [$p_1$]{} This list uses
* [$p_2$]{} math formulas as labels.

## LaTeX code

* [\textbf{a}]{} This list uses
* [\textbf{b}]{} latex code as labels.

Ignored: these are not treated as labels.

## Small caps

* [[All]{.smallcaps}]{} This list uses
* [[Some]{.smallcaps}]{} latex code as labels.

## List with Para items

* [A1]{} $$F(x) > G(x)$$
* [A2]{} $$G(x) > H(x)$$

## items with several blocks

* [**B1**]{} This list's items

    consist of several blocks

    $$\sum_i Fi > \sum_i Gi$$

* [**B2**]{} Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec et
  massa ut eros volutpat gravida ut vel lacus. Proin turpis eros, imperdiet sed
  quam eget, bibendum aliquam massa. Phasellus pellentesque egestas dapibus.
  Proin porta tellus id orci consectetur bibendum. Nam eu cursus quam. Etiam
  vehicula in mi sed interdum. Duis rutrum eleifend consectetur. Phasellus
  ullamcorper, urna at vestibulum venenatis, tellus erat luctus nibh, eget
  hendrerit justo enim nec magna. Duis mollis ac felis ac tristique.

  Pellentesque malesuada arcu ac orci scelerisque vulputate. Aenean at ex
  suscipit, ultricies tellus sit amet, luctus lectus. Duis ut viverra sapien.
  Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac
  turpis egestas. Cras consequat nisi at ex finibus, in condimentum erat auctor.
  In at nulla at est iaculis pulvinar sed id diam. Cras malesuada sit amet tellus id molestie.

## cross-reference with citation syntax

* [**B1**]{#B1ref} This is the first claim.
* [B2]{#B2ref} This is the second claim.
* [*D*]{#Dref} This is the conclusion.

The claim @B1ref together with the claim @B2ref 
entail [@Dref].

## cross-reference with internal link syntax

* [**C1**]{#C1ref} This is the first claim.
* [C2]{#C2ref} This is the second claim.
* [*E*]{#Eref} This is the conclusion.

The claim [](#C1ref) together with the claim [](#C2ref) 
entail ([](#Eref)).

output.html

Code

labelled-lists.lua

--[[-- # Labelled-lists - Pandoc / Quarto filter for labelled lists

@author Julien Dutant <julien.dutant@kcl.ac.uk>
@copyright 2021-2024 Julien Dutant
@license MIT - see LICENSE file for details.
@release 0.4.2

@TODO style the HTML output
@TODO in HTML, leave the BulletList element as is. 
      simply turn the Spans into labels, and wrap in a Div. 
@TODO style the label in all outputs
@TODO Possible solution: first sytle all labels, leaving 
    the pandoc.BulletList as is. Then send it to a formatter
    that wraps it in a Div (html) and adds local CSS style 
    block if needed, or flattens it in Raw for LaTeX.  
@TODO use a Div to declare as custom-label list
]]

-- # Internal settings

--- Options map, including defaults.
-- @disable_citations boolean whether to use pandoc-crossref cite syntax
-- @delimiter list label delimiters (as a list)
-- @text_base_direction: ltr or rtl text (read from meta.dir)
local options = {
    disable_citations = false,
    delimiter = {'(',')'},
    text_base_direction = 'ltr'
}

-- target_formats  filter is triggered when those formats are targeted
local target_formats = {
  "html.*",
  "latex",
  "jats",
}

-- html classes
local html_classes = {
    item = 'labelled-lists-item',
    label = 'labelled-lists-label',
    list = 'labelled-lists-list',
    side = 'labelled-lists-side',
}

-- Css to be used later
local Css = [[
div.labelled-list > ul {
  list-style-type: none;
}
div.labelled-list > ul li {
/*  border: 1px solid dimgray;*/
  padding-left: 1em;
}
div.labelled-list > ul > li > label:first-child{
/*  border: 1px solid blue;*/
  display: inline-block;
  min-width: 3em; /* 2em + padding on li */
  margin-left: -3.5em; /* -(2.5em + padding on li) */
  margin-right: .5em;
  color: blue;
}
div.labelled-list > ul > li > *:first-child > label:first-child{
/*  border: 1px solid red;*/
  display: inline-block;
  min-width: 3em; /* 2em + padding on li */
  margin-left: -3.5em; /* -(2.5em + padding on li) */
  margin-right: .5em;
  color: red;
}
]]

-- # Global variable

-- table of indentified labels
local labels_by_id = {}

-- # Helper functions

--- type: pandoc-friendly type function
-- pandoc.utils.type is only defined in Pandoc >= 2.17
-- if it isn't, we extend Lua's type function to give the same values
-- as pandoc.utils.type on Meta objects: Inlines, Inline, Blocks, Block,
-- string and booleans
-- Caution: not to be used on non-Meta Pandoc elements, the 
-- results will differ (only 'Block', 'Blocks', 'Inline', 'Inlines' in
-- >=2.17, the .t string in <2.17).
local type = pandoc.utils.type or function (obj)
        local tag = type(obj) == 'table' and obj.t and obj.t:gsub('^Meta', '')
        return tag and tag ~= 'Map' and tag or type(obj)
    end

--- format_matches: Test whether the target format is in a given list.
-- @param formats list of formats to be matched
-- @return true if match, false otherwise
function format_matches(formats)
  for _,format in pairs(formats) do
    if FORMAT:match(format) then
      return true
    end
  end
  return false
end

--- message: send message to std_error
-- @param type string INFO, WARNING, ERROR
-- @param text string text of the message
function message(type, text)
    local level = {INFO = 0, WARNING = 1, ERROR = 2}
    if level[type] == nil then type = 'ERROR' end
    if level[PANDOC_STATE.verbosity] <= level[type] then
        io.stderr:write('[' .. type .. '] Labelled-lists lua filter: ' 
            .. text .. '\n')
    end
end

-- # Filter functions

--- filter_citations: process citations to labelled lists
-- Check whether the Cite element only contains references to custom 
-- label items, and if it does, convert them to crossreferences.
-- @param cite pandoc AST Cite element
function filter_citations(cite)

    -- warn if the citations mix cross-label references with 
    -- standard ones
    local has_cl_ref = false
    local has_biblio_ref = false

    for _,citation in ipairs(cite.citations) do
        if labels_by_id[citation.id] then
            has_cl_ref = true
        else
            has_biblio_ref = true
        end
    end

    if has_cl_ref and has_biblio_ref then
        message('WARNING', 'A citation mixes bibliographic references \
            with custom label references '
            .. pandoc.utils.stringify(cite.content) )
        return
    end

    if has_cl_ref and not has_biblio_ref then

        -- get style from the first citation
        local bracketed = true 
        if cite.citations[1].mode == 'AuthorInText' then
            bracketed = false
        end

        local inlines = pandoc.List:new()

        -- create link(s)

        for i = 1, #cite.citations do
           inlines:insert(pandoc.Link(
                labels_by_id[cite.citations[i].id],
                '#' .. cite.citations[i].id
            ))
            -- add separator if needed
            if #cite.citations > 1 and i < #cite.citations then
                inlines:insert(pandoc.Str('; '))
            end
        end


        if bracketed then
            inlines:insert(1, pandoc.Str('('))
            inlines:insert(pandoc.Str(')'))
        end

        return inlines

    end

end

--- filter_links: process internal links to labelled lists
-- Empty links to a custom label are filled with the custom
-- label text. 
-- @param element pandoc AST link
-- @TODO in LaTeX output you need \ref and \label
function filter_links (link)

    if pandoc.utils.stringify(link.content) == '' 
        and link.target:sub(1,1) == '#' 
        and labels_by_id[link.target:sub(2,-1)] then

        link.content = labels_by_id[link.target:sub(2,-1)]
            return link

    end

end

-- style_label: style the label
-- returns a styled label. Default: round brackets
-- @param label Inlines an item's label as list of inlines
-- @param delim (optional) a pair of delimiters (list of two strings)
-- @return pandoc.Inlines label
function style_label(label, delim)
    if not delim then
        delim = options.delimiter
    end
    styled_label = label:clone()
    styled_label:insert(1, pandoc.Str(delim[1]))
    styled_label:insert(pandoc.Str(delim[2]))
    return styled_label
end

---ends_with_side_span: does a block end with Span of 'side' class
---@param block pandoc.Block
---@return boolean
function ends_with_side_span(block)
    return #block.c > 1
        and block.c:at(-1).t == 'Span'
        and block.c:at(-1).classes:includes('side')
        and true or false
end

---build_list: processes a custom label list
---@param element pandoc.BulletList the original Bullet List element
---@return pandoc.Blocks list blocks contain Raw output formatting
function build_list(element)
    local list = pandoc.List:new()

    -- does the first span have a delimiter attribute?
    -- element.c[1] is the first item in the list, type blocks
    -- .. [1].c is the first block's content, type inlines
    -- .. [1] the first inline in that block, our span
    local span = element.c[1][1].c[1]
    local delim = nil
    if span.attributes and span.attributes.delimiter then
        delim = read_delimiter(span.attributes.delimiter)
    end

    -- start

    if FORMAT:match('latex') then
        list:insert(pandoc.RawBlock('latex',
            '\\begin{itemize}\n\\tightlist'
            ))
    elseif FORMAT:match('html') then
        list:insert(pandoc.RawBlock('html',
            '<div class="'..html_classes['list']..'">'
            ))
    end

    -- process each item

    for _,blocks in ipairs(element.c) do

        -- remove the label Span and store its content
        local span = blocks[1].c[1]
        blocks[1].c:remove(1)
        local label = pandoc.List(span.content)
        local id = ''

        -- get identifier if not duplicate, store a copy in global table
        if not (span.identifier == '') then
            if labels_by_id[span.identifier] then
                message('WARNING', 'duplicate item identifier ' 
                    .. span.identifier .. '. The second is ignored.')
            else
                labels_by_id[span.identifier] = label
                id = span.identifier
            end
        end

        -- remove and store side Span at the end if present
        local side_inlines = nil
        if ends_with_side_span(blocks[#blocks]) then
            side_inlines = blocks[#blocks].c:remove().c
        end

        if FORMAT:match('latex') then

            local inlines = pandoc.List:new()
            inlines:insert(pandoc.RawInline('latex','\\item['))
            inlines:extend(style_label(label, delim))
            inlines:insert(pandoc.RawInline('latex',']'))
            -- create link target if needed
            if not(id == '') then 
                inlines:insert(pandoc.Span('', {id = id}))
            end            

            -- insert label span in blocks' first Plain or Para
            -- create Plain if needed
            if blocks[1].t == 'Plain' or blocks[1].t == 'Para' then
                inlines:extend(blocks[1].c)
                blocks[1].c = inlines
            else
                blocks:insert(1, pandoc.Plain(inlines))
            end

            -- if side inlines, add them to the last block
            -- must be a Para or Plain; create a self-standing one if needed
            if side_inlines then
                side_inlines:insert(1,
                    pandoc.RawInline('latex','\\hfill ')
                )
                if blocks[#blocks].t == 'Plain' 
                or blocks[#blocks].t == 'Para' then
                    blocks[#blocks].c:extend(side_inlines)
                else
                    blocks:insert(pandoc.Plain(side_inlines))
                end
            end

            list:extend(blocks)
                
        elseif FORMAT:match('html') then

            local label_span = pandoc.Span(style_label(label, delim))
            label_span.classes = { html_classes['label'] }
            if id then label_span.identifier = id end

            -- insert label span in blocks' first Plain or Para
            -- create Plain if needed
            if blocks[1].t == 'Plain' or blocks[1].t == 'Para' then
                blocks[1].c:insert(1, label_span)
            else
                blocks:insert(1, pandoc.Plain({label_span}))
            end

            -- if single Plain/Para block without side inlines, 
            -- we create it as a single <p> with list item classes
            if #blocks == 1 and 
                (blocks[1].t == 'Plain' or blocks[1].t == 'Para') 
                and not side_inlines then

                    blocks[1].c:insert(1, pandoc.RawInline('html', 
                        '<p class="'..html_classes['item']..'">'))
                    blocks[1].c:insert(pandoc.RawInline('html', '</p>'))
                    list:insert(pandoc.Plain(blocks[1].c))

            -- if multiple blocks but no side inline we create as a Div
            elseif not side_inlines then

                list:insert(pandoc.Div(blocks,  
                    { class = html_classes['item'] } ))        

            -- if side inlines our list item is a flex Div container
            -- with one <div> for the item's blocks
            -- and one <p> class 'labelled-list-side' 
            -- order depends on text direction
            else

                local divs = pandoc.List:new({
                    pandoc.Div(blocks)
                })

                local margin = options.text_base_direction == 'ltr'
                and 'margin-left:auto;' or 'margin-right:auto;'

                side_inlines:insert(1,
                    pandoc.RawInline('html', '<p'
                    ..' class="'..html_classes['side']..'"'
                    ..' style="'..margin..'"'
                    ..'>'
                )) 
                side_inlines:insert(
                    pandoc.RawInline('html', '</p>')
                )

                if options.text_base_direction == 'ltr' then
                    divs:insert(pandoc.Plain(side_inlines))
                else
                    divs:insert(1, pandoc.Plain(side_inlines))
                end

                local flex_dir = options.text_base_direction == 'ltr'
                and 'flex-direction: row;' or 'flex-direction: row-reverse;'

                list:insert(pandoc.Div(divs,
                    { class = html_classes['item'],
                      style = 'display:flex; '..flex_dir
                    }
                ))

            end
 
        end

    end

    if FORMAT:match('latex') then
        list:insert(pandoc.RawBlock('latex',
            '\\end{itemize}\n'
            ))
    elseif FORMAT:match('html') then
        list:insert(pandoc.RawBlock('html','</div>'))        
    end

    return list
end

--- is_custom_labelled_list: Look for custom labels markup
-- Custom label markup requires each item starting with a span
-- containing the label
-- @param element pandoc BulletList element
function is_custom_labelled_list (element)

    local is_cl_list = true

    -- the content of BulletList is a List of List of Blocks
    for _,blocks in ipairs(element.c) do

        -- check that the first element of the first block is Span
        -- ~~and not empty~~ allowing empty
        if not( blocks[1].c[1].t == 'Span' ) 
            -- or pandoc.utils.stringify(blocks[1].c[1].content) == '' 
            then
            is_cl_list = false
            break 
        end
    end

    return is_cl_list
end

--- read_delimiter: process a delimiter option
-- @delim: string, e.g. `Parens` or `[%1]`
-- @return: a pair of delimiter strings
function read_delimiter(delim) 
    delim = pandoc.utils.stringify(delim)

    --- process standard Pandoc attributes and their equivalent
    if delim == '' or delim:lower() == 'none' then
        return {'',''}
    elseif delim == 'Period' or delim == '.' then
        return {'', '.'}
    elseif delim == 'OneParen' or delim == ')' then
        return {'', ')'}
    elseif delim == 'TwoParens' or delim == '(' or delim == '()' then
        return {'(',')'}
    --- if it contains '%1' assume it's a substitution string for gmatch
    -- the left delimiter is before '%1' and the right after
    elseif string.find(delim, '%%1') then
        return {delim:match('^(*.)%%1') or '', 
                delim:match('%%1(*.)$') or ''}
    end

end

--- Read options from metadata block.
--  Get options from the `statement` field in a metadata block.
-- @todo read kinds settings
-- @param meta the document's metadata block.
-- @return nothing, values set in the `options` map.
-- @see options
function get_options(meta)
  if meta['labelled-lists'] then

    if meta['labelled-lists']['disable-citations'] then
        options.disable_citations = true
    end


    -- default-delimiter: string
    if meta['labelled-lists'].delimiter and 
            type(meta['labelled-lists'].delimiter) == 'Inlines' then
        local delim = read_delimiter(pandoc.utils.stringify(
                        meta['labelled-lists'].delimiter))
        if delim then options.delimiter = delim end
    end

    if meta.dir and pandoc.utils.stringify(meta.dir) == 'rtl' then
        options.text_base_direction = 'rtl'
    end

  end
end

-- # Filter

--- Main filters: read options, process lists, process crossreferences
read_options_filter = {
    Meta = get_options
}
process_lists_filter = {
    BulletList = function(element)
        if is_custom_labelled_list(element) then
            return build_list(element)
        end
    end,
}
crossreferences_filter = {
    Link = filter_links,
    Cite = function(element) 
        if not options.disable_citations then 
            return filter_citations(element)
        end
    end
}


--- Main code
-- return the filters in the desired order
if format_matches(target_formats) then
    return { read_options_filter, 
        process_lists_filter, 
        crossreferences_filter
    }
end