First-line Indent

GitHub build status

Pandoc/Quarto filter for smart first-line indents in HTML and LaTeX/PDF.

See on GitHub

Overview

Quarto/Pandoc’s support of first-line indents is limited: it’s not available in HTML output and delegated to LaTeX PDF output. This filter provides a first-line indentation style with smart defaults, full customization, and manual control for fine-grain adjustments.

Background

Paragraphs are typically separated in either of two ways: by vertical whitespace (common on the web) or by indenting their first line (common in books). There is some variation in the first-line indent style itself: some apply it to every paragraph, others don’t apply it to paragraphs below a section heading, blockquote or the like. They also vary in size, the most common being between half an em (the width of the letter ‘m’) for narrow text to 3 ems for wide text. 1 to 1.5em are probably the most standard values.

Quarto and Pandoc use vertical whitespace by default. In HTML outputs that cannot be changed. In LaTeX/PDF output one can switch to first-line indent by setting the metadata variable indent to true. There are some limitations, however:

This filter provides first-line indentation in HTML output and improves its handling in both PDF and HTML outputs.

  1. First-line indentation is used to separate paragraphs, unless indent is set to false.
  2. It generates HTML outputs with first-line indent style. That is done by appending CSS code in the document’s metadata header-includes field. This can be disabled if you want to provide your own CSS.
  3. You can keep or remove the indent of specific paragraphs manually, by adding \indent and \noindent at the beginning of the paragraph in the markdown source. These are LaTeX commands but will work with HTML output too.
  4. First-line indentation is not applied certain block elements: by default, not after lists, block quotes, code blocks and horizontal rules. You can specify which through the filter’s options. This can be overridden on a per-paragraph basis by inserting \indent at the beginning of the paragraph.
  5. The width of first-line indentations can be customized.

Installation

Quarto

Install this filter in a document’s folder by running:

quarto install extension dialoa/first-line-indent

on the command line (terminal in RStudio).

Use it by adding first-line-indent to the filters entry of your YAML header.

---
filters:
  - first-line-indent
---

Pandoc

Copy the file first-line-indent.lua in your document folder. Pass the filter to Pandoc via the --lua-filter (or -L) command line option.

pandoc --lua-filter first-line-indent.lua ...

Or specify it in a defaults file (see Pandoc’s manual: defaults).

You can place the filter file Pandoc’s user data dir, or in an arbitrary folder (-L path/to/first-line-indent.lua). See Pandoc’s manual:Lua filters.

R Markdown

Copy the file first-line-indent.lua in your document folder. Use pandoc_args to invoke the filter. See the R Markdown Cookbook for details.

---
output:
  word_document:
    pandoc_args: ['--lua-filter=first-line-indent.lua']
---

You can place the filter in another folder, provided you specify its path:

---
output:
  word_document:
    pandoc_args: ['--lua-filter=../path/to/first-line-indent.lua']
---

Basic usage

See also the sample input file and the resulting HTML output.

Applying first-line indent to a whole document

To apply first-line indentation to your entire document, set indent to true in the YAML header:

---
indent: true
---

In Quarto, indent may also be set per format:

---
format:
  html:
    indent: false
  pdf:
    indent: true
---

The filter applies some typesetting adjustments, e.g. no first-line indentation after lists. See [typesetting-background] below for details. If you’re not happy with the adjustments, you can control them via options and manually apply or remove indents from some paragraphs.

Manually add or remove first-line indent on a paragraph

Whether or not first-line indentation is activated for the whole document, you can manually add or remove it from a particular paragraph by inserting \indent or \noindent at the beginning of the paragraph:

> This is a blockquote

\indent This paragraph will have an indent even though it follows a
blockquote.

Even though \indent and \noindent are LaTeX commands, the filter handles them in HTML output too.

Warning: citations after \indent. If the paragraph starts with a square-bracketed citation, \indent or \noindent must be marked as a “Raw Inline”, as follows:

`\indent`{=tex} [@Smith2008] says....

That is because Pandoc/Quarto interprets \indent [@cite] as a LaTeX command with a bracketed option rather than a LaTeX command followed by a citation.

Advanced usage

Filter options

Filter options are specified in the document’s YAML header:

indent: true
first-line-indent:
  size: 2em
  auto-remove: true
  set-metadata-variable: true
  set-header-includes: true
  remove-after:
    - BlockQuote
    - BulletList
    - CodeBlock
    - DefinitionList
    - HorizontalRule
    - OrderedList
  dont-remove-after: Table
  remove-after-class: 
    - statement
  dont-remove-after-class: 

Different options can be provided for different output formats. This is standard with Quarto, but the filter also reads these with Pandoc:

format:
  html:
    indent: true
    first-line-indent:
      size: 2em
  pdf:
    indent: true
    first-line-indent:
      size: 1.5em

Format-specific options override global ones. For instance, to disable first line indentation in HTML output only:

# Format-specific options
format:
  html:
    indent: false
    first-line-indent:
      set-header-includes: false
# Global options
indent: true
first-line-indent:
  size: 2em

Options can be passed in a separate metadata file (Quarto, Pandoc or defaults (Pandoc only).

Options reference

indent (default true)

If set to false, paragraphs are separated with vertical whitespace rather than first line indentation. This essentially deactivates the filter, though \indent can still be used to add indent to individual paragraphs for HTML output as well as PDF.

size (default nil)

String specificing size of the first-line indent. Must be in a format suitable for all desired outputs. 1.5em, 2ex, .5pc, 10pt, 25mm, 2.5cm, 0.3in, all work in LaTeX and HTML. 25px only works in HTML. LaTeX commands (\textheight) are not supported.

auto-remove (default true)

Whether the filter automatically removes first line indentation from paragraphs that follow blocks of given types, unless they start with \indent. Set to false to disable. Use the remove-after... and dont-remove-after... options below to control which block types and Div classes are handled that way. By default first-line indentation is removed after Blockquote, lists (DefinitionList, BulletList, OrderedList, which include numbered example lists) and HorizontalRule blocks.

set-metadata-variable (default: true):

Whether the filter adds the metavariable indent with the value true when it is missing. Without this Pandoc’s LaTeX template does not use first-line indentation in PDF output.

set-header-includes (default true)

Whether the filter should add formatting code to the document’s header-includes metadata field. Set it to false if you use a custom template instead.

remove-after, dont-remove-after

Whether to remove first-line indentations automatically after blocks of a certain type. These options can be a single string or a list of strings. The strings are case-sensitive and should correspond to block types in Lua filters: BlockQuote, BulletList, CodeBlock, DefinitionList, Div, Header, HorizontalRule, LineBlock, Null, OrderedList, Para, Plain, RawBlock, Table. Inactive if auto-remove is false.

remove-after-class, dont-remove-after-class

Decide whether to remove first-line indentation automatically after elements of certain classes. For instance, you may use decide that when a block with class “continuing” is followed by a paragraph, the latter should not be first-line indented. Useful for Div elements, if you use Divs of certain classes to wrap and typeset material that doesn’t end a paragraph. Inactive if auto-remove is false.

To illustrate, suppose you don’t want to filter to remove first-line indent after definition lists. You can add the following lines in the document’s metadata block (if the source is markdown):

first-line-indent:
  dont-remove-after: DefinitionList

Styling HTML output

In LaTeX output the filters adds \noindent commands at beginning of paragraphs that shouldn’t be indented. These can be controlled in LaTeX as usual.

In HTML output paragraphs that are explicitly marked to have no first-line indent are preceded by an empty div with class no-first-line-indent-after and those that are explictly marked (with \indent in the markdown source) to have a first-line indent are preceded by an empty div with class first-line-indent-after, as follows:

<ul>
  <li>A bullet</li>
  <li>list</li>
</ul>
<div class="no-first-line-indent-after"></div>
<p>This paragraph should not have first-line indent.</p>
...
<div class="first-line-indent-after"></div>
<p>This paragraph should have first-line indent.</p>

These can be styled in CSS as follows:

p {
  text-indent: 1.5em;
  margin: 0;
}
header p {
  text-indent: 0;
  margin: 1em 0;
}
:is(h1, h2, h3, h4, h5, h6) + p {
  text-indent: 0;
}
li > p, li > div > p, li > div > div > p {
  text-indent: 0;
  margin-bottom: 1rem;
}
div.no-first-line-indent-after + p {
  text-indent: 0;
}
div.first-line-indent-after + p {
  text-indent: SIZE;
}      

The first four rules provide global first line indentation.

The last two rules provide explicit local indentation. The div.no-first-line-indent-after) + p rule removes indent from paragraphs placed just after a Div with the no-first-line-indent-after class, and the second rule keeps them in paragraphs that follow a first-line-indent-after Div.

The indentation filter adds the following rule:

div.labelled-lists-list > p {
    text-indent: 0;
}

To avoid interference with Dialoa’s labelled-lists filter.

Block quotations and the LaTeX quote environment

The filter applies first line indent style within block quotes, with no indent on the first line.

To achieve this in PDF output, the LaTeX quote environment (used by Quarto/Pandoc for block quotes) is redefined as follows in header-includes:

\renewenvironment{quote}
     {\list{}{\listparindent 1.5em%
              \itemindent \listparindent
              \rightmargin \leftmargin
              \parsep \z@ \@plus \p@}%
            \item\relax}
      {\endlist}

Which is the definition of LaTeX’s quotation environment—see the standard classes source.

If you redefine the quote environment, you should use this code as basis.

Overriding the filter’s header-includes

The filter adds its commands at the beginning of the header-includes field. You can thus use header-includes to override the filter’s commands.

Contributing

Issues and PRs welcome.

License

Copyright 2021-2023 Julien Dutant. License MIT - see license file for details.

Example

input.md

---
title: "First-line Indent"
author: Julien Dutant
date: 15 Mar 2023
## for Quarto only (Pandoc ignores this)
filters:
- first-line-indent
# Filter options. You do not need to specify any,
# remove all the lines below the filter still 
# separates paragraphs with indentation rather
# than vertical whitespace.
first-line-indent:
  set-metadata-variable: true
  set-header-includes: true
  auto-remove: true
  remove-after: Table
  dont-remove-after:
    - DefinitionList
    - OrderedList
  size: "2em"
  remove-after-class: chuckit
  dont-remove-after-class: keepit
---

This document illustrates first-line indent typesetting. In English
typography, paragraphs just below a section heading aren't indented,
because a heading is enough to separate them from what is before. The
same should apply to the first paragraph of a document with a
title---so this paragraph is not indented.

This paragraph is indented. But after this quote:

> Lorem ipsum dolor sit amet, consectetur adipiscing elit.

the paragraph continues, so there should not be a first-line indent.

We want this quote to end a paragraph:

> Lorem ipsum dolor sit amet, consectetur adipiscing elit.

\indent The text below therefore begins a new paragraph and should
have a first-line indent. We have to manually specify using `\indent`. 

# Basic tests

After a heading (in English typographic style) the paragraph does not
have a first-line indent.

## Manually specifying indentation on certain paragraphs

In the couple of paragraphs that follow the quotes below, we
have manually specified `\noindent` and `\indent` respectively. This
is to check that the filter doesn't add its own commands to those.

> Lorem ipsum dolor sit amet, consectetur adipiscing elit.

\indent Here we've explicitly required  a first line indent.

\noindent Here we've explicitly required *not* to have one.

## Automatic removal of first line indentation

We can also check that indent is removed after lists:

* A bullet
* list

And after code blocks:

```lua
local variable = "value"
```

Or horizontal rules.

---

We check that this behavour is overriden for specified classes. We
created a custom class to preserve indentation after certain elements:

``` {.markdown .keepit}
This code block 
should be followed 
by an indented 
paragraph
```

And another one to remove indents after others:

::: chuckit
This paragraph's Div container should not
be followed by indentation,
:::

as specified in this document's options. 

# Further tests

In this document we added a few custom filter options. 

## Size

The size of first-line indents is 2em instead of the default 1.5em
(Pandoc) or 1em (Quarto). 

## Indent within quotes

Blockquotes first-line indentation:

> Blockquotes should not be indented on their first paragraph but
> otherwise have the same size of ident as the main text.
>
> Hence this second paragraph has a first-line indentation of
> 2em.
 

## Keep or remove indentation after certain types of elements

We also added an option to 
automatically remove indent after tables:

  Right     Left     Center     Default
-------     ------ ----------   -------
     12     12        12            12
    123     123       123          123
      1     1          1             1

Table:  Demonstration of simple table syntax.

So this paragraph's first line is not indented. We added the option
*not* to remove ident after ordered lists and definition lists:

Definition
: This is a definition block.

So this paragraph is indented.

1. An ordered
2. list

And this one is too.

## Recursion and nesting

The paragraphs below are nested within a Div element---actually, two
nested Divs, in order to check that the filter is applied recursively
within Divs.

::: {#div}

::::: {#subdiv}

The first paragraph within a Div is indented normally, but the 
list below

* list item
* list item

should not be followed by a indented paragraph.

The last paragraph within Divs should be indented normally.

:::::

:::

The filter is also applied recursively within blockquotes. A
blockquote's first paragraph shouldn't be indented, but any subsequent
ones should. Within the block quotes, indents should be removed after
special blocks, as in the main text. 

> The first paragraph of this blockquote does not
> have a first line indent.
>
> The subsequent paragraph has one. It's followed:
>
> * by a
> * list
> 
> after which there is no first line indentation.
> 
> This next paragraph is first line indented again..

## List content

Within lists, paragraphs should be separated by vertical whitespace.

* This list item contains multiple paragraphs.

  The second one should not be indented, but separated by vertical 
  whitespace.

output.html

Code

first-line-indent.lua

--[[# first-line-indent.lua – First line indentation filter

Copyright: © 2021–2023 Contributors
License: MIT – see LICENSE for details

@TODO latex_quote should use options.size (or better, a specific option)
@TODO option for leaving indents after headings (French style)
@TODO smart setting of the post-heading style based on `lang`
@TODO option to leave indent at the beginning of the document

]]

PANDOC_VERSION:must_be_at_least '2.17'
stringify = pandoc.utils.stringify
equals = pandoc.utils.equals
pandoctype = pandoc.utils.type

-- # Options

---@class Options Options map with default values.
---@field format string|nil output format (currently: 'html' or 'latex')
---@field indent boolean whether to use first line indentation globally
---@field set_metadata_variable boolean whether to set the `indent`
--    metadata variable.
---@field set_header_includes boolean whether to provide formatting code in
--    header-includes.
---@field auto_remove_indents boolean whether to automatically remove
--    indents after specific block types.
---@field remove_after table list of strings, Pandoc AST block types
--    after which first-line indents should be automatically removed.
---@field remove_after_class table list of strings, classes of elements
--    after which first-line indents should be automatically removed.
---@field dont_remove_after_class table list of strings, classes of elements
--    after which first-line indents should not be removed. Prevails
--    over remove_after.
---@field size string|nil a CSS / LaTeX specification of the first line
--    indent length
---@field recursive table<string, options> Pandoc Block types to
---     which the filter is recursively applied, with options map.
---     The option `dont_indent_first` controls whether indentation
---     is removed on the first paragraph.
local Options = {
  format = nil,
  indent = true,
  set_metadata_variable = true,
  set_header_includes = true,
  auto_remove = true,
  remove_after = pandoc.List({
    'BlockQuote',
    'BulletList',
    'CodeBlock',
    'DefinitionList',
    'HorizontalRule',
    'OrderedList',
  }),
  remove_after_class = pandoc.List({
    'statement',
  }),
  dont_remove_after_class = pandoc.List:new(),
  size = nil, -- default let LaTeX decide
  size_default = '1.5em', -- default value for HTML
  recursive = {
    Div = {dont_indent_first = false},
    BlockQuote = {dont_indent_first = true},
  }
}

-- # Filter global variables

---@class code map pandoc objects for indent/noindent Raw code.
local code = {
  tex = {
    indent = pandoc.RawInline('tex', '\\indent '),
    noindent = pandoc.RawInline('tex', '\\noindent '),
  },
  latex = {
    indent = pandoc.RawInline('latex', '\\indent '),
    noindent = pandoc.RawInline('latex', '\\noindent '),
  },
  html = {
    indent = pandoc.RawBlock('html',
      '<div class="first-line-indent-after"></div>'),
    noindent = pandoc.RawBlock('html',
      '<div class="no-first-line-indent-after"></div>'),
  }
}


---LATEX_QUOTE_ENV: LaTeX's definition of the quote environement
---used to define HeaderIncludes.
---a \setlength{\parindent}{<size>} will be appended
---@type string
local LATEX_QUOTE_ENV = [[\makeatletter
  \renewenvironment{quote}
     {\list{}{\listparindent 1.5em%
              \itemindent \listparindent
              \rightmargin \leftmargin
              \parsep \z@ \@plus \p@}%
            \item\noindent\relax}
      {\endlist}
  \makeatother
]]

---@class HeaderIncludes map of functions to produce
---header-includes code given a size parameter (string|nil),
--- either for global or for local indentation markup.
--- optionally wrap the constructed global header markup (e.g. <style> tags).
--- glob = {html : function, latex: function}
--- wrap = {html : function, latex: function}
--- loc = {html : function, latex: function}
HeaderIncludes = {
  glob = {
    html = function(size)
      size = size or Options.size_default
      local code = [[  p {
    text-indent: SIZE;
    margin: 0;
  }
  header p {
    text-indent: 0;
    margin: 1em 0;
  }
  :is(h1, h2, h3, h4, h5, h6) + p {
    text-indent: 0;
  }
  li > p, li > div p {
    text-indent: 0;
    margin-bottom: 1rem;
  }
]]
      return code:gsub("SIZE", size)
    end,
    latex = function(size)
      local size_code = size and '\\setlength{\\parindent}{'..size..'}\n'
                        or ''
      return LATEX_QUOTE_ENV .. size_code
    end,
  },
  wrap = {
    html = function(header_str)
      return "<style>\n/* first-line indent styles */\n" .. header_str
        .. "/* end of first-line indent styles */\n</style>"
    end,
    latex = function(str) return str end,
  },
  loc = {
    html = function(size)
      size = size or Options.size_default
      local code = [[  div.no-first-line-indent-after + p {
    text-indent: 0;
  }
  div.first-line-indent-after + p {
    text-indent: SIZE;
  }
]]
      return code:gsub("SIZE", size)
    end,
    latex = function(_) return '' end,
  }
}

-- # encapsulate Quarto/Pandoc variants

---format_match: whether format matches a string pattern
---ex: format_match('html5'), format_match('html*')
---in Quarto we try removing non-alphabetical chars
---@param pattern string
---@return boolean
local function format_match(pattern)
  return quarto and (quarto.doc.is_format(pattern)
      or quarto.doc.is_format(pattern:gsub('%A',''))
    )
    or FORMAT:match(pattern)
end

---add_header_includes: add a block to the document's header-includes
---meta-data field.
---@param meta pandoc.Meta the document's metadata block
---@param blocks pandoc.Blocks list of Pandoc block elements (e.g. RawBlock or Para)
---   to be added to the header-includes of meta
---@return pandoc.Meta meta the modified metadata block
local function add_header_includes(meta, blocks)

  -- Pandoc
  local function pandoc_add_headinc(meta,blocks)

    local header_includes = pandoc.MetaList( { pandoc.MetaBlocks(blocks) })

    -- add any exisiting meta['header-includes']
    -- it can be MetaInlines, MetaBlocks or MetaList
    if meta['header-includes'] then
      if pandoctype(meta['header-includes']) == 'List' then
        header_includes:extend(meta['header-includes'])
      else
        header_includes:insert(meta['header-includes'])
      end
    end

    meta['header-includes'] = header_includes

    return meta

  end

  -- Quarto
  local function quarto_add_headinc(blocks)
    quarto.doc.include_text('in-header', stringify(blocks))
  end

  return quarto and quarto_add_headinc(blocks)
    or pandoc_add_headinc(meta,blocks)

end


-- # Helper functions

-- ensure_list: turns Inlines and Blocks meta values into list
local function ensure_list(elem)
  if elem and (pandoctype(elem) == 'Inlines'
    or pandoctype(elem) == 'Blocks')  then
    elem = pandoc.List:new(elem)
  end
  return elem
end


--- classes_include: check if one of an element's class is in a given
-- list. Returns true if match, nil if no match or the element doesn't
-- have classes.
---@param elem table pandoc AST element
---@param classes table pandoc List of strings
local function classes_include(elem,classes)

  if elem.classes then

    for _,class in ipairs(classes) do
      if elem.classes:includes(class) then return true end
    end

  end

end

--- is_indent_cmd: check if an element is a LaTeX indent command
---@param elem pandoc.Inline
---@return string|nil 'indent', 'noindent' or nil
-- local function is_indent_cmd(elem)
--   return (equals(elem, code.latex.indent)
--     or equals(elem, code.tex.indent)) and 'indent'
--     or (equals(elem, code.latex.noindent)
--     or equals(elem, code.tex.noindent)) and 'noindent'
--     or nil
-- end
local function is_indent_cmd(elem)
  return elem.text and (
      elem.text:match('^%s*\\indent%s*$') and 'indent'
      or elem.text:match('^%s*\\noindent%s*$') and 'noindent'
    )
    or nil
end

-- # Filter functions

--- Add format-specific explicit indent markup to a paragraph.
--- Returns a list of blocks containing a single paragraph
--- or a rawblock followed by a paragraph, depending on format.
---@param type string 'indent' or 'noindent', type of markup to add
---@param elem pandoc.Para
---@return pandoc.Blocks
local function indent_markup(type, elem)
  local result = pandoc.List:new()

  if not (type == 'indent' or type == 'noindent') then

    result:insert(elem)

  elseif format_match('latex') then

    -- in LaTeX, replace any `\indent` or `\noindent`
    -- at the beginning of the paragraph with
    -- with the one corresponding to `type`

    if elem.content[1] and is_indent_cmd(elem.content[1]) then
      elem.content[1] = code.tex[type]
    else
      elem.content:insert(1, code.tex[type])
    end
    result:insert(elem)


  elseif format_match('html') then

    result:extend({ code.html[type], elem })

  end

  return result

end

--- process_blocks: process indentations in a list of blocks.
-- Adds output code for explicitly specified first-line indents,
-- automatically removes first-line indents after blocks of the
-- designed types unless otherwise specified.
---@param blocks pandoc.Blocks element (list of blocks)
---@param dont_indent_first boolean whether to indent the first paragraph
local function process_blocks(blocks, dont_indent_first)
  dont_indent_first = dont_indent_first or false
  -- tag for the first element
  local is_first_block = true -- tags the doc's first element
  -- tag to trigger indentation auto-removal on the next element
  local dont_indent_next_block = false
  local result = pandoc.List:new()

  for _,elem in pairs(blocks) do

    -- Paragraphs: if they have explicit LaTeX indent markup
    -- reproduce it in the output format, otherwise
    -- remove indentation if needed, provided `auto_remove` is on.
    if elem.t == "Para" then

      if elem.content[1] and is_indent_cmd(elem.content[1]) then

        -- 'indent' or 'noindent' ?
        local type = is_indent_cmd(elem.content[1])

        result:extend(indent_markup(type, elem))

      elseif is_first_block and dont_indent_first then

          result:extend(indent_markup('noindent', elem))

      elseif dont_indent_next_block and Options.auto_remove then

        result:extend(indent_markup('noindent', elem))

      else

        result:insert(elem)

      end

      dont_indent_next_block = false

    -- Non-Paragraphs: check first whether it's an element after
    -- which indentation must be removed. Next insert it, applying
    -- this function recursively within the element if needed.
    else

      if Options.auto_remove then

        if Options.remove_after:includes(elem.t) and
            not classes_include(elem, Options.dont_remove_after_class) then

          dont_indent_next_block = true

        elseif elem.classes and
            classes_include(elem, Options.remove_after_class) then

          dont_indent_next_block = true

        else

          dont_indent_next_block = false

        end

      end

      -- recursively process the element if needed
      if Options.recursive[elem.t] then

        local dif = Options.recursive[elem.t].dont_indent_first
        elem.content = process_blocks(elem.content, dif)

      end

      -- insert
      result:insert(elem)

    end

    -- ensure `is_first_block` turns to false
    -- even if the first block wasn't a paragraph
    -- or if it had explicit indent marking
    is_first_block = false

  end

  return result

end

--- process_doc: Process indents in the document's body text.
-- Adds output code for explicitly specified first-line indents,
-- automatically removes first-line indents after blocks of the
-- designed types unless otherwise specified.
local function process_doc(doc)
  local dont_indent_first = false

  -- if no output format, do nothing
  if not Options.format then return end

  -- if the doc has a title, do not indent first paragraph
  if doc.meta.title then
    dont_indent_first = true
  end

  doc.blocks = process_blocks(doc.blocks, dont_indent_first)

  return doc

end


--- read_user_options: read user options from meta element.
-- in Quarto options may be under format/pdf or format/html
-- the latter override root ones.
local function read_user_options(meta)
  local user_options = {}

  if meta.indent == false then
    Options.indent = false
  end

  if meta['first-line-indent'] then
    user_options = meta['first-line-indent']
  end

  local formats = {'pdf', 'html', 'latex'}
  if meta.format then
    for format in ipairs(formats) do
      if format_match(format) and meta.format[format] then
        for k,v in meta.format[format] do
          user_options[k] = v
        end
      end
    end
  end

  if user_options['set-metadata-variable'] == false then
    Options.set_metadata_variable = false
  end

  if user_options['set-header-includes'] == false then
    Options.set_header_includes = false
  end

  -- size
  -- @todo using stringify means that LaTeX commands in
  -- size are erased. But it ensures that the filter gets
  -- a string. Improvement: check that we have a string
  -- and throw a warning otherwise
  if user_options.size and pandoctype(user_options.size == 'Inlines') then

    Options.size = stringify(user_options.size)

  end

  if user_options['auto-remove'] == false then
    Options.auto_remove = false
  end

  -- autoremove elements and classes
  -- for elements we only need a whitelist, remove_after
  -- for classes we need both a whitelist (remove_after_class)
  -- and a blacklist (dont_remove_after_class).

  -- first insert user values in `remove_after`, `remove_after_class`
  -- and `dont_remove_after_class`.
  for optname, metakey in pairs({
    remove_after = 'remove-after',
    remove_after_class = 'remove-after-class',
    dont_remove_after_class = 'dont-remove-after-class',
  }) do

    local user_value = ensure_list(user_options[metakey])

    if user_value and pandoctype(user_value) == 'List' then

        for _,item in ipairs(user_value) do

          Options[optname]:insert(stringify(item))

        end

    end

  end

  -- then remove blacklisted entries from `remove_after`
  -- and `remove_after_class`.
  for optname, metakey in pairs({
    remove_after = 'dont-remove-after',
    remove_after_class = 'dont-remove-after-class'
  }) do

    local user_value = ensure_list(user_options[metakey])

    if user_value and pandoctype(user_value) == 'List' then

      -- stringify the list
      for i,v in ipairs(user_value) do
        user_value[i] = stringify(v)
      end

      -- filter to that returns true iff an item isn't blacklisted
      local predicate = function (str)
        return not(user_value:includes(str))
      end

      -- apply the filter to the whitelist
      Options[optname] = Options[optname]:filter(predicate)

    end

  end

end

--- set_meta: insert options in doc's meta
--- Sets `indent` and extends `header-includes` if needed.
---@param meta pandoc.Meta
---@return pandoc.Meta|nil meta nil if no changes
local function set_meta(meta)
  local changes = false -- only return if changes are made
  local header_code = nil
  local format = Options.format

  -- set the `indent` metadata variable unless otherwise specified or
  -- already set to false
  if Options.set_metadata_variable and not(meta.indent == false) then
    meta.indent = true
    changes = true
  end

  -- set the `header-includes` metadata variable
  if Options.set_header_includes and Options.indent then

    -- do we apply first line indentation globally?
    if Options.indent then
      header_code = HeaderIncludes.glob[format](Options.size)
    end
    -- provide local explicit indentation styles
    header_code = header_code .. HeaderIncludes.loc[format](Options.size)
    -- wrap the header if needed
    header_code = HeaderIncludes.wrap[format](header_code)
    
    -- insert if not empty
    if header_code ~= '' then
      add_header_includes(meta, { pandoc.RawBlock(format, header_code)})
      changes = true
    end

  end

  return changes and meta or nil

end

--- process_metadata: process user options.
-- read user options, set the `indent` metadata variable,
-- add formatting code to `header-includes`.
local function process_metadata(meta)
  local changes = false -- only return if changes are made
  local header_code = nil
  local format = format_match('html') and 'html'
                or (format_match('latex') and 'latex')

  if not format then
    return nil
  else
    Options.format = format
  end

  read_user_options(meta) -- places values in global `options`

  return set_meta(meta)

end

--- Main code
-- Returns the filters in the desired order of execution
return {
  {
    Meta = process_metadata
  },
  {
    Pandoc = process_doc
  }
}