texlive[63757] Master/texmf-dist: markdown (29jun22)

commits+karl at tug.org commits+karl at tug.org
Wed Jun 29 22:10:26 CEST 2022


Revision: 63757
          http://tug.org/svn/texlive?view=revision&revision=63757
Author:   karl
Date:     2022-06-29 22:10:26 +0200 (Wed, 29 Jun 2022)
Log Message:
-----------
markdown (29jun22)

Modified Paths:
--------------
    trunk/Master/texmf-dist/doc/generic/markdown/README.md
    trunk/Master/texmf-dist/doc/generic/markdown/VERSION
    trunk/Master/texmf-dist/doc/generic/markdown/markdown.html
    trunk/Master/texmf-dist/doc/generic/markdown/markdown.pdf
    trunk/Master/texmf-dist/doc/latex/markdown/examples/latex.tex
    trunk/Master/texmf-dist/scripts/markdown/markdown-cli.lua
    trunk/Master/texmf-dist/source/generic/markdown/docstrip.cfg
    trunk/Master/texmf-dist/source/generic/markdown/markdown.dtx
    trunk/Master/texmf-dist/tex/context/third/markdown/t-markdown.tex
    trunk/Master/texmf-dist/tex/generic/markdown/markdown.tex
    trunk/Master/texmf-dist/tex/latex/markdown/markdown.sty
    trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_dot.sty
    trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_graphicx_http.sty
    trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_tilde.sty
    trunk/Master/texmf-dist/tex/luatex/markdown/markdown.lua

Modified: trunk/Master/texmf-dist/doc/generic/markdown/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/generic/markdown/README.md	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/doc/generic/markdown/README.md	2022-06-29 20:10:26 UTC (rev 63757)
@@ -2,10 +2,8 @@
 ========
 
 [![license](https://img.shields.io/github/license/witiko/markdown)](LICENSE)
-[![release](https://img.shields.io/github/release/witiko/markdown.svg)][release]
-[![docker pulls](https://img.shields.io/docker/pulls/witiko/markdown)][docker-witiko/markdown]
-[![docker image size](https://img.shields.io/docker/image-size/witiko/markdown/latest)][docker-witiko/markdown]
 [![ci](https://github.com/witiko/markdown/actions/workflows/main.yml/badge.svg)][ci]
+[![release](https://img.shields.io/github/release/witiko/markdown)][release]
 
  [release]:  https://github.com/Witiko/markdown/releases/latest "Releases · Witiko/markdown"
  [ci]:       https://github.com/Witiko/markdown/actions         "GitHub Actions"
@@ -207,7 +205,7 @@
  [tb133]: https://www.overleaf.com/read/pshkckczmmjn                        "Markdown 2.15.0: What's New?"
 
  [tb131-slides]:   https://tug.org/tug2021/assets/pdf/tug2021-novotny-slides.pdf   "Markdown 2.10.0: LaTeX Themes & Snippets, Two Flavors of Comments, and LuaMetaTeX"
- [tb131-video]:    https://youtu.be/i2GJMnLCZls                                    "Markdown 2.10.0: LaTeX Themes & Snippets, Two Flavors of Comments, and LuaMetaTeX"
+ [tb131-video]:    https://youtu.be/THmPkAncMnc                                    "Markdown 2.10.0: LaTeX Themes & Snippets, Two Flavors of Comments, and LuaMetaTeX"
 
  [10.5300/2016-1-4/78]: https://bulletin.cstug.cz/pdf/2016-1-4.pdf#page=80 "Rendering Markdown inside TeX Documents"
  [10.5300/2020-1-2/48]: https://bulletin.cstug.cz/pdf/2020-1-2.pdf#page=50 "Markdown 2.8.1: Boldly Unto the Throne of Lightweight Markup in TeX"
@@ -262,7 +260,7 @@
 files are produced from the `markdown.dtx` document. The `make base` command
 is provided by `Makefile` for convenience. In `markdown.dtx`, the boundaries
 between the produced files are marked up using an XML-like syntax provided by
-the [DocStrip][] plain TeX package.
+the [l3docstrip][] plain TeX package.
 
 Running the [LaTeXMK][] tool on the `markdown.dtx` file
 (`latexmk markdown.dtx`) after the Markdown package has been
@@ -279,7 +277,7 @@
 `Makefile` for convenience.
 
  [doc]:                  https://ctan.org/pkg/doc                           "doc – Format LaTeX documentation"
- [DocStrip]:             https://ctan.org/pkg/docstrip                      "docstrip – Remove comments from file"
+ [l3docstrip]:           https://ctan.org/pkg/l3docstrip                    "l3docstrip – Strip documentation in LaTeX3 source"
  [LaTeXMK]:              https://ctan.org/pkg/latexmk                       "latexmk – Fully automated LaTeX document generation"
  [literate programming]: https://en.wikipedia.org/wiki/Literate_programming "Literate programming"
  [ltxdockit]:            https://ctan.org/pkg/ltxdockit                     "ltxdockit – Documentation support"

Modified: trunk/Master/texmf-dist/doc/generic/markdown/VERSION
===================================================================
--- trunk/Master/texmf-dist/doc/generic/markdown/VERSION	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/doc/generic/markdown/VERSION	2022-06-29 20:10:26 UTC (rev 63757)
@@ -1 +1 @@
-2.15.2-0-gb238dbc (2022/05/31)
+2.15.3-0-g7c8e03d (2022/06/27)

Modified: trunk/Master/texmf-dist/doc/generic/markdown/markdown.html
===================================================================
--- trunk/Master/texmf-dist/doc/generic/markdown/markdown.html	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/doc/generic/markdown/markdown.html	2022-06-29 20:10:26 UTC (rev 63757)
@@ -82,7 +82,7 @@
 <header id="title-block-header">
 <h1 class="title">Markdown Package User Manual</h1>
 <p class="author">Vít Novotný</p>
-<p class="date">2.15.2-0-gb238dbc (2022/05/31)</p>
+<p class="date">2.15.3-0-g7c8e03d (2022/06/27)</p>
 </header>
 <nav id="TOC" role="doc-toc">
 <ul>
@@ -1273,7 +1273,7 @@
 <dt><code>citations</code> (default value: <code>false</code>)</dt>
 <dd><dl>
 <dt>true</dt>
-<dd><p>Enable the pandoc citation syntax extension:</p>
+<dd><p>Enable the Pandoc citation syntax extension:</p>
 <div class="sourceCode" id="cb123"><pre class="sourceCode md"><code class="sourceCode markdown"><span id="cb123-1"><a href="#cb123-1" aria-hidden="true"></a>Here is a simple parenthetical citation <span class="co">[</span><span class="ot">@doe99</span><span class="co">]</span> and here</span>
 <span id="cb123-2"><a href="#cb123-2" aria-hidden="true"></a>is a string of several [see @doe99, pp. 33-35; also</span>
 <span id="cb123-3"><a href="#cb123-3" aria-hidden="true"></a>@smith04, chap. 1].</span>
@@ -1292,7 +1292,7 @@
 </dd>
 <dd><dl>
 <dt>false</dt>
-<dd><p>Disable the pandoc citation syntax extension.</p>
+<dd><p>Disable the Pandoc citation syntax extension.</p>
 </dd>
 </dl>
 </dd>
@@ -1610,7 +1610,7 @@
 <h4 data-number="2.2.1.11" id="option-contentblockslanguagemap"><span class="header-section-number">2.2.1.11</span> Option <code>contentBlocksLanguageMap</code></h4>
 <dl>
 <dt><code>contentBlocksLanguageMap</code> (default value: <code>"markdown-languages.json"</code>)</dt>
-<dd><p>The filename of the JSON file that maps filename extensions to programming language names in the iA Writer content blocks.</p>
+<dd><p>The filename of the <abbr>JSON</abbr> file that maps filename extensions to programming language names in the iA Writer content blocks.</p>
 </dd>
 </dl>
 <h5 class="unnumbered" data-number="" id="latex-example-10"><span class="latex">L<sup>a</sup>T<sub>e</sub>X</span> Example</h5>
@@ -2141,7 +2141,7 @@
 <dt><code>footnotes</code> (default value: <code>false</code>)</dt>
 <dd><dl>
 <dt>true</dt>
-<dd><p>Enable the pandoc footnote syntax extension:</p>
+<dd><p>Enable the Pandoc footnote syntax extension:</p>
 <div class="sourceCode" id="cb208"><pre class="sourceCode md"><code class="sourceCode markdown"><span id="cb208-1"><a href="#cb208-1" aria-hidden="true"></a>Here is a footnote reference,<span class="ot">[^1]</span> and another.<span class="ot">[^longnote]</span></span>
 <span id="cb208-2"><a href="#cb208-2" aria-hidden="true"></a></span>
 <span id="cb208-3"><a href="#cb208-3" aria-hidden="true"></a><span class="ot">[^1]: </span>Here is the footnote.</span>
@@ -2164,7 +2164,7 @@
 </dd>
 <dd><dl>
 <dt>false</dt>
-<dd><p>Disable the pandoc footnote syntax extension.</p>
+<dd><p>Disable the Pandoc footnote syntax extension.</p>
 </dd>
 </dl>
 </dd>
@@ -2709,7 +2709,7 @@
 <dt><code>inlineFootnotes</code> (default value: <code>false</code>)</dt>
 <dd><dl>
 <dt>true</dt>
-<dd><p>Enable the pandoc inline footnote syntax extension:</p>
+<dd><p>Enable the Pandoc inline footnote syntax extension:</p>
 <div class="sourceCode" id="cb247"><pre class="sourceCode md"><code class="sourceCode markdown"><span id="cb247-1"><a href="#cb247-1" aria-hidden="true"></a>Here is an inline note.^[Inlines notes are easier to</span>
 <span id="cb247-2"><a href="#cb247-2" aria-hidden="true"></a>write, since you don't have to pick an identifier and</span>
 <span id="cb247-3"><a href="#cb247-3" aria-hidden="true"></a>move down to type the note.]</span></code></pre></div>
@@ -2718,7 +2718,7 @@
 </dd>
 <dd><dl>
 <dt>false</dt>
-<dd><p>Disable the pandoc inline footnote syntax extension.</p>
+<dd><p>Disable the Pandoc inline footnote syntax extension.</p>
 </dd>
 </dl>
 </dd>
@@ -2879,7 +2879,7 @@
 <dt><code>pipeTables</code> (default value: <code>false</code>)</dt>
 <dd><dl>
 <dt>true</dt>
-<dd><p>Enable the <abbr>PHP</abbr> Markdown table syntax extension:</p>
+<dd><p>Enable the <abbr>PHP</abbr> Markdown pipe table syntax extension:</p>
 <div class="sourceCode" id="cb259"><pre class="sourceCode md"><code class="sourceCode markdown"><span id="cb259-1"><a href="#cb259-1" aria-hidden="true"></a>| Right | Left | Default | Center |</span>
 <span id="cb259-2"><a href="#cb259-2" aria-hidden="true"></a>|------:|:-----|---------|:------:|</span>
 <span id="cb259-3"><a href="#cb259-3" aria-hidden="true"></a>|   12  |  12  |    12   |    12  |</span>
@@ -2890,7 +2890,7 @@
 </dd>
 <dd><dl>
 <dt>false</dt>
-<dd><p>Disable the <abbr>PHP</abbr> Markdown table syntax extension.</p>
+<dd><p>Disable the <abbr>PHP</abbr> Markdown pipe table syntax extension.</p>
 </dd>
 </dl>
 </dd>
@@ -3825,7 +3825,7 @@
 <dt><code>tightLists</code> (default value: <code>true</code>)</dt>
 <dd><dl>
 <dt>true</dt>
-<dd><p>Unordered and ordered Lists whose items do not consist of multiple paragraphs will be considered <em>tight</em>. Tight lists will produce tight renderers that may produce different output than lists that are not tight:</p>
+<dd><p>Unordered and ordered lists whose items do not consist of multiple paragraphs will be considered <em>tight</em>. Tight lists will produce tight renderers that may produce different output than lists that are not tight:</p>
 <div class="sourceCode" id="cb322"><pre class="sourceCode md"><code class="sourceCode markdown"><span id="cb322-1"><a href="#cb322-1" aria-hidden="true"></a><span class="ss">- </span>This is</span>
 <span id="cb322-2"><a href="#cb322-2" aria-hidden="true"></a><span class="ss">- </span>a tight</span>
 <span id="cb322-3"><a href="#cb322-3" aria-hidden="true"></a><span class="ss">- </span>unordered list.</span>
@@ -4069,7 +4069,7 @@
 <span id="cb339-2"><a href="#cb339-2" aria-hidden="true"></a>  helperScriptFileName = helper-script.lua,</span>
 <span id="cb339-3"><a href="#cb339-3" aria-hidden="true"></a>}</span></code></pre></div>
 <h4 data-number="2.2.3.2" id="latexplain"><span class="header-section-number">2.2.3.2</span> No default token renderer prototypes</h4>
-<p>Default token renderer prototypes require <span class="latex">L<sup>a</sup>T<sub>e</sub>X</span> packages that may clash with other packages used in a document. Additionally, if we redefine token renderers and renderer prototypes ourselves, the default definitions will bring no benefit to us. Using the <code>plain</code> package option, we can keep the default definitions from the plain <span class="tex">T<sub>e</sub>X</span> implementation and prevent the soft <span class="latex">L<sup>a</sup>T<sub>e</sub>X</span> prerequisites from being loaded:</p>
+<p>Default token renderer prototypes require <span class="latex">L<sup>a</sup>T<sub>e</sub>X</span> packages that may clash with other packages used in a document. Additionally, if we redefine token renderers and renderer prototypes ourselves, the default definitions will bring no benefit to us. Using the <code>plain</code> package option, we can keep the default definitions from the plain <span class="tex">T<sub>e</sub>X</span> implementation and prevent the soft <span class="latex">L<sup>a</sup>T<sub>e</sub>X</span> prerequisites from being loaded: The plain option must be set before or when loading the package. Setting the option after loading the package will have no effect.</p>
 <div class="sourceCode" id="cb340"><pre class="sourceCode tex"><code class="sourceCode latex"><span id="cb340-1"><a href="#cb340-1" aria-hidden="true"></a><span class="bu">\usepackage</span>[plain]{<span class="ex">markdown</span>}</span></code></pre></div>
 <h4 data-number="2.2.3.3" id="latexthemes"><span class="header-section-number">2.2.3.3</span> <span class="latex">L<sup>a</sup>T<sub>e</sub>X</span> themes</h4>
 <p>User-contributed <span class="latex">L<sup>a</sup>T<sub>e</sub>X</span> themes for the Markdown package provide a domain-specific interpretation of some Markdown tokens. Similarly to <span class="latex">L<sup>a</sup>T<sub>e</sub>X</span> packages, themes allow the authors to achieve a specific look and other high-level goals without low-level programming.</p>

Modified: trunk/Master/texmf-dist/doc/generic/markdown/markdown.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/latex/markdown/examples/latex.tex
===================================================================
--- trunk/Master/texmf-dist/doc/latex/markdown/examples/latex.tex	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/doc/latex/markdown/examples/latex.tex	2022-06-29 20:10:26 UTC (rev 63757)
@@ -52,7 +52,7 @@
 \end{markdown}
 
 \begin{markdown*}{html, hybrid}
-Here is some <b>HTML code</b> mixed *with Markdown*. In pdf \TeX, the HTML code
+Here is some <b>HTML code</b> mixed *with Markdown*. In \TeX, the HTML code
 will be silently ignored, whereas in \TeX 4ht, the HTML code will be passed
 through to the output:
 

Modified: trunk/Master/texmf-dist/scripts/markdown/markdown-cli.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/markdown/markdown-cli.lua	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/scripts/markdown/markdown-cli.lua	2022-06-29 20:10:26 UTC (rev 63757)
@@ -58,7 +58,7 @@
 -- those in the standard .ins files.
 -- 
 local metadata = {
-    version   = "2.15.2-0-gb238dbc",
+    version   = "2.15.3-0-g7c8e03d",
     comment   = "A module for the conversion from markdown to plain TeX",
     author    = "John MacFarlane, Hans Hagen, Vít Novotný",
     copyright = {"2009-2016 John MacFarlane, Hans Hagen",

Modified: trunk/Master/texmf-dist/source/generic/markdown/docstrip.cfg
===================================================================
--- trunk/Master/texmf-dist/source/generic/markdown/docstrip.cfg	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/source/generic/markdown/docstrip.cfg	2022-06-29 20:10:26 UTC (rev 63757)
@@ -62,18 +62,15 @@
 \let\luapostamble\empty
 \declarepreamble\texpreamble
 
-Copyright (C) 2016-2021 Vít Novotný
+Copyright (C) 2016-2022 Vít Novotný
 
 This work may be distributed and/or modified under the
-conditions of the LaTeX Project Public License, either version 1.3
+conditions of the LaTeX Project Public License, either version 1.3c
 of this license or (at your option) any later version.
 The latest version of this license is in
 
    http://www.latex-project.org/lppl.txt
 
-and version 1.3 or later is part of all distributions of LaTeX
-version 2005/12/01 or later.
-
 This work has the LPPL maintenance status `maintained'.
 The Current Maintainer of this work is Vít Novotný.
 

Modified: trunk/Master/texmf-dist/source/generic/markdown/markdown.dtx
===================================================================
--- trunk/Master/texmf-dist/source/generic/markdown/markdown.dtx	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/source/generic/markdown/markdown.dtx	2022-06-29 20:10:26 UTC (rev 63757)
@@ -1,7 +1,6 @@
 % \iffalse
-%<@@=markdown>
 %<*driver>
-\documentclass{ltxdockit}
+\documentclass[nohyperref]{ltxdockit}
 \usepackage[american]{babel}
 \usepackage{amsmath,btxdockit,doc,fancyvrb,graphicx,hologo,microtype,minted}
 
@@ -99,9 +98,9 @@
 \newunicodechar{☒}{\markdownRendererTickedBox}
 \newunicodechar{⌛}{\markdownRendererHalfTickedBox}
 \newunicodechar{☐}{\markdownRendererUntickedBox}
+\usepackage{emoji}
 \makeatletter
 \@ifpackagelater{emoji}{2020/03/16}{
-  \usepackage{emoji}
   \newunicodechar{😉}{\emoji{winking-face}}
 }{
   \newunicodechar{😉}{;-)}
@@ -812,8 +811,6 @@
   #1\footnote{See \url{#3}.}}
 \RequirePackage{varioref}
 \vrefwarning
-\def\markdownLaTeXRendererRelativeLink#1{%
-  \ref{#1}}
 \markdownSetupSnippet{options}{
   rendererPrototypes = {
     dlBegin = {\begin{optionlist}},
@@ -1106,15 +1103,18 @@
 %
 % The plain \TeX{} part of the package requires that the plain \TeX{}
 % format (or its superset) is loaded, all the Lua prerequisites (see
-% Section <#sec:luaprerequisites>), and the following package:
+% Section <#sec:luaprerequisites>), and the following packages:
 %
 % \pkg{expl3}
 %
 %:    A package that enables the expl3 language from the \LaTeX3 kernel in
-%     \TeX{} Live${}\leq{}2019$. It is not used for anything... yet.
+%     \TeX{} Live${}\leq{}2019$. It is used to implement reflection
+%     capabilities that allow us to enumerate and inspect high-level concepts
+%     such as options, renderers, and renderer prototypes.
 %
 % \end{markdown}
 %  \begin{macrocode}
+%<@@=markdown>
 \ifx\ExplSyntaxOn\undefined
   \input expl3-generic\relax
 \fi
@@ -1121,6 +1121,12 @@
 %    \end{macrocode}
 % \begin{markdown}
 %
+% \pkg{lt3luabridge}
+%
+%:    A package that allows us to execute Lua code with LuaTeX as well as
+%     with other TeX engines that provide the *shell escape* capability,
+%     which allows them to execute code with the system's shell.
+%
 % The plain \TeX{} part of the package also requires the following Lua module:
 %
 % \pkg{Lua File System}
@@ -1842,6 +1848,229 @@
 %  \begin{macrocode}
 \prop_new:N \g_@@_lua_option_types_prop
 \prop_new:N \g_@@_default_lua_options_prop
+\seq_new:N \g_@@_option_layers_seq
+\tl_const:Nn \c_@@_option_layer_lua_tl { lua }
+\seq_put_right:NV \g_@@_option_layers_seq \c_@@_option_layer_lua_tl
+\cs_new:Nn
+  \@@_add_lua_option:nnn
+  {
+    \@@_add_option:Vnnn
+      \c_@@_option_layer_lua_tl
+      { #1 }
+      { #2 }
+      { #3 }
+  }
+\cs_new:Nn
+  \@@_add_option:nnnn
+  {
+    \seq_put_right:cn
+      { g_@@_ #1 _options_seq }
+      { #2 }
+    \prop_put:cnn
+      { g_@@_ #1 _option_types_prop }
+      { #2 }
+      { #3 }
+    \prop_put:cnn
+      { g_@@_default_ #1 _options_prop }
+      { #2 }
+      { #4 }
+    \@@_typecheck_option:n
+      { #2 }
+  }
+\cs_generate_variant:Nn
+  \@@_add_option:nnnn
+  { Vnnn }
+\tl_const:Nn \c_@@_option_value_true_tl  { true  }
+\tl_const:Nn \c_@@_option_value_false_tl { false }
+\cs_new:Nn \@@_typecheck_option:n
+  {
+    \@@_get_option_type:nN
+      { #1 }
+      \l_tmpa_tl
+    \str_case_e:Vn
+      \l_tmpa_tl
+      {
+        { \c_@@_option_type_boolean_tl }
+          {
+            \@@_get_option_value:nN
+              { #1 }
+              \l_tmpa_tl
+            \bool_if:nF
+              {
+                \str_if_eq_p:VV
+                  \l_tmpa_tl
+                  \c_@@_option_value_true_tl ||
+                \str_if_eq_p:VV
+                  \l_tmpa_tl
+                  \c_@@_option_value_false_tl
+              }
+              {
+                \msg_error:nnnV
+                  { @@ }
+                  { failed-typecheck-for-boolean-option }
+                  { #1 }
+                  \l_tmpa_tl
+              }
+          }
+      }
+  }
+\msg_new:nnn
+  { @@ }
+  { failed-typecheck-for-boolean-option }
+  {
+    Option~#1~has~value~#2,~
+    but~a~boolean~(true~or~false)~was~expected.
+  }
+\cs_generate_variant:Nn
+  \str_case_e:nn
+  { Vn }
+\cs_generate_variant:Nn
+  \msg_error:nnnn
+  { nnnV }
+\seq_new:N \g_@@_option_types_seq
+\tl_const:Nn \c_@@_option_type_counter_tl { counter }
+\seq_put_right:NV \g_@@_option_types_seq \c_@@_option_type_counter_tl
+\tl_const:Nn \c_@@_option_type_boolean_tl { boolean }
+\seq_put_right:NV \g_@@_option_types_seq \c_@@_option_type_boolean_tl
+\tl_const:Nn \c_@@_option_type_number_tl  { number  }
+\seq_put_right:NV \g_@@_option_types_seq \c_@@_option_type_number_tl
+\tl_const:Nn \c_@@_option_type_path_tl    { path    }
+\seq_put_right:NV \g_@@_option_types_seq \c_@@_option_type_path_tl
+\tl_const:Nn \c_@@_option_type_slice_tl   { slice   }
+\seq_put_right:NV \g_@@_option_types_seq \c_@@_option_type_slice_tl
+\tl_const:Nn \c_@@_option_type_string_tl  { string  }
+\seq_put_right:NV \g_@@_option_types_seq \c_@@_option_type_string_tl
+\cs_new:Nn
+  \@@_get_option_type:nN
+  {
+    \bool_set_false:N
+      \l_tmpa_bool
+    \seq_map_inline:Nn
+      \g_@@_option_layers_seq
+      {
+        \prop_get:cnNT
+          { g_@@_ ##1 _option_types_prop }
+          { #1 }
+          \l_tmpa_tl
+          {
+            \bool_set_true:N
+              \l_tmpa_bool
+            \seq_map_break:
+          }
+      }
+    \bool_if:nF
+      \l_tmpa_bool
+      {
+        \msg_error:nnn
+          { @@ }
+          { undefined-option }
+          { #1 }
+      }
+    \seq_if_in:NVF
+      \g_@@_option_types_seq
+      \l_tmpa_tl
+      {
+        \msg_error:nnnV
+          { @@ }
+          { unknown-option-type }
+          { #1 }
+          \l_tmpa_tl
+      }
+    \tl_set_eq:NN
+      #2
+      \l_tmpa_tl
+  }
+\msg_new:nnn
+  { @@ }
+  { unknown-option-type }
+  {
+    Option~#1~has~unknown~type~#2.
+  }
+\msg_new:nnn
+  { @@ }
+  { undefined-option }
+  {
+    Option~#1~is~undefined.
+  }
+\cs_new:Nn
+  \@@_get_default_option_value:nN
+  {
+    \bool_set_false:N
+      \l_tmpa_bool
+    \seq_map_inline:Nn
+      \g_@@_option_layers_seq
+      {
+        \prop_get:cnNT
+          { g_@@_default_ ##1 _options_prop }
+          { #1 }
+          #2
+          {
+            \bool_set_true:N
+              \l_tmpa_bool
+            \seq_map_break:
+          }
+      }
+    \bool_if:nF
+      \l_tmpa_bool
+      {
+        \msg_error:nnn
+          { @@ }
+          { undefined-option }
+          { #1 }
+      }
+  }
+\cs_new:Nn
+  \@@_get_option_value:nN
+  {
+    \@@_option_tl_to_csname:nN
+      { #1 }
+      \l_tmpa_tl
+    \cs_if_free:cTF
+      { \l_tmpa_tl }
+      {
+        \@@_get_default_option_value:nN
+          { #1 }
+          #2
+      }
+      {
+        \@@_get_option_type:nN
+          { #1 }
+          \l_tmpa_tl
+        \str_if_eq:NNTF
+          \c_@@_option_type_counter_tl
+          \l_tmpa_tl
+          {
+            \@@_option_tl_to_csname:nN
+              { #1 }
+              \l_tmpa_tl
+            \tl_set:Nx
+              #2
+              { \the \cs:w \l_tmpa_tl \cs_end: }
+          }
+          {
+            \@@_option_tl_to_csname:nN
+              { #1 }
+              \l_tmpa_tl
+            \tl_set:Nv
+              #2
+              { \l_tmpa_tl }
+          }
+      }
+  }
+\cs_new:Nn \@@_option_tl_to_csname:nN
+  {
+    \tl_set:Nn
+      \l_tmpa_tl
+%     TODO: Replace with \str_uppercase:n in TeX Live 2020.
+      { \str_upper_case:n { #1 } }
+    \tl_set:Nx
+      #2
+      {
+        markdownOption
+        \tl_head:f { \l_tmpa_tl }
+        \tl_tail:n { #1 }
+      }
+  }
 %    \end{macrocode}
 % \iffalse
 %</tex>
@@ -1848,9 +2077,9 @@
 %<*manual-options>
 % \fi
 % \begin{markdown}
-% 
+%
 %### File and Directory Names
-% 
+%
 % \end{markdown}
 % \par
 % \iffalse
@@ -1998,17 +2227,10 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { cacheDir }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { cacheDir }
-  { string }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { cacheDir }
-  { . }
+  { path }
+  { \markdownOptionOutputDir / _markdown_\jobname }
 %    \end{macrocode}
 % \iffalse
 %</tex>
@@ -2233,17 +2455,10 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { frozenCacheFileName }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { frozenCacheFileName }
-  { string }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { frozenCacheFileName }
-  { frozenCache.tex }
+  { path }
+  { \markdownOptionCacheDir / frozenCache.tex }
 %    \end{macrocode}
 % \iffalse
 %</tex>
@@ -2464,16 +2679,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { blankBeforeBlockquote }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { blankBeforeBlockquote }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { blankBeforeBlockquote }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -2720,16 +2928,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { blankBeforeCodeFence }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { blankBeforeCodeFence }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { blankBeforeCodeFence }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -2959,16 +3160,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { blankBeforeHeading }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { blankBeforeHeading }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { blankBeforeHeading }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -3190,16 +3384,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { breakableBlockquotes }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { breakableBlockquotes }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { breakableBlockquotes }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -3291,16 +3478,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { citationNbsps }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { citationNbsps }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { citationNbsps }
   { true }
 %    \end{macrocode}
 % \iffalse
@@ -3326,7 +3506,7 @@
 %
 :    true
 
-     :  Enable the pandoc citation syntax extension:
+     :  Enable the Pandoc citation syntax extension:
 
         ``` md
         Here is a simple parenthetical citation [@doe99] and here
@@ -3346,7 +3526,7 @@
 
 :    false
 
-     :  Disable the pandoc citation syntax extension.
+     :  Disable the Pandoc citation syntax extension.
 
 % \end{markdown}
 % \iffalse
@@ -3399,16 +3579,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { citations }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { citations }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { citations }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -3624,16 +3797,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { codeSpans }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { codeSpans }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { codeSpans }
   { true }
 %    \end{macrocode}
 % \iffalse
@@ -3805,16 +3971,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { contentBlocks }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { contentBlocks }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { contentBlocks }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -3838,7 +3997,7 @@
 %
 % \Valitem[markdown-languages.json]{contentBlocksLanguageMap}{filename}
 %
-:    The filename of the JSON file that maps filename extensions to
+:    The filename of the \acro{JSON} file that maps filename extensions to
      programming language names in the iA\,Writer content blocks.
 %    See Section <#sec:texcontentblockrenderers> for more information.
 
@@ -3972,16 +4131,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { contentBlocksLanguageMap }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { contentBlocksLanguageMap }
-  { string }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { contentBlocksLanguageMap }
+  { path }
   { markdown-languages.json }
 %    \end{macrocode}
 % \iffalse
@@ -4118,16 +4270,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { definitionLists }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { definitionLists }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { definitionLists }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -4226,16 +4371,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { eagerCache }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { eagerCache }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { eagerCache }
   { true }
 %    \end{macrocode}
 % \iffalse
@@ -4361,16 +4499,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { expectJekyllData }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { expectJekyllData }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { expectJekyllData }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -4536,16 +4667,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { fencedCode }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { fencedCode }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { fencedCode }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -4707,16 +4831,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { finalizeCache }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { finalizeCache }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { finalizeCache }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -4742,7 +4859,7 @@
 %
 :    true
 
-     :  Enable the pandoc footnote syntax extension:
+     :  Enable the Pandoc footnote syntax extension:
 
         ``` md
         Here is a footnote reference,[^1] and another.[^longnote]
@@ -4766,7 +4883,7 @@
 
 :    false
 
-     :    Disable the pandoc footnote syntax extension.
+     :    Disable the Pandoc footnote syntax extension.
 
 % \end{markdown}
 % \iffalse
@@ -4883,16 +5000,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { footnotes }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { footnotes }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { footnotes }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -4955,16 +5065,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { frozenCacheCounter }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { frozenCacheCounter }
   { counter }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { frozenCacheCounter }
   { 0 }
 %    \end{macrocode}
 % \iffalse
@@ -5046,16 +5149,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { hardLineBreaks }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { hardLineBreaks }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { hardLineBreaks }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -5184,16 +5280,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { hashEnumerators }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { hashEnumerators }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { hashEnumerators }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -5246,16 +5335,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { headerAttributes }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { headerAttributes }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { headerAttributes }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -5515,16 +5597,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { html }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { html }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { html }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -5733,16 +5808,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { hybrid }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { hybrid }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { hybrid }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -5768,7 +5836,7 @@
 %
 :    true
 
-     :  Enable the pandoc inline footnote syntax extension:
+     :  Enable the Pandoc inline footnote syntax extension:
 
         ``` md
         Here is an inline note.^[Inlines notes are easier to
@@ -5778,7 +5846,7 @@
 
 :    false
 
-     :  Disable the pandoc inline footnote syntax extension.
+     :  Disable the Pandoc inline footnote syntax extension.
 
 % \end{markdown}
 % \iffalse
@@ -5839,16 +5907,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { inlineFootnotes }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { inlineFootnotes }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { inlineFootnotes }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -6014,16 +6075,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { jekyllData }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { jekyllData }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { jekyllData }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -6049,7 +6103,7 @@
 %
 :    true
 
-     :  Enable the \acro{PHP} Markdown table syntax extension:
+     :  Enable the \acro{PHP} Markdown pipe table syntax extension:
 
         ``` md
         | Right | Left | Default | Center |
@@ -6061,7 +6115,7 @@
 
 :    false
 
-     :  Disable the \acro{PHP} Markdown table syntax extension.
+     :  Disable the \acro{PHP} Markdown pipe table syntax extension.
 
 % \end{markdown}
 % \iffalse
@@ -6130,16 +6184,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { pipeTables }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { pipeTables }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { pipeTables }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -6177,16 +6224,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { preserveTabs }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { preserveTabs }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { preserveTabs }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -6273,16 +6313,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { relativeReferences }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { relativeReferences }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { relativeReferences }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -6425,16 +6458,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { shiftHeadings }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { shiftHeadings }
   { number }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { shiftHeadings }
   { 0 }
 %    \end{macrocode}
 % \iffalse
@@ -6650,16 +6676,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { slice }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
   { slice }
-  { string }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { slice }
   { ^~$ }
 %    \end{macrocode}
 % \iffalse
@@ -6864,16 +6883,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { smartEllipses }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { smartEllipses }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { smartEllipses }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -7007,16 +7019,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { startNumber }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { startNumber }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { startNumber }
   { true }
 %    \end{macrocode}
 % \iffalse
@@ -7127,16 +7132,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { stripIndent }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { stripIndent }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { stripIndent }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -7258,16 +7256,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { tableCaptions }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { tableCaptions }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { tableCaptions }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -7375,16 +7366,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { taskLists }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { taskLists }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { taskLists }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -7498,16 +7482,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { texComments }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { texComments }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { texComments }
   { false }
 %    \end{macrocode}
 % \iffalse
@@ -7534,7 +7511,7 @@
 %
 :    true
 
-     :   Unordered and ordered Lists whose items do not consist of multiple
+     :   Unordered and ordered lists whose items do not consist of multiple
          paragraphs will be considered *tight*. Tight lists will produce tight
          renderers that may produce different output than lists that are not
          tight:
@@ -7627,16 +7604,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { tightLists }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { tightLists }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { tightLists }
   { true }
 %    \end{macrocode}
 % \iffalse
@@ -7776,16 +7746,9 @@
 %<*tex>
 % \fi
 %  \begin{macrocode}
-\seq_put_right:Nn
-  \g_@@_lua_options_seq
+\@@_add_lua_option:nnn
   { underscores }
-\prop_put:Nnn
-  \g_@@_lua_option_types_prop
-  { underscores }
   { boolean }
-\prop_put:Nnn
-  \g_@@_default_lua_options_prop
-  { underscores }
   { true }
 \ExplSyntaxOff
 %    \end{macrocode}
@@ -8179,7 +8142,36 @@
 % Section <#sec:luaoptions>), while some of them are specific to the plain
 % \TeX{} interface.
 %
+% To enable the enumeration of plain \TeX{} options, we will maintain the
+% \mdef{g_\@\@_plain_tex_options_seq} sequence.
+%
 % \end{markdown}
+%  \begin{macrocode}
+\ExplSyntaxOn
+\seq_new:N \g_@@_plain_tex_options_seq
+%    \end{macrocode}
+% \begin{markdown}
+%
+% To enable the reflection of default plain \TeX{} options and their types, we
+% will maintain the \mdef{g_\@\@_default_plain_tex_options_prop} and
+% \mdef{g_\@\@_plain_tex_option_types_prop} property lists, respectively.
+%
+% \end{markdown}
+%  \begin{macrocode}
+\prop_new:N \g_@@_plain_tex_option_types_prop
+\prop_new:N \g_@@_default_plain_tex_options_prop
+\tl_const:Nn \c_@@_option_layer_plain_tex_tl { plain_tex }
+\seq_put_right:NV \g_@@_option_layers_seq \c_@@_option_layer_plain_tex_tl
+\cs_new:Nn
+  \@@_add_plain_tex_option:nnn
+  {
+    \@@_add_option:Vnnn
+      \c_@@_option_layer_plain_tex_tl
+      { #1 }
+      { #2 }
+      { #3 }
+  }
+%    \end{macrocode}
 % \iffalse
 %</tex>
 %<*manual-options>
@@ -8214,11 +8206,6 @@
 % between an enumeration of the markdown documents in the plain \TeX{} document
 % and their auxiliary files cached in the \Opt{cacheDir} directory.
 %
-% \end{markdown}
-%  \begin{macrocode}
-\let\markdownOptionFinalizeCache\undefined
-%    \end{macrocode}
-% \par
 % \iffalse
 %</tex>
 %<*manual-options>
@@ -8226,7 +8213,6 @@
 #### Finalizing and Freezing the Cache
 
 % \fi
-% \begin{markdown}
 %
 The \mdef{markdownOptionFrozenCache} option uses the mapping previously
 % created by the \mref{markdownOptionFinalizeCache} option,
@@ -8238,6 +8224,23 @@
 in the order and the content of markdown documents will not be reflected. It
 defaults to `false`.
 
+% \end{markdown}
+% \iffalse
+%</manual-options>
+%<*tex>
+% \fi
+%  \begin{macrocode}
+\@@_add_plain_tex_option:nnn
+  { frozenCache }
+  { boolean }
+  { false }
+%    \end{macrocode}
+% \iffalse
+%</tex>
+%<*manual-options>
+% \fi
+% \begin{markdown}
+%
 % The standard usage of the above two options is as follows:
 % \iffalse
 The standard usage of the \Opt{finalizeCache} and \Opt{frozenCache}
@@ -8280,20 +8283,41 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownOptionHelperScriptFileName{\jobname.markdown.lua}%
+\@@_add_plain_tex_option:nnn
+  { helperScriptFileName }
+  { path }
+  { \jobname.markdown.lua }
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
+% The \mref{markdownOptionHelperScriptFileName} macro has been deprecated and
+% will be removed in Markdown 3.0.0. To control the filename of the helper Lua
+% script file, use the \mref{g_luabridge_helper_script_filename_str} macro
+% from the \pkg{lt3luabridge} package.
+%
+% \end{markdown}
+%  \begin{macrocode}
+\str_new:N
+  \g_luabridge_helper_script_filename_str
+\tl_gset:Nn
+  \g_luabridge_helper_script_filename_str
+  { \markdownOptionHelperScriptFileName }
+%    \end{macrocode}
+% \begin{markdown}
+%
 % The \mdef{markdownOptionInputTempFileName} macro sets the filename of the
-% temporary input file that is created during the conversion from markdown to
-% plain \TeX{} in \mref{markdownMode} other than `2`. It defaults to
-% \mref{jobname}`.markdown.in`. The same limitations as in the case of the
-% \mref{markdownOptionHelperScriptFileName} macro apply here.
+% temporary input file that is created during the buffering of markdown text
+% from a \TeX{} source. It defaults to \mref{jobname}`.markdown.in`. The same
+% limitations as in the case of the \mref{markdownOptionHelperScriptFileName}
+% macro apply here.
 %
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownOptionInputTempFileName{\jobname.markdown.in}%
+\@@_add_plain_tex_option:nnn
+  { inputTempFileName }
+  { path }
+  { \jobname.markdown.in }
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -8300,17 +8324,35 @@
 %
 % The \mdef{markdownOptionOutputTempFileName} macro sets the filename of the
 % temporary output file that is created during the conversion from markdown to
-% plain \TeX{} in \mref{markdownMode} other than `2`. It defaults to
+% plain \TeX{} in \mref{markdownMode} other than `2` It defaults to
 % \mref{jobname}`.markdown.out`. The same limitations apply here as in the case
 % of the \mref{markdownOptionHelperScriptFileName} macro.
 %
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownOptionOutputTempFileName{\jobname.markdown.out}%
+\@@_add_plain_tex_option:nnn
+  { outputTempFileName }
+  { path }
+  { \jobname.markdown.out }
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
+% The \mref{markdownOptionOutputTempFileName} macro has been deprecated and
+% will be removed in Markdown 3.0.0. To control the filename of the temporary
+% file for Lua output, use the \mref{g_luabridge_error_output_filename_str}
+% macro from the \pkg{lt3luabridge} package.
+%
+% \end{markdown}
+%  \begin{macrocode}
+\str_new:N
+  \g_luabridge_standard_output_filename_str
+\tl_gset:Nn
+  \g_luabridge_standard_output_filename_str
+  { \markdownOptionOutputTempFileName }
+%    \end{macrocode}
+% \begin{markdown}
+%
 % The \mdef{markdownOptionErrorTempFileName} macro sets the filename of the
 % temporary output file that is created when a Lua error is encountered during
 % the conversion from markdown to plain \TeX{} in \mref{markdownMode} other than
@@ -8320,11 +8362,29 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownOptionErrorTempFileName{\jobname.markdown.err}%
+\@@_add_plain_tex_option:nnn
+  { errorTempFileName }
+  { path }
+  { \jobname.markdown.err }
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
+% The \mref{markdownOptionErrorTempFileName} macro has been deprecated and
+% will be removed in Markdown 3.0.0. To control the filename of the temporary
+% file for Lua errors, use the \mref{g_luabridge_error_output_filename_str}
+% macro from the \pkg{lt3luabridge} package.
+%
+% \end{markdown}
+%  \begin{macrocode}
+\str_new:N
+  \g_luabridge_error_output_filename_str
+\tl_gset:Nn
+  \g_luabridge_error_output_filename_str
+  { \markdownOptionErrorTempFileName }
+%    \end{macrocode}
+% \begin{markdown}
+%
 % The \mdef{markdownOptionOutputDir} macro sets the path to the directory that
 % will contain the auxiliary cache files produced by the Lua implementation and
 % also the auxiliary files produced by the plain \TeX{} implementation. The
@@ -8338,35 +8398,119 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownOptionOutputDir{.}%
+\@@_add_plain_tex_option:nnn
+  { outputDir }
+  { path }
+  { . }
 %    \end{macrocode}
-% \par
 % \begin{markdown}
 %
-% The \mdef{markdownOptionCacheDir} macro corresponds to the Lua interface
-% \Opt{cacheDir} option that sets the path to the directory that will contain
-% the produced cache files. The option defaults to `_markdown_`\mref{jobname},
-% which is a similar naming scheme to the one used by the \pkg{minted} \LaTeX{}
-% package. The same limitations apply here as in the case of the
-% \mref{markdownOptionHelperScriptFileName} macro.
+% The \mref{markdownOptionOutputDir} macro is also used to set the
+% \mref{g_luabridge_output_dirname_str} macro from the \pkg{lt3luabridge}
+% package.
 %
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownOptionCacheDir{\markdownOptionOutputDir/_markdown_\jobname}%
+\str_new:N
+  \g_luabridge_output_dirname_str
+\tl_gset:Nn
+  \g_luabridge_output_dirname_str
+  { \markdownOptionOutputDir }
 %    \end{macrocode}
-% \par
 % \begin{markdown}
 %
-% The \mdef{markdownOptionFrozenCacheFileName} macro corresponds to the Lua
-% interface \Opt{frozenCacheFileName} option that sets the path to an output
-% file (frozen cache) that will contain a mapping between an enumeration of the
-% markdown documents in the plain \TeX{} document and their auxiliary cache
-% files. The option defaults to `frozenCache.tex`. The same limitations apply
-% here as in the case of the \mref{markdownOptionHelperScriptFileName} macro.
+% Here, we automatically define plain \TeX{} macros for the above plain \TeX{}
+% options.
 %
+% Furthemore, we also define macros that map directly to the options recognized
+% by the Lua interface, such as \mdef{markdownOptionHybrid} for the
+% \Opt{hybrid} Lua option (see Section <#sec:luaoptions>), which are not
+% processed by the plain \TeX{} implementation, only passed along to Lua.
+%
+% For the macros that correspond to the non-boolean options recognized by the
+% Lua interface, the same limitations apply here in the case of the
+% \mref{markdownOptionHelperScriptFileName} macro.
+%
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownOptionFrozenCacheFileName{\markdownOptionCacheDir/frozenCache.tex}
+\cs_new:Nn \@@_plain_tex_define_option_commands:
+  {
+    \seq_map_inline:Nn
+      \g_@@_option_layers_seq
+      {
+        \seq_map_inline:cn
+          { g_@@_ ##1 _options_seq }
+          {
+              \@@_plain_tex_define_option_command:n
+                { ####1 }
+          }
+      }
+  }
+\cs_new:Nn \@@_plain_tex_define_option_command:n
+  {
+    \@@_get_default_option_value:nN
+      { #1 }
+      \l_tmpa_tl
+    \@@_set_option_value:nV
+      { #1 }
+      \l_tmpa_tl
+  }
+\cs_new:Nn
+  \@@_set_option_value:nn
+  {
+    \@@_define_option:n
+      { #1 }
+    \@@_get_option_type:nN
+      { #1 }
+      \l_tmpa_tl
+    \str_if_eq:NNTF
+      \c_@@_option_type_counter_tl
+      \l_tmpa_tl
+      {
+        \@@_option_tl_to_csname:nN
+          { #1 }
+          \l_tmpa_tl
+        \int_gset:cn
+          { \l_tmpa_tl }
+          { #2 }
+      }
+      {
+        \@@_option_tl_to_csname:nN
+          { #1 }
+          \l_tmpa_tl
+        \cs_set:cpn
+          { \l_tmpa_tl }
+          { #2 }
+      }
+  }
+\cs_generate_variant:Nn
+  \@@_set_option_value:nn
+  { nV }
+\cs_new:Nn
+  \@@_define_option:n
+  {
+    \@@_option_tl_to_csname:nN
+      { #1 }
+      \l_tmpa_tl
+    \cs_if_free:cT
+      { \l_tmpa_tl }
+      {
+        \@@_get_option_type:nN
+          { #1 }
+          \l_tmpb_tl
+        \str_if_eq:NNT
+          \c_@@_option_type_counter_tl
+          \l_tmpb_tl
+          {
+            \@@_option_tl_to_csname:nN
+              { #1 }
+              \l_tmpa_tl
+            \int_new:c
+              { \l_tmpa_tl }
+          }
+      }
+  }
+\@@_plain_tex_define_option_commands:
 %    \end{macrocode}
 %
 % \iffalse
@@ -8433,61 +8577,6 @@
 while executing the Lua code. If this happens, please [file a
 bug](https://github.com/witiko/markdown/issues).
 
-%</manual-options>
-%<*tex>
-% \fi
-% \par
-% \begin{markdown}
-%
-%#### Lua Interface Options
-% The following macros map directly to the options recognized by the Lua
-% interface (see Section <#sec:luaoptions>) and are not processed by the
-% plain \TeX{} implementation, only passed along to Lua. They are undefined, which
-% makes them fall back to the default values provided by the Lua interface.
-%
-% For the macros that correspond to the non-boolean options recognized by the
-% Lua interface, the same limitations apply here in the case of the
-% \mref{markdownOptionHelperScriptFileName} macro.
-%
-% \end{markdown}
-%  \begin{macrocode}
-\let\markdownOptionBlankBeforeBlockquote\undefined
-\let\markdownOptionBlankBeforeCodeFence\undefined
-\let\markdownOptionBlankBeforeHeading\undefined
-\let\markdownOptionBreakableBlockquotes\undefined
-\let\markdownOptionCitations\undefined
-\let\markdownOptionCitationNbsps\undefined
-\let\markdownOptionContentBlocks\undefined
-\let\markdownOptionContentBlocksLanguageMap\undefined
-\let\markdownOptionDefinitionLists\undefined
-\let\markdownOptionEagerCache\undefined
-\let\markdownOptionFootnotes\undefined
-\let\markdownOptionFencedCode\undefined
-\let\markdownOptionHardLineBreaks\undefined
-\let\markdownOptionHashEnumerators\undefined
-\let\markdownOptionHeaderAttributes\undefined
-\let\markdownOptionHtml\undefined
-\let\markdownOptionHybrid\undefined
-\let\markdownOptionInlineFootnotes\undefined
-\let\markdownOptionJekyllData\undefined
-\let\markdownOptionPipeTables\undefined
-\let\markdownOptionPreserveTabs\undefined
-\let\markdownOptionRelativeReferences\undefined
-\let\markdownOptionShiftHeadings\undefined
-\let\markdownOptionSlice\undefined
-\let\markdownOptionSmartEllipses\undefined
-\let\markdownOptionStartNumber\undefined
-\let\markdownOptionStripIndent\undefined
-\let\markdownOptionTableCaptions\undefined
-\let\markdownOptionTaskLists\undefined
-\let\markdownOptionTexComments\undefined
-\let\markdownOptionTightLists\undefined
-%    \end{macrocode}
-% \par
-% \iffalse
-%</tex>
-%<*manual-options>
-
 #### Package Documentation
 
 The \mdef{markdownOptionStripPercentSigns} macro controls whether a percent
@@ -8540,7 +8629,18 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownOptionStripPercentSigns{false}%
+\seq_put_right:Nn
+  \g_@@_plain_tex_options_seq
+  { stripPercentSigns }
+\prop_put:Nnn
+  \g_@@_plain_tex_option_types_prop
+  { stripPercentSigns }
+  { boolean }
+\prop_put:Nnx
+  \g_@@_default_plain_tex_options_prop
+  { stripPercentSigns }
+  { false }
+\ExplSyntaxOff
 %    \end{macrocode}
 % \iffalse
 %</tex>
@@ -14180,84 +14280,58 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-\def\markdownRendererAttributeIdentifierPrototype#1{}%
-\def\markdownRendererAttributeClassNamePrototype#1{}%
-\def\markdownRendererAttributeKeyValuePrototype#1#2{}%
-\def\markdownRendererDocumentBeginPrototype{}%
-\def\markdownRendererDocumentEndPrototype{}%
-\def\markdownRendererInterblockSeparatorPrototype{}%
-\def\markdownRendererLineBreakPrototype{}%
-\def\markdownRendererEllipsisPrototype{}%
-\def\markdownRendererHeaderAttributeContextBeginPrototype{}%
-\def\markdownRendererHeaderAttributeContextEndPrototype{}%
-\def\markdownRendererNbspPrototype{}%
-\def\markdownRendererLeftBracePrototype{}%
-\def\markdownRendererRightBracePrototype{}%
-\def\markdownRendererDollarSignPrototype{}%
-\def\markdownRendererPercentSignPrototype{}%
-\def\markdownRendererAmpersandPrototype{}%
-\def\markdownRendererUnderscorePrototype{}%
-\def\markdownRendererHashPrototype{}%
-\def\markdownRendererCircumflexPrototype{}%
-\def\markdownRendererBackslashPrototype{}%
-\def\markdownRendererTildePrototype{}%
-\def\markdownRendererPipePrototype{}%
-\def\markdownRendererCodeSpanPrototype#1{}%
-\def\markdownRendererLinkPrototype#1#2#3#4{}%
-\def\markdownRendererImagePrototype#1#2#3#4{}%
-\def\markdownRendererContentBlockPrototype#1#2#3#4{}%
-\def\markdownRendererContentBlockOnlineImagePrototype#1#2#3#4{}%
-\def\markdownRendererContentBlockCodePrototype#1#2#3#4#5{}%
-\def\markdownRendererUlBeginPrototype{}%
-\def\markdownRendererUlBeginTightPrototype{}%
-\def\markdownRendererUlItemPrototype{}%
-\def\markdownRendererUlItemEndPrototype{}%
-\def\markdownRendererUlEndPrototype{}%
-\def\markdownRendererUlEndTightPrototype{}%
-\def\markdownRendererOlBeginPrototype{}%
-\def\markdownRendererOlBeginTightPrototype{}%
-\def\markdownRendererOlItemPrototype{}%
-\def\markdownRendererOlItemWithNumberPrototype#1{}%
-\def\markdownRendererOlItemEndPrototype{}%
-\def\markdownRendererOlEndPrototype{}%
-\def\markdownRendererOlEndTightPrototype{}%
-\def\markdownRendererDlBeginPrototype{}%
-\def\markdownRendererDlBeginTightPrototype{}%
-\def\markdownRendererDlItemPrototype#1{}%
-\def\markdownRendererDlItemEndPrototype{}%
-\def\markdownRendererDlDefinitionBeginPrototype{}%
-\def\markdownRendererDlDefinitionEndPrototype{}%
-\def\markdownRendererDlEndPrototype{}%
-\def\markdownRendererDlEndTightPrototype{}%
-\def\markdownRendererEmphasisPrototype#1{}%
-\def\markdownRendererStrongEmphasisPrototype#1{}%
-\def\markdownRendererBlockQuoteBeginPrototype{}%
-\def\markdownRendererBlockQuoteEndPrototype{}%
-\def\markdownRendererInputVerbatimPrototype#1{}%
-\def\markdownRendererInputFencedCodePrototype#1#2{}%
-\def\markdownRendererJekyllDataBeginPrototype{}%
-\def\markdownRendererJekyllDataEndPrototype{}%
-\def\markdownRendererHeadingOnePrototype#1{}%
-\def\markdownRendererHeadingTwoPrototype#1{}%
-\def\markdownRendererHeadingThreePrototype#1{}%
-\def\markdownRendererHeadingFourPrototype#1{}%
-\def\markdownRendererHeadingFivePrototype#1{}%
-\def\markdownRendererHeadingSixPrototype#1{}%
-\def\markdownRendererHorizontalRulePrototype{}%
-\def\markdownRendererFootnotePrototype#1{}%
-\def\markdownRendererCitePrototype#1{}%
-\def\markdownRendererTextCitePrototype#1{}%
-\def\markdownRendererTablePrototype#1#2#3{}%
-\def\markdownRendererInlineHtmlCommentPrototype#1{}%
-\let\markdownRendererBlockHtmlCommentBeginPrototype=\iffalse
-\let\markdownRendererBlockHtmlCommentBegin=\iffalse
-\let\markdownRendererBlockHtmlCommentEndPrototype=\fi
-\let\markdownRendererBlockHtmlCommentEnd=\fi
-\def\markdownRendererInlineHtmlTagPrototype#1{}%
-\def\markdownRendererInputBlockHtmlElementPrototype#1{}%
-\def\markdownRendererTickedBoxPrototype{}%
-\def\markdownRendererHalfTickedBoxPrototype{}%
-\def\markdownRendererUntickedBoxPrototype{}%
+\ExplSyntaxOn
+\cs_new:Nn \@@_plaintex_define_renderer_prototypes:
+  {
+    \seq_map_function:NN
+      \g_@@_renderers_seq
+      \@@_plaintex_define_renderer_prototype:n
+    \let\markdownRendererBlockHtmlCommentBeginPrototype=\iffalse
+    \let\markdownRendererBlockHtmlCommentBegin=\iffalse
+    \let\markdownRendererBlockHtmlCommentEndPrototype=\fi
+    \let\markdownRendererBlockHtmlCommentEnd=\fi
+  }
+\cs_new:Nn \@@_plaintex_define_renderer_prototype:n
+  {
+    \@@_renderer_prototype_tl_to_csname:nN
+      { #1 }
+      \l_tmpa_tl
+    \prop_get:NnN
+      \g_@@_renderer_arities_prop
+      { #1 }
+      \l_tmpb_tl
+    \@@_plaintex_define_renderer_prototype:cV
+      { \l_tmpa_tl }
+      \l_tmpb_tl
+  }
+\cs_new:Nn \@@_renderer_prototype_tl_to_csname:nN
+  {
+    \tl_set:Nn
+      \l_tmpa_tl
+%     TODO: Replace with \str_uppercase:n in TeX Live 2020.
+      { \str_upper_case:n { #1 } }
+    \tl_set:Nx
+      #2
+      {
+        markdownRenderer
+        \tl_head:f { \l_tmpa_tl }
+        \tl_tail:n { #1 }
+        Prototype
+      }
+  }
+\cs_new:Nn \@@_plaintex_define_renderer_prototype:Nn
+  {
+    \cs_generate_from_arg_count:NNnn
+      #1
+      \cs_set:Npn
+      { #2 }
+      { }
+  }
+\cs_generate_variant:Nn
+  \@@_plaintex_define_renderer_prototype:Nn
+  { cV }
+\@@_plaintex_define_renderer_prototypes:
+\ExplSyntaxOff
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -14325,6 +14399,7 @@
 % - `0` – Shell escape via the 18 output file stream
 % - `1` – Shell escape via the Lua \luamref{os.execute} method
 % - `2` – Direct Lua access
+% - `3` – The \pkg{lt3luabridge} Lua package
 %
 % By defining the macro, the user can coerce the package to use a specific mode.
 % If the user does not define the macro prior to loading the plain \TeX{}
@@ -14332,21 +14407,60 @@
 % of changing the value of \mref{markdownMode} after the implementation has been
 % loaded is undefined.
 %
+% The \mref{markdownMode} macro has been deprecated and will be removed in
+% Markdown 3.0.0. The code that corresponds to \mref{markdownMode} value of `3`
+% will be the only implementation.
+%
 % \end{markdown}
 %  \begin{macrocode}
-\ifx\markdownMode\undefined
-  \ifx\directlua\undefined
-    \def\markdownMode{0}%
-  \else
-    \def\markdownMode{2}%
-  \fi
-\fi
+\ExplSyntaxOn
+\cs_if_exist:NF
+  \markdownMode
+  {
+    \file_if_exist:nTF
+      { lt3luabridge.tex }
+      {
+        \cs_new:Npn
+          \markdownMode
+          { 3 }
+      }
+      {
+        \cs_if_exist:NTF
+          \directlua
+          {
+            \cs_new:Npn
+              \markdownMode
+              { 2 }
+          }
+          {
+            \cs_new:Npn
+              \markdownMode
+              { 0 }
+          }
+      }
+  }
 %    \end{macrocode}
+% \begin{markdown}
+%
+% \end{markdown}
+%  \begin{macrocode}
+\int_compare:nF
+  { \markdownMode = 3 }
+  {
+    \int_new:N
+      \g_luabridge_method_int
+    \int_gset:Nn
+      \g_luabridge_method_int
+      { \markdownMode }
+  }
+\ExplSyntaxOff
+%    \end{macrocode}
 % \par
 % \begin{markdown}
 %
-% The following macros are no longer a part of the plain \TeX{} interface and
-% are only defined for backwards compatibility:
+% The \mdef{markdownLuaRegisterIBCallback} and
+% \mdef{markdownLuaUnregisterIBCallback} macros have been deprecated and will
+% be removed in Markdown 3.0.0:
 %
 % \end{markdown}
 %  \begin{macrocode}
@@ -14423,6 +14537,19 @@
 % are rendered. The rest of the interface is inherited from the plain \TeX{}
 % interface (see Section <#sec:texinterface>).
 %
+% The \LaTeX{} implementation redefines the plain \TeX{} logging macros (see
+% Section <#sec:texinterfacelogging>) to use the \LaTeX{} \mref{PackageInfo},
+% \mref{PackageWarning}, and \mref{PackageError} macros.
+%
+% \end{markdown}
+%  \begin{macrocode}
+\newcommand\markdownInfo[1]{\PackageInfo{markdown}{#1}}%
+\newcommand\markdownWarning[1]{\PackageWarning{markdown}{#1}}%
+\newcommand\markdownError[2]{\PackageError{markdown}{#1}{#2.}}%
+\input markdown/markdown
+%    \end{macrocode}
+% \begin{markdown}
+%
 % The \LaTeX{} interface is implemented by the `markdown.sty` file, which
 % can be loaded from the \LaTeX{} document preamble as follows:
 % \end{markdown}
@@ -14602,9 +14729,47 @@
 }
 ```
 
+%</manual-options>
+%<*latex>
 % \fi
 % \par
 % \begin{markdown}
+%
+% To enable the enumeration of \LaTeX{} options, we will maintain the
+% \mdef{g_\@\@_latex_options_seq} sequence.
+%
+% \end{markdown}
+%  \begin{macrocode}
+\ExplSyntaxOn
+\seq_new:N \g_@@_latex_options_seq
+%    \end{macrocode}
+% \begin{markdown}
+%
+% To enable the reflection of default \LaTeX{} options and their types, we
+% will maintain the \mdef{g_\@\@_default_latex_options_prop} and
+% \mdef{g_\@\@_latex_option_types_prop} property lists, respectively.
+%
+% \end{markdown}
+%  \begin{macrocode}
+\prop_new:N \g_@@_latex_option_types_prop
+\prop_new:N \g_@@_default_latex_options_prop
+\tl_const:Nn \c_@@_option_layer_latex_tl { latex }
+\seq_put_right:NV \g_@@_option_layers_seq \c_@@_option_layer_latex_tl
+\cs_new:Nn
+  \@@_add_latex_option:nnn
+  {
+    \@@_add_option:Vnnn
+      \c_@@_option_layer_latex_tl
+      { #1 }
+      { #2 }
+      { #3 }
+  }
+%    \end{macrocode}
+% \iffalse
+%</latex>
+%<*manual-options>
+% \fi
+% \begin{markdown}
 
 #### No default token renderer prototypes {#latexplain}
 
@@ -14616,7 +14781,8 @@
 % (see Section <#sec:textokenrendererprototypes>)
 and prevent the soft \LaTeX{} prerequisites
 % in Section <#sec:latexprerequisites>
-from being loaded:
+from being loaded: The plain option must be set before or when loading the
+package. Setting the option after loading the package will have no effect.
 
 ``` tex
 \usepackage[plain]{markdown}
@@ -14628,15 +14794,11 @@
 %<*latex>
 % \fi
 %  \begin{macrocode}
-\newif\ifmarkdownLaTeXPlain
-  \markdownLaTeXPlainfalse
-\define at key{markdownOptions}{plain}[true]{%
-  \ifmarkdownLaTeXLoaded
-    \markdownWarning
-      {The plain option must be specified when loading the package}%
-  \else
-    \markdownLaTeXPlaintrue
-  \fi}
+\@@_add_latex_option:nnn
+  { plain }
+  { boolean }
+  { false }
+\ExplSyntaxOff
 %    \end{macrocode}
 % \iffalse
 %</latex>
@@ -15088,99 +15250,82 @@
 
 ```````
 
-%#### Plain \TeX{} Interface Options
-% The following options map directly to the option macros exposed by the plain
-% \TeX{} interface (see Section <#sec:texoptions>).
-%
 % \markdownEnd
 % \iffalse
 %</manual-options>
 %<*latex>
 % \fi
+% \begin{markdown}
+%
+%#### Plain \TeX{} Interface Options
+% Here, we automatically define plain \TeX{} macros and the
+% \meta{key}`=`\meta{value} interface for the above \LaTeX{} options.
+%
+% \end{markdown}
 %  \begin{macrocode}
-\define at key{markdownOptions}{helperScriptFileName}{%
-  \def\markdownOptionHelperScriptFileName{#1}}%
-\define at key{markdownOptions}{inputTempFileName}{%
-  \def\markdownOptionInputTempFileName{#1}}%
-\define at key{markdownOptions}{outputTempFileName}{%
-  \def\markdownOptionOutputTempFileName{#1}}%
-\define at key{markdownOptions}{errorTempFileName}{%
-  \def\markdownOptionErrorTempFileName{#1}}%
-\define at key{markdownOptions}{cacheDir}{%
-  \def\markdownOptionCacheDir{#1}}%
-\define at key{markdownOptions}{outputDir}{%
-  \def\markdownOptionOutputDir{#1}}%
-\define at key{markdownOptions}{blankBeforeBlockquote}[true]{%
-  \def\markdownOptionBlankBeforeBlockquote{#1}}%
-\define at key{markdownOptions}{blankBeforeCodeFence}[true]{%
-  \def\markdownOptionBlankBeforeCodeFence{#1}}%
-\define at key{markdownOptions}{blankBeforeHeading}[true]{%
-  \def\markdownOptionBlankBeforeHeading{#1}}%
-\define at key{markdownOptions}{breakableBlockquotes}[true]{%
-  \def\markdownOptionBreakableBlockquotes{#1}}%
-\define at key{markdownOptions}{citations}[true]{%
-  \def\markdownOptionCitations{#1}}%
-\define at key{markdownOptions}{citationNbsps}[true]{%
-  \def\markdownOptionCitationNbsps{#1}}%
-\define at key{markdownOptions}{contentBlocks}[true]{%
-  \def\markdownOptionContentBlocks{#1}}%
-\define at key{markdownOptions}{codeSpans}[true]{%
-  \def\markdownOptionCodeSpans{#1}}%
-\define at key{markdownOptions}{contentBlocksLanguageMap}{%
-  \def\markdownOptionContentBlocksLanguageMap{#1}}%
-\define at key{markdownOptions}{definitionLists}[true]{%
-  \def\markdownOptionDefinitionLists{#1}}%
-\define at key{markdownOptions}{eagerCache}[true]{%
-  \def\markdownOptionEagerCache{#1}}%
-\define at key{markdownOptions}{expectJekyllData}[true]{%
-  \def\markdownOptionExpectJekyllData{#1}}%
-\define at key{markdownOptions}{footnotes}[true]{%
-  \def\markdownOptionFootnotes{#1}}%
-\define at key{markdownOptions}{fencedCode}[true]{%
-  \def\markdownOptionFencedCode{#1}}%
-\define at key{markdownOptions}{jekyllData}[true]{%
-  \def\markdownOptionJekyllData{#1}}%
-\define at key{markdownOptions}{hardLineBreaks}[true]{%
-  \def\markdownOptionHardLineBreaks{#1}}%
-\define at key{markdownOptions}{hashEnumerators}[true]{%
-  \def\markdownOptionHashEnumerators{#1}}%
-\define at key{markdownOptions}{headerAttributes}[true]{%
-  \def\markdownOptionHeaderAttributes{#1}}%
-\define at key{markdownOptions}{html}[true]{%
-  \def\markdownOptionHtml{#1}}%
-\define at key{markdownOptions}{hybrid}[true]{%
-  \def\markdownOptionHybrid{#1}}%
-\define at key{markdownOptions}{inlineFootnotes}[true]{%
-  \def\markdownOptionInlineFootnotes{#1}}%
-\define at key{markdownOptions}{pipeTables}[true]{%
-  \def\markdownOptionPipeTables{#1}}%
-\define at key{markdownOptions}{preserveTabs}[true]{%
-  \def\markdownOptionPreserveTabs{#1}}%
-\define at key{markdownOptions}{relativeReferences}[true]{%
-  \def\markdownOptionRelativeReferences{#1}}%
-\define at key{markdownOptions}{smartEllipses}[true]{%
-  \def\markdownOptionSmartEllipses{#1}}%
-\define at key{markdownOptions}{shiftHeadings}{%
-  \def\markdownOptionShiftHeadings{#1}}%
-\define at key{markdownOptions}{slice}{%
-  \def\markdownOptionSlice{#1}}%
-\define at key{markdownOptions}{startNumber}[true]{%
-  \def\markdownOptionStartNumber{#1}}%
-\define at key{markdownOptions}{stripIndent}[true]{%
-  \def\markdownOptionStripIndent{#1}}%
-\define at key{markdownOptions}{tableCaptions}[true]{%
-  \def\markdownOptionTableCaptions{#1}}%
-\define at key{markdownOptions}{taskLists}[true]{%
-  \def\markdownOptionTaskLists{#1}}%
-\define at key{markdownOptions}{texComments}[true]{%
-  \def\markdownOptionTexComments{#1}}%
-\define at key{markdownOptions}{tightLists}[true]{%
-  \def\markdownOptionTightLists{#1}}%
-\define at key{markdownOptions}{underscores}[true]{%
-  \def\markdownOptionUnderscores{#1}}%
-\define at key{markdownOptions}{stripPercentSigns}[true]{%
-  \def\markdownOptionStripPercentSigns{#1}}%
+\ExplSyntaxOn
+\cs_new:Nn \@@_latex_define_option_commands_and_keyvals:
+  {
+    \seq_map_inline:Nn
+      \g_@@_latex_options_seq
+      {
+          \@@_plain_tex_define_option_command:n
+            { ##1 }
+      }
 %    \end{macrocode}
+% \begin{markdown}
+%
+% Furthermore, we also define the \meta{key}`=`\meta{value} interface
+% for all option macros recognized by the Lua plain \TeX{} interfaces.
+%
+% \end{markdown}
+%  \begin{macrocode}
+    \seq_map_inline:Nn
+      \g_@@_option_layers_seq
+      {
+        \seq_map_inline:cn
+          { g_@@_ ##1 _options_seq }
+          {
+              \@@_latex_define_option_keyval:nn
+                { ##1 }
+                { ####1 }
+          }
+      }
+  }
+\cs_new:Nn \@@_latex_define_option_keyval:nn
+  {
+    \prop_get:cnN
+      { g_@@_ #1 _option_types_prop }
+      { #2 }
+      \l_tmpa_tl
+    \str_if_eq:VVTF
+      \l_tmpa_tl
+      \c_@@_option_type_boolean_tl
+      {
+        \define at key
+          { markdownOptions }
+          { #2 }
+          [ true ]
+          {
+            \@@_set_option_value:nn
+              { #2 }
+              { ##1 }
+          }
+      }
+      {
+        \define at key
+          { markdownOptions }
+          { #2 }
+          {
+            \@@_set_option_value:nn
+              { #2 }
+              { ##1 }
+          }
+      }
+  }
+\@@_latex_define_option_commands_and_keyvals:
+\ExplSyntaxOff
+%    \end{macrocode}
 % \par
 % \begin{markdown}
 %
@@ -15209,14 +15354,8 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-\define at key{markdownOptions}{finalizeCache}[true]{%
-  \def\markdownOptionFinalizeCache{#1}}%
 \DeclareOption{finalizecache}{\markdownSetup{finalizeCache}}
-\define at key{markdownOptions}{frozenCache}[true]{%
-  \def\markdownOptionFrozenCache{#1}}%
 \DeclareOption{frozencache}{\markdownSetup{frozenCache}}
-\define at key{markdownOptions}{frozenCacheFileName}{%
-  \def\markdownOptionFrozenCacheFileName{#1}}%
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -15250,17 +15389,9 @@
   }
 \cs_new:Nn \@@_latex_define_renderer:n
   {
-    \tl_set:Nn
-      \l_tmpb_tl
-%     TODO: Replace with \str_uppercase:n in TeX Live 2020.
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
+    \@@_renderer_tl_to_csname:nN
+      { #1 }
       \l_tmpa_tl
-      {
-        markdownRenderer
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
-      }
     \prop_get:NnN
       \g_@@_renderer_arities_prop
       { #1 }
@@ -15270,6 +15401,20 @@
       { \l_tmpa_tl }
       \l_tmpb_tl
   }
+\cs_new:Nn \@@_renderer_tl_to_csname:nN
+  {
+    \tl_set:Nn
+      \l_tmpa_tl
+%     TODO: Replace with \str_uppercase:n in TeX Live 2020.
+      { \str_upper_case:n { #1 } }
+    \tl_set:Nx
+      #2
+      {
+        markdownRenderer
+        \tl_head:f { \l_tmpa_tl }
+        \tl_tail:n { #1 }
+      }
+  }
 \cs_new:Nn \@@_latex_define_renderer:nNn
   {
     \define at key
@@ -15321,18 +15466,9 @@
   }
 \cs_new:Nn \@@_latex_define_renderer_prototype:n
   {
-    \tl_set:Nn
-      \l_tmpb_tl
-%     TODO: Replace with \str_uppercase:n in TeX Live 2020.
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
+    \@@_renderer_prototype_tl_to_csname:nN
+      { #1 }
       \l_tmpa_tl
-      {
-        markdownRenderer
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
-        Prototype
-      }
     \prop_get:NnN
       \g_@@_renderer_arities_prop
       { #1 }
@@ -15512,7 +15648,9 @@
 %--------------------
 %
 % The Lua implementation implements \luamdef{writer} and \luamdef{reader}
-% objects that provide the conversion from markdown to plain \TeX{}.
+% objects, which provide the conversion from markdown to plain \TeX, and
+% \luamdef{extension} objects, which provide syntax extensions for the
+% \luamref{writer} and \luamref{reader} objects.
 %
 % The Lunamark Lua module implements writers for the conversion to various
 % other formats, such as DocBook, Groff, or \acro{HTML}. These were stripped
@@ -17956,22 +18094,31 @@
 %  \begin{macrocode}
 function M.writer.new(options)
   local self = {}
-  options = options or {}
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
-% Make the `options` table inherit from the \luamref{defaultOptions} table.
+% Make `options.cacheDir` available as \luamdef{writer->cacheDir}, so that it
+% is accessible from extensions.
 %
 % \end{markdown}
 %  \begin{macrocode}
-  setmetatable(options, { __index = function (_, key)
-    return defaultOptions[key] end })
+  self.cacheDir = options.cacheDir
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
-% Parse the \Opt{slice} option and define \luamdef{writer->slice\_begin}
+% Make `options.hybrid` available as \luamdef{writer->hybrid}, so that it is
+% accessible from extensions.
+%
+% \end{markdown}
+%  \begin{macrocode}
+  self.hybrid = options.hybrid
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Parse the \Opt{slice} option and define \luamdef{writer->slice\_begin},
 % \luamdef{writer->slice\_end}, and \luamdef{writer->is\_writing}. The
 % \luamref{writer->is\_writing} member variable is mutable.
 %
@@ -18115,26 +18262,20 @@
 % \par
 % \begin{markdown}
 %
-% Define tables \luamdef{escaped_uri_chars}, \luamdef{escaped_citation_chars},
-% and \luamdef{escaped_minimal_strings} containing the mapping from special
-% plain characters and character strings that always need to be escaped.
+% Define tables \luamdef{writer->escaped_uri_chars} and
+% \luamdef{writer->escaped_minimal_strings} containing the mapping from
+% special plain characters and character strings that always need to be
+% escaped.
 %
 % \end{markdown}
 %  \begin{macrocode}
-   local escaped_uri_chars = {
+   self.escaped_uri_chars = {
      ["{"] = "\\markdownRendererLeftBrace{}",
      ["}"] = "\\markdownRendererRightBrace{}",
      ["%"] = "\\markdownRendererPercentSign{}",
      ["\\"] = "\\markdownRendererBackslash{}",
    }
-   local escaped_citation_chars = {
-     ["{"] = "\\markdownRendererLeftBrace{}",
-     ["}"] = "\\markdownRendererRightBrace{}",
-     ["%"] = "\\markdownRendererPercentSign{}",
-     ["\\"] = "\\markdownRendererBackslash{}",
-     ["#"] = "\\markdownRendererHash{}",
-   }
-   local escaped_minimal_strings = {
+   self.escaped_minimal_strings = {
      ["^^"] = "\\markdownRendererCircumflex\\markdownRendererCircumflex ",
      ["☒"] = "\\markdownRendererTickedBox{}",
      ["⌛"] = "\\markdownRendererHalfTickedBox{}",
@@ -18144,13 +18285,13 @@
 % \par
 % \begin{markdown}
 %
-% Define a table \luamdef{escaped_chars} containing the mapping from special
-% plain \TeX{} characters (including the active pipe character (`|`) of
-% \Hologo{ConTeXt}) that need to be escaped for typeset content.
+% Define a table \luamdef{writer->escaped_chars} containing the mapping from
+% special plain \TeX{} characters (including the active pipe character (`|`)
+% of \Hologo{ConTeXt}) that need to be escaped for typeset content.
 %
 % \end{markdown}
 %  \begin{macrocode}
-  local escaped_chars = {
+  self.escaped_chars = {
      ["{"] = "\\markdownRendererLeftBrace{}",
      ["}"] = "\\markdownRendererRightBrace{}",
      ["%"] = "\\markdownRendererPercentSign{}",
@@ -18167,57 +18308,40 @@
 % \par
 % \begin{markdown}
 %
-% Use the \luamref{escaped_chars}, \luamref{escaped_uri_chars},
-% \luamref{escaped_citation_chars}, and \luamref{escaped_minimal_strings} tables
-% to create the \luamdef{escape}, \luamdef{escape_citation},
-% \luamdef{escape_uri}, and \luamdef{escape_minimal} escaper functions.
+% Use the \luamref{writer->escaped_chars}, \luamref{writer->escaped_uri_chars},
+% and \luamref{writer->escaped_minimal_strings} tables to create the
+% \luamdef{writer->escape}, \luamdef{writer->escape_uri}, and
+% \luamdef{writer->escape_minimal} escaper functions.
 %
 % \end{markdown}
 %  \begin{macrocode}
-  local escape = util.escaper(escaped_chars, escaped_minimal_strings)
-  local escape_citation = util.escaper(escaped_citation_chars,
-    escaped_minimal_strings)
-  local escape_uri = util.escaper(escaped_uri_chars, escaped_minimal_strings)
-  local escape_minimal = util.escaper({}, escaped_minimal_strings)
+  self.escape = util.escaper(self.escaped_chars, self.escaped_minimal_strings)
+  self.escape_uri = util.escaper(self.escaped_uri_chars, self.escaped_minimal_strings)
+  self.escape_minimal = util.escaper({}, self.escaped_minimal_strings)
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
 % Define \luamdef{writer->string} as a function that will transform an input
-% plain text span `s` to the output format, \luamdef{writer->citation} as a
-% function that will transform an input citation name `c` to the output format,
-% and \luamdef{writer->uri} as a function that will transform an input
-% \acro{uri} `u` to the output format. If the \Opt{hybrid} option is enabled,
-% use the \luamref{escape_minimal}. Otherwise, use the \luamref{escape},
-% \luamref{escape_citation}, and \luamref{escape_uri} functions.
+% plain text span `s` to the output format and \luamdef{writer->uri} as a
+% function that will transform an input \acro{uri} `u` to the output format.
+% If the \Opt{hybrid} option is enabled, use the
+% \luamref{writer->escape_minimal}.  Otherwise, use the
+% \luamref{writer->escape}, and \luamref{writer->escape_uri} functions.
 %
 % \end{markdown}
 %  \begin{macrocode}
   if options.hybrid then
-    self.string = escape_minimal
-    self.citation = escape_minimal
-    self.uri = escape_minimal
+    self.string = self.escape_minimal
+    self.uri = self.escape_minimal
   else
-    self.string = escape
-    self.citation = escape_citation
-    self.uri = escape_uri
+    self.string = self.escape
+    self.uri = self.escape_uri
   end
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
-% Define \luamdef{writer->escape} as a function that will transform an input
-% plain text span to the output format. Unlike the \luamref{writer->string}
-% function, \luamref{writer->escape} always uses the \luamref{escape} function,
-% even when the \Opt{hybrid} option is enabled.
-%
-% \end{markdown}
-%  \begin{macrocode}
-  self.escape = escape
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 % Define \luamdef{writer->code} as a function that will transform an input
 % inlined code span `s` to the output format.
 %
@@ -18246,38 +18370,6 @@
 % \par
 % \begin{markdown}
 %
-% Define \luamdef{writer->table} as a function that will transform an input
-% table to the output format, where `rows` is a sequence of columns and a
-% column is a sequence of cell texts.
-%
-% \end{markdown}
-%  \begin{macrocode}
-  function self.table(rows, caption)
-    if not self.is_writing then return "" end
-    local buffer = {"\\markdownRendererTable{",
-      caption or "", "}{", #rows - 1, "}{", #rows[1], "}"}
-    local temp = rows[2] -- put alignments on the first row
-    rows[2] = rows[1]
-    rows[1] = temp
-    for i, row in ipairs(rows) do
-      table.insert(buffer, "{")
-      for _, column in ipairs(row) do
-        if i > 1 then -- do not use braces for alignments
-          table.insert(buffer, "{")
-        end
-        table.insert(buffer, column)
-        if i > 1 then
-          table.insert(buffer, "}")
-        end
-      end
-      table.insert(buffer, "}")
-    end
-    return buffer
-  end
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 % Define \luamdef{writer->image} as a function that will transform an input
 % image to the output format, where `lab` corresponds to the label, `src`
 % to the \acro{url}, and `tit` to the title of the image.
@@ -18294,90 +18386,6 @@
 % \par
 % \begin{markdown}
 %
-% The \luamdef{languages_json} table maps programming language filename
-% extensions to fence infostrings. All `options.`\luamref{contentBlocksLanguageMap}
-% files located by the KPathSea library are loaded into a chain of tables.
-% \luamref{languages_json} corresponds to the first table and is chained with
-% the rest via Lua metatables.
-%
-% \end{markdown}
-%  \begin{macrocode}
-  local languages_json = (function()
-    local ran_ok, kpse = pcall(require, "kpse")
-    if ran_ok then
-      kpse.set_program_name("luatex")
-%    \end{macrocode}
-% \begin{markdown}
-%
-% If the KPathSea library is unavailable, perhaps because we are using
-% LuaMeta\TeX, we will only locate the `options.`\luamref{contentBlocksLanguageMap}
-% in the current working directory:
-%
-% \end{markdown}
-%  \begin{macrocode}
-    else
-      kpse = {lookup=function(filename, options) return filename end}
-    end
-    local base, prev, curr
-    for _, filename in ipairs{kpse.lookup(options.contentBlocksLanguageMap,
-                                          { all=true })} do
-      local file = io.open(filename, "r")
-      if not file then goto continue end
-      json = file:read("*all"):gsub('("[^\n]-"):','[%1]=')
-      curr = (function()
-        local _ENV={ json=json, load=load } -- run in sandbox
-        return load("return "..json)()
-      end)()
-      if type(curr) == "table" then
-        if base == nil then
-          base = curr
-        else
-          setmetatable(prev, { __index = curr })
-        end
-        prev = curr
-      end
-      ::continue::
-    end
-    return base or {}
-  end)()
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
-% Define \luamdef{writer->contentblock} as a function that will transform an
-% input iA\,Writer content block to the output format, where `src`
-% corresponds to the \acro{uri} prefix, `suf` to the \acro{uri} extension,
-% `type` to the type of the content block (`localfile` or `onlineimage`),
-% and `tit` to the title of the content block.
-%
-% \end{markdown}
-%  \begin{macrocode}
-  function self.contentblock(src,suf,type,tit)
-    if not self.is_writing then return "" end
-    src = src.."."..suf
-    suf = suf:lower()
-    if type == "onlineimage" then
-      return {"\\markdownRendererContentBlockOnlineImage{",suf,"}",
-                             "{",self.string(src),"}",
-                             "{",self.uri(src),"}",
-                             "{",self.string(tit or ""),"}"}
-    elseif languages_json[suf] then
-      return {"\\markdownRendererContentBlockCode{",suf,"}",
-                             "{",self.string(languages_json[suf]),"}",
-                             "{",self.string(src),"}",
-                             "{",self.uri(src),"}",
-                             "{",self.string(tit or ""),"}"}
-    else
-      return {"\\markdownRendererContentBlock{",suf,"}",
-                             "{",self.string(src),"}",
-                             "{",self.uri(src),"}",
-                             "{",self.string(tit or ""),"}"}
-    end
-  end
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 % Define \luamdef{writer->bulletlist} as a function that will transform an input
 % bulleted list to the output format, where `items` is an array of the list
 % items and `tight` specifies, whether the list is tight or not.
@@ -18499,7 +18507,7 @@
 %  \begin{macrocode}
   function self.block_html_element(s)
     if not self.is_writing then return "" end
-    local name = util.cache(options.cacheDir, s, nil, nil, ".verbatim")
+    local name = util.cache(self.cacheDir, s, nil, nil, ".verbatim")
     return {"\\markdownRendererInputBlockHtmlElement{",name,"}"}
   end
 %    \end{macrocode}
@@ -18506,42 +18514,6 @@
 % \par
 % \begin{markdown}
 %
-% Define \luamdef{writer->definitionlist} as a function that will transform an
-% input definition list to the output format, where `items` is an array of
-% tables, each of the form `{ term = t, definitions = defs }`, where `t`
-% is a term and `defs` is an array of definitions. `tight` specifies,
-% whether the list is tight or not.
-%
-% \end{markdown}
-%  \begin{macrocode}
-  local function dlitem(term, defs)
-    local retVal = {"\\markdownRendererDlItem{",term,"}"}
-    for _, def in ipairs(defs) do
-      retVal[#retVal+1] = {"\\markdownRendererDlDefinitionBegin ",def,
-                           "\\markdownRendererDlDefinitionEnd "}
-    end
-    retVal[#retVal+1] = "\\markdownRendererDlItemEnd "
-    return retVal
-  end
-
-  function self.definitionlist(items,tight)
-    if not self.is_writing then return "" end
-    local buffer = {}
-    for _,item in ipairs(items) do
-      buffer[#buffer + 1] = dlitem(item.term, item.definitions)
-    end
-    if tight and options.tightLists then
-      return {"\\markdownRendererDlBeginTight\n", buffer,
-        "\n\\markdownRendererDlEndTight"}
-    else
-      return {"\\markdownRendererDlBegin\n", buffer,
-        "\n\\markdownRendererDlEnd"}
-    end
-  end
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 % Define \luamdef{writer->emphasis} as a function that will transform an
 % emphasized span `s` of input text to the output format.
 %
@@ -18606,7 +18578,7 @@
   function self.verbatim(s)
     if not self.is_writing then return "" end
     s = string.gsub(s, '[\r\n%s]*$', '')
-    local name = util.cache(options.cacheDir, s, nil, nil, ".verbatim")
+    local name = util.cache(self.cacheDir, s, nil, nil, ".verbatim")
     return {"\\markdownRendererInputVerbatim{",name,"}"}
   end
 %    \end{macrocode}
@@ -18613,22 +18585,6 @@
 % \par
 % \begin{markdown}
 %
-% Define \luamdef{writer->codeFence} as a function that will transform an
-% input fenced code block `s` with the infostring `i` to the output
-% format.
-%
-% \end{markdown}
-%  \begin{macrocode}
-  function self.fencedCode(i, s)
-    if not self.is_writing then return "" end
-    s = string.gsub(s, '[\r\n%s]*$', '')
-    local name = util.cache(options.cacheDir, s, nil, nil, ".verbatim")
-    return {"\\markdownRendererInputFencedCode{",name,"}{",i,"}"}
-  end
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 % Define \luamdef{writer->document} as a function that will transform a
 % document `d` to the output format.
 %
@@ -18657,101 +18613,6 @@
 % \par
 % \begin{markdown}
 %
-% Define \luamdef{writer->jekyllData} as a function that will transform an
-% input \acro{yaml} table `d` to the output format. The table is the value for
-% the key `p` in the parent table; if `p` is nil, then the table has no parent.
-% All scalar keys and values encountered in the table will be cast to a string
-% following \acro{yaml} serialization rules. String values will also be
-% transformed using the function `t`.
-%
-% \end{markdown}
-%  \begin{macrocode}
-  function self.jekyllData(d, t, p)
-    if not self.is_writing then return "" end
-
-    local buf = {}
-
-    local keys = {}
-    for k, _ in pairs(d) do
-      table.insert(keys, k)
-    end
-    table.sort(keys)
-
-    if not p then
-      table.insert(buf, "\\markdownRendererJekyllDataBegin")
-    end
-
-    if #d > 0 then
-        table.insert(buf, "\\markdownRendererJekyllDataSequenceBegin{")
-        table.insert(buf, self.uri(p or "null"))
-        table.insert(buf, "}{")
-        table.insert(buf, #keys)
-        table.insert(buf, "}")
-    else
-        table.insert(buf, "\\markdownRendererJekyllDataMappingBegin{")
-        table.insert(buf, self.uri(p or "null"))
-        table.insert(buf, "}{")
-        table.insert(buf, #keys)
-        table.insert(buf, "}")
-    end
-
-    for _, k in ipairs(keys) do
-      local v = d[k]
-      local typ = type(v)
-      k = tostring(k or "null")
-      if typ == "table" and next(v) ~= nil then
-        table.insert(
-          buf,
-          self.jekyllData(v, t, k)
-        )
-      else
-        k = self.uri(k)
-        v = tostring(v)
-        if typ == "boolean" then
-          table.insert(buf, "\\markdownRendererJekyllDataBoolean{")
-          table.insert(buf, k)
-          table.insert(buf, "}{")
-          table.insert(buf, v)
-          table.insert(buf, "}")
-        elseif typ == "number" then
-          table.insert(buf, "\\markdownRendererJekyllDataNumber{")
-          table.insert(buf, k)
-          table.insert(buf, "}{")
-          table.insert(buf, v)
-          table.insert(buf, "}")
-        elseif typ == "string" then
-          table.insert(buf, "\\markdownRendererJekyllDataString{")
-          table.insert(buf, k)
-          table.insert(buf, "}{")
-          table.insert(buf, t(v))
-          table.insert(buf, "}")
-        elseif typ == "table" then
-          table.insert(buf, "\\markdownRendererJekyllDataEmpty{")
-          table.insert(buf, k)
-          table.insert(buf, "}")
-        else
-          error(format("Unexpected type %s for value of " ..
-                       "YAML key %s", typ, k))
-        end
-      end
-    end
-
-    if #d > 0 then
-      table.insert(buf, "\\markdownRendererJekyllDataSequenceEnd")
-    else
-      table.insert(buf, "\\markdownRendererJekyllDataMappingEnd")
-    end
-
-    if not p then
-      table.insert(buf, "\\markdownRendererJekyllDataEnd")
-    end
-
-    return buf
-  end
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 % Define \luamdef{writer->active\_attributes} as a stack of attributes
 % of the headings that are currently active. The
 % \luamref{writer->active\_headings} member variable is mutable.
@@ -18890,47 +18751,6 @@
 % \par
 % \begin{markdown}
 %
-% Define \luamdef{writer->note} as a function that will transform an
-% input footnote `s` to the output format.
-%
-% \end{markdown}
-%  \begin{macrocode}
-  function self.note(s)
-    return {"\\markdownRendererFootnote{",s,"}"}
-  end
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
-% Define \luamdef{writer->citations} as a function that will transform an
-% input array of citations `cites` to the output format. If `text_cites`
-% is enabled, the citations should be rendered in-text, when applicable.
-% The `cites` array contains tables with the following keys and values:
-% \begin{itemize}
-%   \item`suppress_author` -- If the value of the key is true, then the
-%     author of the work should be omitted in the citation, when applicable.
-%   \item`prenote` -- The value of the key is either `nil` or a rope
-%     that should be inserted before the citation.
-%   \item`postnote` -- The value of the key is either `nil` or a rope
-%     that should be inserted after the citation.
-%   \item`name` -- The value of this key is the citation name.
-% \end{itemize}
-%
-% \end{markdown}
-%  \begin{macrocode}
-  function self.citations(text_cites, cites)
-    local buffer = {"\\markdownRenderer", text_cites and "TextCite" or "Cite",
-      "{", #cites, "}"}
-    for _,cite in ipairs(cites) do
-      buffer[#buffer+1] = {cite.suppress_author and "-" or "+", "{",
-        cite.prenote or "", "}{", cite.postnote or "", "}{", cite.name, "}"}
-    end
-    return buffer
-  end
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 % Define \luamdef{writer->get_state} as a function that returns the current
 % state of the writer, where the state of a writer are its mutable member
 % variables.
@@ -19041,8 +18861,6 @@
 parsers.alphanumeric           = R("AZ","az","09")
 parsers.keyword                = parsers.letter
                                * parsers.alphanumeric^0
-parsers.citation_chars         = parsers.alphanumeric
-                               + S("#$%&-+<>~/_")
 parsers.internal_punctuation   = S(":;,.?")
 
 parsers.doubleasterisks        = P("**")
@@ -19375,236 +19193,6 @@
 % \par
 % \begin{markdown}
 %
-%#### Parsers Used for iA\,Writer Content Blocks
-%
-% \end{markdown}
-%  \begin{macrocode}
-parsers.contentblock_tail
-                    = parsers.optionaltitle
-                    * (parsers.newline + parsers.eof)
-
--- case insensitive online image suffix:
-parsers.onlineimagesuffix
-                    = (function(...)
-                        local parser = nil
-                        for _,suffix in ipairs({...}) do
-                          local pattern=nil
-                          for i=1,#suffix do
-                            local char=suffix:sub(i,i)
-                            char = S(char:lower()..char:upper())
-                            if pattern == nil then
-                              pattern = char
-                            else
-                              pattern = pattern * char
-                            end
-                          end
-                          if parser == nil then
-                            parser = pattern
-                          else
-                            parser = parser + pattern
-                          end
-                        end
-                        return parser
-                      end)("png", "jpg", "jpeg", "gif", "tif", "tiff")
-
--- online image url for iA Writer content blocks with mandatory suffix,
--- allowing nested brackets:
-parsers.onlineimageurl
-                    = (parsers.less
-                      * Cs((parsers.anyescaped
-                           - parsers.more
-                           - #(parsers.period
-                              * parsers.onlineimagesuffix
-                              * parsers.more
-                              * parsers.contentblock_tail))^0)
-                      * parsers.period
-                      * Cs(parsers.onlineimagesuffix)
-                      * parsers.more
-                      + (Cs((parsers.inparens
-                            + (parsers.anyescaped
-                              - parsers.spacing
-                              - parsers.rparent
-                              - #(parsers.period
-                                 * parsers.onlineimagesuffix
-                                 * parsers.contentblock_tail)))^0)
-                        * parsers.period
-                        * Cs(parsers.onlineimagesuffix))
-                      ) * Cc("onlineimage")
-
--- filename for iA Writer content blocks with mandatory suffix:
-parsers.localfilepath
-                    = parsers.slash
-                    * Cs((parsers.anyescaped
-                         - parsers.tab
-                         - parsers.newline
-                         - #(parsers.period
-                            * parsers.alphanumeric^1
-                            * parsers.contentblock_tail))^1)
-                    * parsers.period
-                    * Cs(parsers.alphanumeric^1)
-                    * Cc("localfile")
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
-%#### Parsers Used for Citations
-%
-% \end{markdown}
-%  \begin{macrocode}
-parsers.citation_name = Cs(parsers.dash^-1) * parsers.at
-                      * Cs(parsers.citation_chars
-                          * (((parsers.citation_chars + parsers.internal_punctuation
-                              - parsers.comma - parsers.semicolon)
-                             * -#((parsers.internal_punctuation - parsers.comma
-                                  - parsers.semicolon)^0
-                                 * -(parsers.citation_chars + parsers.internal_punctuation
-                                    - parsers.comma - parsers.semicolon)))^0
-                            * parsers.citation_chars)^-1)
-
-parsers.citation_body_prenote
-                    = Cs((parsers.alphanumeric^1
-                         + parsers.bracketed
-                         + parsers.inticks
-                         + (parsers.anyescaped
-                           - (parsers.rbracket + parsers.blankline^2))
-                         - (parsers.spnl * parsers.dash^-1 * parsers.at))^0)
-
-parsers.citation_body_postnote
-                    = Cs((parsers.alphanumeric^1
-                         + parsers.bracketed
-                         + parsers.inticks
-                         + (parsers.anyescaped
-                           - (parsers.rbracket + parsers.semicolon 
-                             + parsers.blankline^2))
-                         - (parsers.spnl * parsers.rbracket))^0)
-
-parsers.citation_body_chunk
-                    = parsers.citation_body_prenote
-                    * parsers.spnl * parsers.citation_name
-                    * (parsers.internal_punctuation - parsers.semicolon)^-1
-                    * parsers.spnl * parsers.citation_body_postnote
-
-parsers.citation_body
-                    = parsers.citation_body_chunk
-                    * (parsers.semicolon * parsers.spnl
-                      * parsers.citation_body_chunk)^0
-
-parsers.citation_headless_body_postnote
-                    = Cs((parsers.alphanumeric^1
-                         + parsers.bracketed
-                         + parsers.inticks
-                         + (parsers.anyescaped
-                           - (parsers.rbracket + parsers.at
-                             + parsers.semicolon + parsers.blankline^2))
-                         - (parsers.spnl * parsers.rbracket))^0)
-
-parsers.citation_headless_body
-                    = parsers.citation_headless_body_postnote
-                    * (parsers.sp * parsers.semicolon * parsers.spnl
-                      * parsers.citation_body_chunk)^0
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
-%#### Parsers Used for Footnotes
-%
-% \end{markdown}
-%  \begin{macrocode}
-local function strip_first_char(s)
-  return s:sub(2)
-end
-
-parsers.RawNoteRef = #(parsers.lbracket * parsers.circumflex)
-                   * parsers.tag / strip_first_char
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
-%#### Parsers Used for Tables
-%
-% \end{markdown}
-%  \begin{macrocode}
-local function make_pipe_table_rectangular(rows)
-  local num_columns = #rows[2]
-  local rectangular_rows = {}
-  for i = 1, #rows do
-    local row = rows[i]
-    local rectangular_row = {}
-    for j = 1, num_columns do
-      rectangular_row[j] = row[j] or ""
-    end
-    table.insert(rectangular_rows, rectangular_row)
-  end
-  return rectangular_rows
-end
-
-local function pipe_table_row(allow_empty_first_column
-                             , nonempty_column
-                             , column_separator
-                             , column)
-  local row_beginning
-  if allow_empty_first_column then
-    row_beginning = -- empty first column
-                    #(parsers.spacechar^4
-                     * column_separator)
-                  * parsers.optionalspace
-                  * column
-                  * parsers.optionalspace
-                  -- non-empty first column
-                  + parsers.nonindentspace
-                  * nonempty_column^-1
-                  * parsers.optionalspace
-  else
-    row_beginning = parsers.nonindentspace
-                  * nonempty_column^-1
-                  * parsers.optionalspace
-  end
-
-  return Ct(row_beginning
-           * (-- single column with no leading pipes
-              #(column_separator
-               * parsers.optionalspace
-               * parsers.newline)
-             * column_separator
-             * parsers.optionalspace
-             -- single column with leading pipes or
-             -- more than a single column
-             + (column_separator
-               * parsers.optionalspace
-               * column
-               * parsers.optionalspace)^1
-             * (column_separator
-               * parsers.optionalspace)^-1))
-end
-
-parsers.table_hline_separator = parsers.pipe + parsers.plus
-parsers.table_hline_column = (parsers.dash
-                             - #(parsers.dash
-                                * (parsers.spacechar
-                                  + parsers.table_hline_separator
-                                  + parsers.newline)))^1
-                           * (parsers.colon * Cc("r")
-                             + parsers.dash * Cc("d"))
-                           + parsers.colon
-                           * (parsers.dash
-                             - #(parsers.dash
-                                * (parsers.spacechar
-                                  + parsers.table_hline_separator
-                                  + parsers.newline)))^1
-                           * (parsers.colon * Cc("c")
-                             + parsers.dash * Cc("l"))
-parsers.table_hline = pipe_table_row(false
-                                    , parsers.table_hline_column
-                                    , parsers.table_hline_separator
-                                    , parsers.table_hline_column)
-parsers.table_caption_beginning = parsers.skipblanklines
-                                * parsers.nonindentspace
-                                * (P("Table")^-1 * parsers.colon)
-                                * parsers.optionalspace
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 %#### Parsers Used for HTML
 %
 % \end{markdown}
@@ -19759,31 +19347,6 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-parsers.OnlineImageURL
-                     = parsers.leader
-                     * parsers.onlineimageurl
-                     * parsers.optionaltitle
-
-parsers.LocalFilePath
-                     = parsers.leader
-                     * parsers.localfilepath
-                     * parsers.optionaltitle
-
-parsers.TildeFencedCode
-                     = parsers.fencehead(parsers.tilde)
-                     * Cs(parsers.fencedline(parsers.tilde)^0)
-                     * parsers.fencetail(parsers.tilde)
-
-parsers.BacktickFencedCode
-                     = parsers.fencehead(parsers.backtick)
-                     * Cs(parsers.fencedline(parsers.backtick)^0)
-                     * parsers.fencetail(parsers.backtick)
-
-parsers.JekyllFencedCode
-                     = parsers.fencehead(parsers.dash)
-                     * Cs(parsers.fencedline(parsers.dash)^0)
-                     * parsers.fencetail(parsers.dash)
-
 parsers.lineof = function(c)
     return (parsers.leader * (P(c) * parsers.optionalspace)^3
            * (parsers.newline * parsers.blankline^1
@@ -19797,19 +19360,6 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-parsers.defstartchar = S("~:")
-parsers.defstart     = ( parsers.defstartchar * #parsers.spacing
-                                              * (parsers.tab + parsers.space^-3)
-                     + parsers.space * parsers.defstartchar * #parsers.spacing
-                                     * (parsers.tab + parsers.space^-2)
-                     + parsers.space * parsers.space * parsers.defstartchar
-                                     * #parsers.spacing
-                                     * (parsers.tab + parsers.space^-1)
-                     + parsers.space * parsers.space * parsers.space
-                                     * parsers.defstartchar * #parsers.spacing
-                     )
-
-parsers.dlchunk = Cs(parsers.line * (parsers.indentedline - parsers.blankline)^0)
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -19870,31 +19420,58 @@
 % \end{markdown}
 %  \begin{macrocode}
 M.reader = {}
-function M.reader.new(writer, options)
+function M.reader.new(writer, options, extensions)
   local self = {}
-  options = options or {}
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
-% Make the `options` table inherit from the \luamref{defaultOptions} table.
+% Make the `writer` parameter available as \luamdef{reader->writer}, so that it
+% is accessible from extensions.
 %
 % \end{markdown}
 %  \begin{macrocode}
-  setmetatable(options, { __index = function (_, key)
-    return defaultOptions[key] end })
+  self.writer = writer
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
+% Create a \luamdef{reader->parsers} hash table that stores \acro{peg} patterns
+% that depend on the received `options`. Make \luamdef{reader->parsers} inherit
+% from the global \luamref{parsers} table.
+%
+% \end{markdown}
+%  \begin{macrocode}
+  self.parsers = {}
+  (function(parsers)
+    setmetatable(self.parsers, {
+      __index = function (_, key)
+        return parsers[key]
+      end
+    })
+  end)(parsers)
+%    \end{macrocode}
+% \begin{markdown}
+%
+% Make \luamref{reader->parsers} available as a local `parsers` variable that
+% will shadow the global \luamref{parsers} table and will make
+% \luamref{reader->parsers} easier to type in the rest of the reader code.
+%
+% \end{markdown}
+%  \begin{macrocode}
+  local parsers = self.parsers
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
 %#### Top-Level Helper Functions
-% Define \luamdef{normalize_tag} as a function that normalizes a markdown
-% reference tag by lowercasing it, and by collapsing any adjacent whitespace
-% characters.
+% Define \luamdef{reader->normalize_tag} as a function that normalizes a
+% markdown reference tag by lowercasing it, and by collapsing any adjacent
+% whitespace characters.
 %
 % \end{markdown}
 %  \begin{macrocode}
-  local function normalize_tag(tag)
+  function self.normalize_tag(tag)
     return string.lower(
       gsub(util.rope_to_string(tag), "[ \n\r\t]+", " "))
   end
@@ -19922,47 +19499,43 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-  local expandtabs
   if options.preserveTabs then
-    expandtabs = function(s) return s end
+    self.expandtabs = function(s) return s end
   else
-    expandtabs = function(s)
-                   if s:find("\t") then
-                     return iterlines(s, util.expand_tabs_in_line)
-                   else
-                     return s
-                   end
-                 end
+    self.expandtabs = function(s)
+                        if s:find("\t") then
+                          return iterlines(s, util.expand_tabs_in_line)
+                        else
+                          return s
+                        end
+                      end
   end
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
-% The \luamdef{larsers} (as in ``local \luamref{parsers}'') hash table stores
-% \acro{peg} patterns that depend on the received `options`, which impedes
-% their reuse between different \luamref{reader} objects.
+%#### High-Level Parser Functions
 %
-% \end{markdown}
-%  \begin{macrocode}
-  local larsers    = {}
-%    \end{macrocode}
-% \par
-% \begin{markdown}
+% Create a \luamdef{reader->parser_functions} hash table that stores high-level
+% parser functions. Define \luamdef{reader->create_parser} as a function that
+% will create a high-level parser function \luamdef{reader->parser_functions.name},
+% that matches input using grammar `grammar`. If `toplevel` is true, the input
+% is expected to come straight from the user, not from a recursive call, and
+% will be preprocessed.
 %
-%#### Top-Level Parser Functions
-%
 % \end{markdown}
 %  \begin{macrocode}
-  local function create_parser(name, grammar, toplevel)
-    return function(str)
+  self.parser_functions = {}
+  self.create_parser = function(name, grammar, toplevel)
+    self.parser_functions[name] = function(str)
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
-% If the parser is top-level and the \Opt{stripIndent} Lua option is enabled,
-% we will first expand tabs in the input string `str` into spaces and then we
-% will count the minimum indent across all lines, skipping blank lines. Next,
-% we will remove the minimum indent from all lines.
+% If the parser function is top-level and the \Opt{stripIndent} Lua option is
+% enabled, we will first expand tabs in the input string `str` into spaces
+% and then we will count the minimum indent across all lines, skipping
+% blank lines. Next, we will remove the minimum indent from all lines.
 %
 % \end{markdown}
 %  \begin{macrocode}
@@ -20007,47 +19580,40 @@
     end
   end
 
-  local parse_blocks
-    = create_parser("parse_blocks",
-                    function()
-                      return larsers.blocks
-                    end, true)
+  self.create_parser("parse_blocks",
+                     function()
+                       return parsers.blocks
+                     end, true)
 
-  local parse_blocks_nested
-    = create_parser("parse_blocks_nested",
-                    function()
-                      return larsers.blocks_nested
-                    end, false)
+  self.create_parser("parse_blocks_nested",
+                     function()
+                       return parsers.blocks_nested
+                     end, false)
 
-  local parse_inlines
-    = create_parser("parse_inlines",
-                    function()
-                      return larsers.inlines
-                    end, false)
+  self.create_parser("parse_inlines",
+                     function()
+                       return parsers.inlines
+                     end, false)
 
-  local parse_inlines_no_link
-    = create_parser("parse_inlines_no_link",
-                    function()
-                      return larsers.inlines_no_link
-                    end, false)
+  self.create_parser("parse_inlines_no_link",
+                     function()
+                       return parsers.inlines_no_link
+                     end, false)
 
-  local parse_inlines_no_inline_note
-    = create_parser("parse_inlines_no_inline_note",
-                    function()
-                      return larsers.inlines_no_inline_note
-                    end, false)
+  self.create_parser("parse_inlines_no_inline_note",
+                     function()
+                       return parsers.inlines_no_inline_note
+                     end, false)
 
-  local parse_inlines_no_html
-    = create_parser("parse_inlines_no_html",
-                    function()
-                      return larsers.inlines_no_html
-                    end, false)
+  self.create_parser("parse_inlines_no_html",
+                     function()
+                       return parsers.inlines_no_html
+                     end, false)
 
-  local parse_inlines_nbsp
-    = create_parser("parse_inlines_nbsp",
-                    function()
-                      return larsers.inlines_nbsp
-                    end, false)
+  self.create_parser("parse_inlines_nbsp",
+                     function()
+                       return parsers.inlines_nbsp
+                     end, false)
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -20057,22 +19623,22 @@
 % \end{markdown}
 %  \begin{macrocode}
   if options.hashEnumerators then
-    larsers.dig = parsers.digit + parsers.hash
+    parsers.dig = parsers.digit + parsers.hash
   else
-    larsers.dig = parsers.digit
+    parsers.dig = parsers.digit
   end
 
-  larsers.enumerator = C(larsers.dig^3 * parsers.period) * #parsers.spacing
-                     + C(larsers.dig^2 * parsers.period) * #parsers.spacing
+  parsers.enumerator = C(parsers.dig^3 * parsers.period) * #parsers.spacing
+                     + C(parsers.dig^2 * parsers.period) * #parsers.spacing
                                        * (parsers.tab + parsers.space^1)
-                     + C(larsers.dig * parsers.period) * #parsers.spacing
+                     + C(parsers.dig * parsers.period) * #parsers.spacing
                                      * (parsers.tab + parsers.space^-2)
-                     + parsers.space * C(larsers.dig^2 * parsers.period)
+                     + parsers.space * C(parsers.dig^2 * parsers.period)
                                      * #parsers.spacing
-                     + parsers.space * C(larsers.dig * parsers.period)
+                     + parsers.space * C(parsers.dig * parsers.period)
                                      * #parsers.spacing
                                      * (parsers.tab + parsers.space^-1)
-                     + parsers.space * parsers.space * C(larsers.dig^1
+                     + parsers.space * parsers.space * C(parsers.dig^1
                                      * parsers.period) * #parsers.spacing
 %    \end{macrocode}
 % \par
@@ -20083,7 +19649,7 @@
 % \end{markdown}
 %  \begin{macrocode}
   -- strip off leading > and indents, and run through blocks
-  larsers.blockquote_body = ((parsers.leader * parsers.more * parsers.space^-1)/""
+  parsers.blockquote_body = ((parsers.leader * parsers.more * parsers.space^-1)/""
                              * parsers.linechar^0 * parsers.newline)^1
                             * (-(parsers.leader * parsers.more
                                 + parsers.blankline) * parsers.linechar^1
@@ -20090,7 +19656,7 @@
                               * parsers.newline)^0
                             
   if not options.breakableBlockquotes then
-    larsers.blockquote_body = larsers.blockquote_body
+    parsers.blockquote_body = parsers.blockquote_body
                             * (parsers.blankline^0 / "")
   end
 %    \end{macrocode}
@@ -20097,103 +19663,14 @@
 % \par
 % \begin{markdown}
 %
-%#### Parsers Used for Citations (local)
-%
-% \end{markdown}
-%  \begin{macrocode}
-  larsers.citations = function(text_cites, raw_cites)
-      local function normalize(str)
-          if str == "" then
-              str = nil
-          else
-              str = (options.citationNbsps and parse_inlines_nbsp or
-                parse_inlines)(str)
-          end
-          return str
-      end
-
-      local cites = {}
-      for i = 1,#raw_cites,4 do
-          cites[#cites+1] = {
-              prenote = normalize(raw_cites[i]),
-              suppress_author = raw_cites[i+1] == "-",
-              name = writer.citation(raw_cites[i+2]),
-              postnote = normalize(raw_cites[i+3]),
-          }
-      end
-      return writer.citations(text_cites, cites)
-  end
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 %#### Parsers Used for Footnotes (local)
 %
 % \end{markdown}
 %  \begin{macrocode}
-  local rawnotes = {}
-
-  -- like indirect_link
-  local function lookup_note(ref)
-    return writer.defer_call(function()
-      local found = rawnotes[normalize_tag(ref)]
-      if found then
-        return writer.note(parse_blocks_nested(found))
-      else
-        return {"[", parse_inlines("^" .. ref), "]"}
-      end
-    end)
-  end
-
-  local function register_note(ref,rawnote)
-    rawnotes[normalize_tag(ref)] = rawnote
-    return ""
-  end
-
-  larsers.NoteRef    = parsers.RawNoteRef / lookup_note
-
-
-  larsers.NoteBlock  = parsers.leader * parsers.RawNoteRef * parsers.colon
-                     * parsers.spnl * parsers.indented_blocks(parsers.chunk)
-                     / register_note
-
-  larsers.InlineNote = parsers.circumflex
-                     * (parsers.tag / parse_inlines_no_inline_note) -- no notes inside notes
-                     / writer.note
 %    \end{macrocode}
 % \par
 % \begin{markdown}
 %
-%#### Parsers Used for Tables (local)
-%
-% \end{markdown}
-%  \begin{macrocode}
-larsers.table_row = pipe_table_row(true
-                                  , (C((parsers.linechar - parsers.pipe)^1)
-                                    / parse_inlines)
-                                  , parsers.pipe
-                                  , (C((parsers.linechar - parsers.pipe)^0)
-                                    / parse_inlines))
-
-if options.tableCaptions then
-  larsers.table_caption = #parsers.table_caption_beginning
-                        * parsers.table_caption_beginning
-                        * Ct(parsers.IndentedInline^1)
-                        * parsers.newline
-else
-  larsers.table_caption = parsers.fail
-end
-
-larsers.PipeTable = Ct(larsers.table_row * parsers.newline
-                    * parsers.table_hline
-                    * (parsers.newline * larsers.table_row)^0)
-                  / make_pipe_table_rectangular
-                  * larsers.table_caption^-1
-                  / writer.table
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 %#### Helpers for Links and References (local)
 %
 % \end{markdown}
@@ -20203,7 +19680,7 @@
 
   -- add a reference to the list
   local function register_link(tag,url,title)
-      references[normalize_tag(tag)] = { url = url, title = title }
+      references[self.normalize_tag(tag)] = { url = url, title = title }
       return ""
   end
 
@@ -20218,16 +19695,20 @@
           tag = label
           tagpart = "[]"
       else
-          tagpart = {"[", parse_inlines(tag), "]"}
+          tagpart = {"[",
+            self.parser_functions.parse_inlines(tag),
+            "]"}
       end
       if sps then
         tagpart = {sps, tagpart}
       end
-      local r = references[normalize_tag(tag)]
+      local r = references[self.normalize_tag(tag)]
       if r then
         return r
       else
-        return nil, {"[", parse_inlines(label), "]", tagpart}
+        return nil, {"[",
+          self.parser_functions.parse_inlines(label),
+          "]", tagpart}
       end
   end
 
@@ -20237,7 +19718,9 @@
     return writer.defer_call(function()
       local r,fallback = lookup_reference(label,sps,tag)
       if r then
-        return writer.link(parse_inlines_no_link(label), r.url, r.title)
+        return writer.link(
+          self.parser_functions.parse_inlines_no_link(label),
+          r.url, r.title)
       else
         return fallback
       end
@@ -20264,56 +19747,51 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-  larsers.Str      = (parsers.normalchar * (parsers.normalchar + parsers.at)^0)
+  parsers.Str      = (parsers.normalchar * (parsers.normalchar + parsers.at)^0)
                    / writer.string
 
-  larsers.Symbol   = (parsers.specialchar - parsers.tightblocksep)
+  parsers.Symbol   = (parsers.specialchar - parsers.tightblocksep)
                    / writer.string
 
-  larsers.Ellipsis = P("...") / writer.ellipsis
+  parsers.Ellipsis = P("...") / writer.ellipsis
 
-  larsers.Smart    = larsers.Ellipsis
+  parsers.Smart    = parsers.Ellipsis
 
-  larsers.Code     = parsers.inticks / writer.code
+  parsers.Code     = parsers.inticks / writer.code
 
   if options.blankBeforeBlockquote then
-    larsers.bqstart = parsers.fail
+    parsers.bqstart = parsers.fail
   else
-    larsers.bqstart = parsers.more
+    parsers.bqstart = parsers.more
   end
 
   if options.blankBeforeHeading then
-    larsers.headerstart = parsers.fail
+    parsers.headerstart = parsers.fail
   else
-    larsers.headerstart = parsers.hash
+    parsers.headerstart = parsers.hash
                         + (parsers.line * (parsers.equal^1 + parsers.dash^1)
                         * parsers.optionalspace * parsers.newline)
   end
 
-  if not options.fencedCode or options.blankBeforeCodeFence then
-    larsers.fencestart = parsers.fail
-  else
-    larsers.fencestart = parsers.fencehead(parsers.backtick)
-                       + parsers.fencehead(parsers.tilde)
-  end
+  parsers.EndlineExceptions
+                     = parsers.blankline -- paragraph break
+                     + parsers.tightblocksep  -- nested list
+                     + parsers.eof       -- end of document
+                     + parsers.bqstart
+                     + parsers.headerstart
 
-  larsers.Endline   = parsers.newline * -( -- newline, but not before...
-                        parsers.blankline -- paragraph break
-                      + parsers.tightblocksep  -- nested list
-                      + parsers.eof       -- end of document
-                      + larsers.bqstart
-                      + larsers.headerstart
-                      + larsers.fencestart
-                    ) * parsers.spacechar^0
+  parsers.Endline   = parsers.newline
+                    * -V("EndlineExceptions")
+                    * parsers.spacechar^0
                     / (options.hardLineBreaks and writer.linebreak
                                                or writer.space)
 
-  larsers.OptionalIndent
+  parsers.OptionalIndent
                      = parsers.spacechar^1 / writer.space
 
-  larsers.Space      = parsers.spacechar^2 * larsers.Endline / writer.linebreak
-                     + parsers.spacechar^1 * larsers.Endline^-1 * parsers.eof / ""
-                     + parsers.spacechar^1 * larsers.Endline
+  parsers.Space      = parsers.spacechar^2 * parsers.Endline / writer.linebreak
+                     + parsers.spacechar^1 * parsers.Endline^-1 * parsers.eof / ""
+                     + parsers.spacechar^1 * parsers.Endline
                                            * parsers.optionalspace
                                            / (options.hardLineBreaks
                                               and writer.linebreak
@@ -20321,22 +19799,17 @@
                      + parsers.spacechar^1 * parsers.optionalspace
                                            / writer.space
 
-  larsers.NonbreakingEndline
-                    = parsers.newline * -( -- newline, but not before...
-                        parsers.blankline -- paragraph break
-                      + parsers.tightblocksep  -- nested list
-                      + parsers.eof       -- end of document
-                      + larsers.bqstart
-                      + larsers.headerstart
-                      + larsers.fencestart
-                    ) * parsers.spacechar^0
+  parsers.NonbreakingEndline
+                    = parsers.newline
+                    * -V("EndlineExceptions")
+                    * parsers.spacechar^0
                     / (options.hardLineBreaks and writer.linebreak
                                                or writer.nbsp)
 
-  larsers.NonbreakingSpace
-                  = parsers.spacechar^2 * larsers.Endline / writer.linebreak
-                  + parsers.spacechar^1 * larsers.Endline^-1 * parsers.eof / ""
-                  + parsers.spacechar^1 * larsers.Endline
+  parsers.NonbreakingSpace
+                  = parsers.spacechar^2 * parsers.Endline / writer.linebreak
+                  + parsers.spacechar^1 * parsers.Endline^-1 * parsers.eof / ""
+                  + parsers.spacechar^1 * parsers.Endline
                                         * parsers.optionalspace
                                         / (options.hardLineBreaks
                                            and writer.linebreak
@@ -20345,28 +19818,28 @@
                                         / writer.nbsp
 
   if options.underscores then
-    larsers.Strong = ( parsers.between(parsers.Inline, parsers.doubleasterisks,
+    parsers.Strong = ( parsers.between(parsers.Inline, parsers.doubleasterisks,
                                        parsers.doubleasterisks)
                      + parsers.between(parsers.Inline, parsers.doubleunderscores,
                                        parsers.doubleunderscores)
                      ) / writer.strong
 
-    larsers.Emph   = ( parsers.between(parsers.Inline, parsers.asterisk,
+    parsers.Emph   = ( parsers.between(parsers.Inline, parsers.asterisk,
                                        parsers.asterisk)
                      + parsers.between(parsers.Inline, parsers.underscore,
                                        parsers.underscore)
                      ) / writer.emphasis
   else
-    larsers.Strong = ( parsers.between(parsers.Inline, parsers.doubleasterisks,
+    parsers.Strong = ( parsers.between(parsers.Inline, parsers.doubleasterisks,
                                        parsers.doubleasterisks)
                      ) / writer.strong
 
-    larsers.Emph   = ( parsers.between(parsers.Inline, parsers.asterisk,
+    parsers.Emph   = ( parsers.between(parsers.Inline, parsers.asterisk,
                                        parsers.asterisk)
                      ) / writer.emphasis
   end
 
-  larsers.AutoLinkUrl    = parsers.less
+  parsers.AutoLinkUrl    = parsers.less
                          * C(parsers.alphanumeric^1 * P("://") * parsers.urlchar^1)
                          * parsers.more
                          / function(url)
@@ -20373,7 +19846,7 @@
                              return writer.link(writer.escape(url), url)
                            end
 
-  larsers.AutoLinkEmail = parsers.less
+  parsers.AutoLinkEmail = parsers.less
                         * C((parsers.alphanumeric + S("-._+"))^1
                         * P("@") * parsers.urlchar^1)
                         * parsers.more
@@ -20382,7 +19855,7 @@
                                                "mailto:"..email)
                           end
 
-  larsers.AutoLinkRelativeReference
+  parsers.AutoLinkRelativeReference
                          = parsers.less
                          * C(parsers.urlchar^1)
                          * parsers.more
@@ -20390,7 +19863,7 @@
                              return writer.link(writer.escape(url), url)
                            end
 
-  larsers.DirectLink    = (parsers.tag / parse_inlines_no_link)  -- no links inside links
+  parsers.DirectLink    = (parsers.tag / self.parser_functions.parse_inlines_no_link)
                         * parsers.spnl
                         * parsers.lparent
                         * (parsers.url + Cc(""))  -- link can be empty [foo]()
@@ -20398,14 +19871,14 @@
                         * parsers.rparent
                         / writer.link
 
-  larsers.IndirectLink  = parsers.tag * (C(parsers.spnl) * parsers.tag)^-1
+  parsers.IndirectLink  = parsers.tag * (C(parsers.spnl) * parsers.tag)^-1
                         / indirect_link
 
   -- parse a link or image (direct or indirect)
-  larsers.Link          = larsers.DirectLink + larsers.IndirectLink
+  parsers.Link          = parsers.DirectLink + parsers.IndirectLink
 
-  larsers.DirectImage   = parsers.exclamation
-                        * (parsers.tag / parse_inlines)
+  parsers.DirectImage   = parsers.exclamation
+                        * (parsers.tag / self.parser_functions.parse_inlines)
                         * parsers.spnl
                         * parsers.lparent
                         * (parsers.url + Cc(""))  -- link can be empty [foo]()
@@ -20413,47 +19886,25 @@
                         * parsers.rparent
                         / writer.image
 
-  larsers.IndirectImage = parsers.exclamation * parsers.tag
+  parsers.IndirectImage = parsers.exclamation * parsers.tag
                         * (C(parsers.spnl) * parsers.tag)^-1 / indirect_image
 
-  larsers.Image         = larsers.DirectImage + larsers.IndirectImage
+  parsers.Image         = parsers.DirectImage + parsers.IndirectImage
 
-  larsers.TextCitations = Ct((parsers.spnl
-                        * Cc("")
-                        * parsers.citation_name
-                        * ((parsers.spnl
-                            * parsers.lbracket
-                            * parsers.citation_headless_body
-                            * parsers.rbracket) + Cc("")))^1)
-                        / function(raw_cites)
-                            return larsers.citations(true, raw_cites)
-                          end
-  
-  larsers.ParenthesizedCitations
-                        = Ct((parsers.spnl
-                        * parsers.lbracket
-                        * parsers.citation_body
-                        * parsers.rbracket)^1)
-                        / function(raw_cites)
-                            return larsers.citations(false, raw_cites)
-                          end
-
-  larsers.Citations     = larsers.TextCitations + larsers.ParenthesizedCitations
-
   -- avoid parsing long strings of * or _ as emph/strong
-  larsers.UlOrStarLine  = parsers.asterisk^4 + parsers.underscore^4
+  parsers.UlOrStarLine  = parsers.asterisk^4 + parsers.underscore^4
                         / writer.string
 
-  larsers.EscapedChar   = parsers.backslash * C(parsers.escapable) / writer.string
+  parsers.EscapedChar   = parsers.backslash * C(parsers.escapable) / writer.string
   
-  larsers.InlineHtml    = parsers.emptyelt_any / writer.inline_html_tag
-                        + (parsers.htmlcomment / parse_inlines_no_html)
+  parsers.InlineHtml    = parsers.emptyelt_any / writer.inline_html_tag
+                        + (parsers.htmlcomment / self.parser_functions.parse_inlines_no_html)
                         / writer.inline_html_comment
                         + parsers.htmlinstruction
                         + parsers.openelt_any / writer.inline_html_tag
                         + parsers.closeelt_any / writer.inline_html_tag
   
-  larsers.HtmlEntity    = parsers.hexentity / entities.hex_entity  / writer.string
+  parsers.HtmlEntity    = parsers.hexentity / entities.hex_entity  / writer.string
                         + parsers.decentity / entities.dec_entity  / writer.string
                         + parsers.tagentity / entities.char_entity / writer.string
 %    \end{macrocode}
@@ -20464,12 +19915,7 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-  larsers.ContentBlock = parsers.leader
-                       * (parsers.localfilepath + parsers.onlineimageurl)
-                       * parsers.contentblock_tail
-                       / writer.contentblock
-
-  larsers.DisplayHtml  = (parsers.htmlcomment / parse_blocks_nested)
+  parsers.DisplayHtml  = (parsers.htmlcomment / self.parser_functions.parse_blocks_nested)
                        / writer.block_html_comment
                        + parsers.emptyelt_block / writer.block_html_element
                        + parsers.openelt_exact("hr") / writer.block_html_element
@@ -20476,60 +19922,22 @@
                        + parsers.in_matched_block_tags / writer.block_html_element
                        + parsers.htmlinstruction
 
-  larsers.Verbatim     = Cs( (parsers.blanklines
+  parsers.Verbatim     = Cs( (parsers.blanklines
                            * ((parsers.indentedline - parsers.blankline))^1)^1
-                           ) / expandtabs / writer.verbatim
+                           ) / self.expandtabs / writer.verbatim
 
-  larsers.FencedCode   = (parsers.TildeFencedCode
-                         + parsers.BacktickFencedCode)
-                       / function(infostring, code)
-                           return writer.fencedCode(writer.string(infostring),
-                                                    expandtabs(code))
-                         end
+  parsers.Blockquote   = Cs(parsers.blockquote_body^1)
+                       / self.parser_functions.parse_blocks_nested
+                       / writer.blockquote
 
-  larsers.JekyllData   = Cmt( C((parsers.line - P("---") - P("..."))^0)
-                            , function(s, i, text)
-                                local data
-                                local ran_ok, error = pcall(function()
-                                  local tinyyaml = require("markdown-tinyyaml")
-                                  data = tinyyaml.parse(text, {timestamps=false})
-                                end)
-                                if ran_ok and data ~= nil then
-                                  return true, writer.jekyllData(data, function(s)
-                                    return parse_blocks_nested(s)
-                                  end, nil)
-                                else
-                                  return false
-                                end
-                              end
-                            )
-
-  larsers.UnexpectedJekyllData
-                       = P("---")
-                       * parsers.blankline / 0
-                       * #(-parsers.blankline)  -- if followed by blank, it's an hrule
-                       * larsers.JekyllData
-                       * (P("---") + P("..."))
-
-  larsers.ExpectedJekyllData
-                       = ( P("---")
-                         * parsers.blankline / 0
-                         * #(-parsers.blankline)  -- if followed by blank, it's an hrule
-                         )^-1
-                       * larsers.JekyllData
-                       * (P("---") + P("..."))^-1
-
-  larsers.Blockquote   = Cs(larsers.blockquote_body^1)
-                       / parse_blocks_nested / writer.blockquote
-
-  larsers.HorizontalRule = ( parsers.lineof(parsers.asterisk)
+  parsers.HorizontalRule = ( parsers.lineof(parsers.asterisk)
                            + parsers.lineof(parsers.dash)
                            + parsers.lineof(parsers.underscore)
                            ) / writer.hrule
 
-  larsers.Reference    = parsers.define_reference_parser / register_link
+  parsers.Reference    = parsers.define_reference_parser / register_link
 
-  larsers.Paragraph    = parsers.nonindentspace * Ct(parsers.Inline^1)
+  parsers.Paragraph    = parsers.nonindentspace * Ct(parsers.Inline^1)
                        * ( parsers.newline
                        * ( parsers.blankline^1
                          + #parsers.hash
@@ -20539,7 +19947,7 @@
                        + parsers.eof )
                        / writer.paragraph
 
-  larsers.Plain        = parsers.nonindentspace * Ct(parsers.Inline^1)
+  parsers.Plain        = parsers.nonindentspace * Ct(parsers.Inline^1)
                        / writer.plain
 %    \end{macrocode}
 % \par
@@ -20549,50 +19957,50 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-  larsers.starter = parsers.bullet + larsers.enumerator
+  parsers.starter = parsers.bullet + parsers.enumerator
 
   if options.taskLists then
-    larsers.tickbox = ( parsers.ticked_box
+    parsers.tickbox = ( parsers.ticked_box
                       + parsers.halfticked_box
                       + parsers.unticked_box
                       ) / writer.tickbox
   else
-     larsers.tickbox = parsers.fail
+     parsers.tickbox = parsers.fail
   end
 
   -- we use \001 as a separator between a tight list item and a
   -- nested list under it.
-  larsers.NestedList            = Cs((parsers.optionallyindentedline
-                                     - larsers.starter)^1)
+  parsers.NestedList            = Cs((parsers.optionallyindentedline
+                                     - parsers.starter)^1)
                                 / function(a) return "\001"..a end
 
-  larsers.ListBlockLine         = parsers.optionallyindentedline
+  parsers.ListBlockLine         = parsers.optionallyindentedline
                                 - parsers.blankline - (parsers.indent^-1
-                                                      * larsers.starter)
+                                                      * parsers.starter)
 
-  larsers.ListBlock             = parsers.line * larsers.ListBlockLine^0
+  parsers.ListBlock             = parsers.line * parsers.ListBlockLine^0
 
-  larsers.ListContinuationBlock = parsers.blanklines * (parsers.indent / "")
-                                * larsers.ListBlock
+  parsers.ListContinuationBlock = parsers.blanklines * (parsers.indent / "")
+                                * parsers.ListBlock
 
-  larsers.TightListItem = function(starter)
-      return -larsers.HorizontalRule
-             * (Cs(starter / "" * larsers.tickbox^-1 * larsers.ListBlock * larsers.NestedList^-1)
-               / parse_blocks_nested)
+  parsers.TightListItem = function(starter)
+      return -parsers.HorizontalRule
+             * (Cs(starter / "" * parsers.tickbox^-1 * parsers.ListBlock * parsers.NestedList^-1)
+               / self.parser_functions.parse_blocks_nested)
              * -(parsers.blanklines * parsers.indent)
   end
 
-  larsers.LooseListItem = function(starter)
-      return -larsers.HorizontalRule
-             * Cs( starter / "" * larsers.tickbox^-1 * larsers.ListBlock * Cc("\n")
-               * (larsers.NestedList + larsers.ListContinuationBlock^0)
+  parsers.LooseListItem = function(starter)
+      return -parsers.HorizontalRule
+             * Cs( starter / "" * parsers.tickbox^-1 * parsers.ListBlock * Cc("\n")
+               * (parsers.NestedList + parsers.ListContinuationBlock^0)
                * (parsers.blanklines / "\n\n")
-               ) / parse_blocks_nested
+               ) / self.parser_functions.parse_blocks_nested
   end
 
-  larsers.BulletList = ( Ct(larsers.TightListItem(parsers.bullet)^1) * Cc(true)
+  parsers.BulletList = ( Ct(parsers.TightListItem(parsers.bullet)^1) * Cc(true)
                        * parsers.skipblanklines * -parsers.bullet
-                       + Ct(larsers.LooseListItem(parsers.bullet)^1) * Cc(false)
+                       + Ct(parsers.LooseListItem(parsers.bullet)^1) * Cc(false)
                        * parsers.skipblanklines )
                      / writer.bulletlist
 
@@ -20608,35 +20016,14 @@
     return writer.orderedlist(items,tight,startNumber)
   end
 
-  larsers.OrderedList = Cg(larsers.enumerator, "listtype") *
-                      ( Ct(larsers.TightListItem(Cb("listtype"))
-                          * larsers.TightListItem(larsers.enumerator)^0)
-                      * Cc(true) * parsers.skipblanklines * -larsers.enumerator
-                      + Ct(larsers.LooseListItem(Cb("listtype"))
-                          * larsers.LooseListItem(larsers.enumerator)^0)
+  parsers.OrderedList = Cg(parsers.enumerator, "listtype") *
+                      ( Ct(parsers.TightListItem(Cb("listtype"))
+                          * parsers.TightListItem(parsers.enumerator)^0)
+                      * Cc(true) * parsers.skipblanklines * -parsers.enumerator
+                      + Ct(parsers.LooseListItem(Cb("listtype"))
+                          * parsers.LooseListItem(parsers.enumerator)^0)
                       * Cc(false) * parsers.skipblanklines
                       ) * Cb("listtype") / ordered_list
-
-  local function definition_list_item(term, defs, tight)
-    return { term = parse_inlines(term), definitions = defs }
-  end
-
-  larsers.DefinitionListItemLoose = C(parsers.line) * parsers.skipblanklines
-                                  * Ct((parsers.defstart
-                                       * parsers.indented_blocks(parsers.dlchunk)
-                                       / parse_blocks_nested)^1)
-                                  * Cc(false) / definition_list_item
-
-  larsers.DefinitionListItemTight = C(parsers.line)
-                                  * Ct((parsers.defstart * parsers.dlchunk
-                                       / parse_blocks_nested)^1)
-                                  * Cc(true) / definition_list_item
-
-  larsers.DefinitionList = ( Ct(larsers.DefinitionListItemLoose^1) * Cc(false)
-                           + Ct(larsers.DefinitionListItemTight^1)
-                           * (parsers.skipblanklines
-                             * -larsers.DefinitionListItemLoose * Cc(true))
-                           ) / writer.definitionlist
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -20645,9 +20032,8 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-  larsers.Blank        = parsers.blankline / ""
-                       + larsers.NoteBlock
-                       + larsers.Reference
+  parsers.Blank        = parsers.blankline / ""
+                       + parsers.Reference
                        + (parsers.tightblocksep / "\n")
 %    \end{macrocode}
 % \par
@@ -20658,65 +20044,24 @@
 % \end{markdown}
 %  \begin{macrocode}
   -- parse atx header
-  if options.headerAttributes then
-    larsers.AtxHeading = Cg(parsers.HeadingStart,"level")
-                       * parsers.optionalspace
-                       * (C(((parsers.linechar
-                             - ((parsers.hash^1
-                                * parsers.optionalspace
-                                * parsers.HeadingAttributes^-1
-                                + parsers.HeadingAttributes)
-                               * parsers.optionalspace
-                               * parsers.newline))
-                            * (parsers.linechar
-                              - parsers.hash
-                              - parsers.lbrace)^0)^1)
-                           / parse_inlines)
-                       * Cg(Ct(parsers.newline
-                              + (parsers.hash^1
-                                * parsers.optionalspace
-                                * parsers.HeadingAttributes^-1
-                                + parsers.HeadingAttributes)
-                              * parsers.optionalspace
-                              * parsers.newline), "attributes")
-                       * Cb("level")
-                       * Cb("attributes")
-                       / writer.heading
+  parsers.AtxHeading = Cg(parsers.HeadingStart,"level")
+                     * parsers.optionalspace
+                     * (C(parsers.line)
+                       / strip_atx_end
+                       / self.parser_functions.parse_inlines)
+                     * Cb("level")
+                     / writer.heading
 
-    larsers.SetextHeading = #(parsers.line * S("=-"))
-                          * (C(((parsers.linechar
-                                - (parsers.HeadingAttributes
-                                  * parsers.optionalspace
-                                  * parsers.newline))
-                               * (parsers.linechar
-                                 - parsers.lbrace)^0)^1)
-                              / parse_inlines)
-                          * Cg(Ct(parsers.newline
-                                 + (parsers.HeadingAttributes
-                                   * parsers.optionalspace
-                                   * parsers.newline)), "attributes")
-                          * parsers.HeadingLevel
-                          * Cb("attributes")
-                          * parsers.optionalspace
-                          * parsers.newline
-                          / writer.heading
-  else
-    larsers.AtxHeading = Cg(parsers.HeadingStart,"level")
-                       * parsers.optionalspace
-                       * (C(parsers.line) / strip_atx_end / parse_inlines)
-                       * Cb("level")
-                       / writer.heading
+  parsers.SetextHeading = #(parsers.line * S("=-"))
+                        * Ct(parsers.linechar^1
+                            / self.parser_functions.parse_inlines)
+                        * parsers.newline
+                        * parsers.HeadingLevel
+                        * parsers.optionalspace
+                        * parsers.newline
+                        / writer.heading
 
-    larsers.SetextHeading = #(parsers.line * S("=-"))
-                          * Ct(parsers.linechar^1 / parse_inlines)
-                          * parsers.newline
-                          * parsers.HeadingLevel
-                          * parsers.optionalspace
-                          * parsers.newline
-                          / writer.heading
-  end
-
-  larsers.Heading = larsers.AtxHeading + larsers.SetextHeading
+  parsers.Heading = parsers.AtxHeading + parsers.SetextHeading
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -20723,9 +20068,12 @@
 %
 %#### Syntax Specification
 %
+% Create a \luamdef{reader->syntax} hash table that stores the \acro{peg}
+% grammar.
+%
 % \end{markdown}
 %  \begin{macrocode}
-  local syntax =
+  self.syntax =
     { "Blocks",
 
       Blocks                = ( V("ExpectedJekyllData")
@@ -20737,10 +20085,10 @@
                               * V("Block"))^0
                             * V("Blank")^0 * parsers.eof,
 
-      Blank                 = larsers.Blank,
+      Blank                 = parsers.Blank,
 
-      UnexpectedJekyllData  = larsers.UnexpectedJekyllData,
-      ExpectedJekyllData    = larsers.ExpectedJekyllData,
+      UnexpectedJekyllData  = parsers.fail,
+      ExpectedJekyllData    = parsers.fail,
 
       Block                 = V("ContentBlock")
                             + V("UnexpectedJekyllData")
@@ -20757,19 +20105,20 @@
                             + V("Paragraph")
                             + V("Plain"),
 
-      ContentBlock          = larsers.ContentBlock,
-      Blockquote            = larsers.Blockquote,
-      Verbatim              = larsers.Verbatim,
-      FencedCode            = larsers.FencedCode,
-      HorizontalRule        = larsers.HorizontalRule,
-      BulletList            = larsers.BulletList,
-      OrderedList           = larsers.OrderedList,
-      Heading               = larsers.Heading,
-      DefinitionList        = larsers.DefinitionList,
-      DisplayHtml           = larsers.DisplayHtml,
-      Paragraph             = larsers.Paragraph,
-      PipeTable             = larsers.PipeTable,
-      Plain                 = larsers.Plain,
+      ContentBlock          = parsers.fail,
+      Blockquote            = parsers.Blockquote,
+      Verbatim              = parsers.Verbatim,
+      FencedCode            = parsers.fail,
+      HorizontalRule        = parsers.HorizontalRule,
+      BulletList            = parsers.BulletList,
+      OrderedList           = parsers.OrderedList,
+      Heading               = parsers.Heading,
+      DefinitionList        = parsers.fail,
+      DisplayHtml           = parsers.DisplayHtml,
+      Paragraph             = parsers.Paragraph,
+      PipeTable             = parsers.fail,
+      Plain                 = parsers.Plain,
+      EndlineExceptions     = parsers.EndlineExceptions,
 
       Inline                = V("Str")
                             + V("Space")
@@ -20813,117 +20162,94 @@
                             + V("Smart")
                             + V("Symbol"),
 
-      Str                   = larsers.Str,
-      Space                 = larsers.Space,
-      OptionalIndent        = larsers.OptionalIndent,
-      Endline               = larsers.Endline,
-      UlOrStarLine          = larsers.UlOrStarLine,
-      Strong                = larsers.Strong,
-      Emph                  = larsers.Emph,
-      InlineNote            = larsers.InlineNote,
-      NoteRef               = larsers.NoteRef,
-      Citations             = larsers.Citations,
-      Link                  = larsers.Link,
-      Image                 = larsers.Image,
-      Code                  = larsers.Code,
-      AutoLinkUrl           = larsers.AutoLinkUrl,
-      AutoLinkEmail         = larsers.AutoLinkEmail,
+      Str                   = parsers.Str,
+      Space                 = parsers.Space,
+      OptionalIndent        = parsers.OptionalIndent,
+      Endline               = parsers.Endline,
+      UlOrStarLine          = parsers.UlOrStarLine,
+      Strong                = parsers.Strong,
+      Emph                  = parsers.Emph,
+      InlineNote            = parsers.fail,
+      NoteRef               = parsers.fail,
+      Citations             = parsers.fail,
+      Link                  = parsers.Link,
+      Image                 = parsers.Image,
+      Code                  = parsers.Code,
+      AutoLinkUrl           = parsers.AutoLinkUrl,
+      AutoLinkEmail         = parsers.AutoLinkEmail,
       AutoLinkRelativeReference
-                            = larsers.AutoLinkRelativeReference,
-      InlineHtml            = larsers.InlineHtml,
-      HtmlEntity            = larsers.HtmlEntity,
-      EscapedChar           = larsers.EscapedChar,
-      Smart                 = larsers.Smart,
-      Symbol                = larsers.Symbol,
+                            = parsers.AutoLinkRelativeReference,
+      InlineHtml            = parsers.InlineHtml,
+      HtmlEntity            = parsers.HtmlEntity,
+      EscapedChar           = parsers.EscapedChar,
+      Smart                 = parsers.Smart,
+      Symbol                = parsers.Symbol,
     }
 
-  if not options.citations then
-    syntax.Citations = parsers.fail
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Apply syntax extensions.
+%
+% \end{markdown}
+%  \begin{macrocode}
+  for _, extension in ipairs(extensions) do
+    extension.extend_writer(writer)
+    extension.extend_reader(self)
   end
 
-  if not options.contentBlocks then
-    syntax.ContentBlock = parsers.fail
-  end
-
   if not options.codeSpans then
-    syntax.Code = parsers.fail
+    self.syntax.Code = parsers.fail
   end
 
-  if not options.definitionLists then
-    syntax.DefinitionList = parsers.fail
-  end
-
-  if not options.fencedCode then
-    syntax.FencedCode = parsers.fail
-  end
-
-  if not options.footnotes then
-    syntax.NoteRef = parsers.fail
-  end
-
   if not options.html then
-    syntax.DisplayHtml = parsers.fail
-    syntax.InlineHtml = parsers.fail
-    syntax.HtmlEntity  = parsers.fail
+    self.syntax.DisplayHtml = parsers.fail
+    self.syntax.InlineHtml = parsers.fail
+    self.syntax.HtmlEntity  = parsers.fail
   end
 
-  if not options.inlineFootnotes then
-    syntax.InlineNote = parsers.fail
-  end
-
-  if not options.jekyllData then
-    syntax.UnexpectedJekyllData = parsers.fail
-  end
-
-  if not options.jekyllData or not options.expectJekyllData then
-    syntax.ExpectedJekyllData = parsers.fail
-  end
-
   if options.preserveTabs then
     options.stripIndent = false
   end
 
-  if not options.pipeTables then
-    syntax.PipeTable = parsers.fail
-  end
-
   if not options.smartEllipses then
-    syntax.Smart = parsers.fail
+    self.syntax.Smart = parsers.fail
   end
 
   if not options.relativeReferences then
-    syntax.AutoLinkRelativeReference = parsers.fail
+    self.syntax.AutoLinkRelativeReference = parsers.fail
   end
 
-  local blocks_nested_t = util.table_copy(syntax)
+  local blocks_nested_t = util.table_copy(self.syntax)
   blocks_nested_t.ExpectedJekyllData = parsers.fail
-  larsers.blocks_nested = Ct(blocks_nested_t)
+  parsers.blocks_nested = Ct(blocks_nested_t)
 
-  larsers.blocks = Ct(syntax)
+  parsers.blocks = Ct(self.syntax)
 
-  local inlines_t = util.table_copy(syntax)
+  local inlines_t = util.table_copy(self.syntax)
   inlines_t[1] = "Inlines"
   inlines_t.Inlines = parsers.Inline^0 * (parsers.spacing^0 * parsers.eof / "")
-  larsers.inlines = Ct(inlines_t)
+  parsers.inlines = Ct(inlines_t)
 
   local inlines_no_link_t = util.table_copy(inlines_t)
   inlines_no_link_t.Link = parsers.fail
-  larsers.inlines_no_link = Ct(inlines_no_link_t)
+  parsers.inlines_no_link = Ct(inlines_no_link_t)
 
   local inlines_no_inline_note_t = util.table_copy(inlines_t)
   inlines_no_inline_note_t.InlineNote = parsers.fail
-  larsers.inlines_no_inline_note = Ct(inlines_no_inline_note_t)
+  parsers.inlines_no_inline_note = Ct(inlines_no_inline_note_t)
 
   local inlines_no_html_t = util.table_copy(inlines_t)
   inlines_no_html_t.DisplayHtml = parsers.fail
   inlines_no_html_t.InlineHtml = parsers.fail
   inlines_no_html_t.HtmlEntity = parsers.fail
-  larsers.inlines_no_html = Ct(inlines_no_html_t)
+  parsers.inlines_no_html = Ct(inlines_no_html_t)
 
   local inlines_nbsp_t = util.table_copy(inlines_t)
-  inlines_nbsp_t.Endline = larsers.NonbreakingEndline
-  inlines_nbsp_t.Space = larsers.NonbreakingSpace
-  larsers.inlines_nbsp = Ct(inlines_nbsp_t)
+  inlines_nbsp_t.Endline = parsers.NonbreakingEndline
+  inlines_nbsp_t.Space = parsers.NonbreakingSpace
+  parsers.inlines_nbsp = Ct(inlines_nbsp_t)
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -20962,7 +20288,7 @@
 % \end{markdown}
 %  \begin{macrocode}
     local function convert(input)
-      local document = parse_blocks(input)
+      local document = self.parser_functions.parse_blocks(input)
       return util.rope_to_string(writer.document(document))
     end
     if options.eagerCache or options.finalizeCache then
@@ -21003,10 +20329,1084 @@
   return self
 end
 %    \end{macrocode}
+% \begin{markdown}
+%
+%### Syntax Extensions for Markdown
+%
+% Create \luamdef{extensions} hash table that contains syntax extensions.
+% Syntax extensions are functions that produce objects with two methods:
+% `extend_writer` and `extend_reader`. The `extend_writer` object takes a
+% \luamref{writer} object as the only parameter and mutates it. Similarly,
+% `extend_reader` takes a \luamref{reader} object as the only parameter and
+% mutates it.
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions = {}
+%    \end{macrocode}
+% \begin{markdown}
+%
+%#### Citations
+%
+% The \luamdef{extensions.citations} function implements the Pandoc citation
+% syntax extension. When the `citation_nbsps` parameter is `true`, the syntax
+% extension will replace regular spaces with non-breaking spaces inside the
+% prenotes and postnotes of citations.
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions.citations = function(citation_nbsps)
+%    \end{macrocode}
 % \par
 % \begin{markdown}
 %
+% Define table \luamdef{escaped_citation_chars} containing the characters to
+% escape in citations.
+%
+% \end{markdown}
+%  \begin{macrocode}
+  local escaped_citation_chars = {
+    ["{"] = "\\markdownRendererLeftBrace{}",
+    ["}"] = "\\markdownRendererRightBrace{}",
+    ["%"] = "\\markdownRendererPercentSign{}",
+    ["\\"] = "\\markdownRendererBackslash{}",
+    ["#"] = "\\markdownRendererHash{}",
+  }
+  return {
+    extend_writer = function(self)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Use the \luamref{escaped_citation_chars} to create the
+% \luamdef{escape_citation} escaper functions.
+%
+% \end{markdown}
+%  \begin{macrocode}
+      local escape_citation = util.escaper(
+        escaped_citation_chars,
+        self.escaped_minimal_strings)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamdef{writer->citation} as a function that will transform an input
+% citation name `c` to the output format. If \luamref{writer->hybrid} is `true`,
+% use the \luamref{writer->escape_minimal} function. Otherwise, use the
+% \luamref{escape_citation} function.
+%
+% \end{markdown}
+%  \begin{macrocode}
+      if self.hybrid then
+        self.citation = self.escape_minimal
+      else
+        self.citation = escape_citation
+      end
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamdef{writer->citations} as a function that will transform an
+% input array of citations `cites` to the output format. If `text_cites`
+% is enabled, the citations should be rendered in-text, when applicable.
+% The `cites` array contains tables with the following keys and values:
+% \begin{itemize}
+%   \item`suppress_author` -- If the value of the key is true, then the
+%     author of the work should be omitted in the citation, when applicable.
+%   \item`prenote` -- The value of the key is either `nil` or a rope
+%     that should be inserted before the citation.
+%   \item`postnote` -- The value of the key is either `nil` or a rope
+%     that should be inserted after the citation.
+%   \item`name` -- The value of this key is the citation name.
+% \end{itemize}
+%
+% \end{markdown}
+%  \begin{macrocode}
+      function self.citations(text_cites, cites)
+        local buffer = {"\\markdownRenderer", text_cites and "TextCite" or "Cite",
+          "{", #cites, "}"}
+        for _,cite in ipairs(cites) do
+          buffer[#buffer+1] = {cite.suppress_author and "-" or "+", "{",
+            cite.prenote or "", "}{", cite.postnote or "", "}{", cite.name, "}"}
+        end
+        return buffer
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local citation_chars
+                    = parsers.alphanumeric
+                    + S("#$%&-+<>~/_")
+
+      local citation_name
+                    = Cs(parsers.dash^-1) * parsers.at
+                    * Cs(citation_chars
+                        * (((citation_chars + parsers.internal_punctuation
+                            - parsers.comma - parsers.semicolon)
+                           * -#((parsers.internal_punctuation - parsers.comma
+                                - parsers.semicolon)^0
+                               * -(citation_chars + parsers.internal_punctuation
+                                  - parsers.comma - parsers.semicolon)))^0
+                          * citation_chars)^-1)
+
+      local citation_body_prenote
+                    = Cs((parsers.alphanumeric^1
+                         + parsers.bracketed
+                         + parsers.inticks
+                         + (parsers.anyescaped
+                           - (parsers.rbracket + parsers.blankline^2))
+                         - (parsers.spnl * parsers.dash^-1 * parsers.at))^0)
+
+      local citation_body_postnote
+                    = Cs((parsers.alphanumeric^1
+                         + parsers.bracketed
+                         + parsers.inticks
+                         + (parsers.anyescaped
+                           - (parsers.rbracket + parsers.semicolon
+                             + parsers.blankline^2))
+                         - (parsers.spnl * parsers.rbracket))^0)
+
+      local citation_body_chunk
+                    = citation_body_prenote
+                    * parsers.spnl * citation_name
+                    * (parsers.internal_punctuation - parsers.semicolon)^-1
+                    * parsers.spnl * citation_body_postnote
+
+      local citation_body
+                    = citation_body_chunk
+                    * (parsers.semicolon * parsers.spnl
+                      * citation_body_chunk)^0
+
+      local citation_headless_body_postnote
+                    = Cs((parsers.alphanumeric^1
+                         + parsers.bracketed
+                         + parsers.inticks
+                         + (parsers.anyescaped
+                           - (parsers.rbracket + parsers.at
+                             + parsers.semicolon + parsers.blankline^2))
+                         - (parsers.spnl * parsers.rbracket))^0)
+
+      local citation_headless_body
+                    = citation_headless_body_postnote
+                    * (parsers.sp * parsers.semicolon * parsers.spnl
+                      * citation_body_chunk)^0
+
+      local citations
+                    = function(text_cites, raw_cites)
+          local function normalize(str)
+              if str == "" then
+                  str = nil
+              else
+                  str = (citation_nbsps and
+                    self.parser_functions.parse_inlines_nbsp or
+                    self.parser_functions.parse_inlines)(str)
+              end
+              return str
+          end
+
+          local cites = {}
+          for i = 1,#raw_cites,4 do
+              cites[#cites+1] = {
+                  prenote = normalize(raw_cites[i]),
+                  suppress_author = raw_cites[i+1] == "-",
+                  name = writer.citation(raw_cites[i+2]),
+                  postnote = normalize(raw_cites[i+3]),
+              }
+          end
+          return writer.citations(text_cites, cites)
+      end
+
+      local TextCitations
+                    = Ct((parsers.spnl
+                    * Cc("")
+                    * citation_name
+                    * ((parsers.spnl
+                        * parsers.lbracket
+                        * citation_headless_body
+                        * parsers.rbracket) + Cc("")))^1)
+                    / function(raw_cites)
+                        return citations(true, raw_cites)
+                      end
+
+      local ParenthesizedCitations
+                    = Ct((parsers.spnl
+                    * parsers.lbracket
+                    * citation_body
+                    * parsers.rbracket)^1)
+                    / function(raw_cites)
+                        return citations(false, raw_cites)
+                      end
+
+      local Citations = TextCitations + ParenthesizedCitations
+
+      syntax.Citations = Citations
+    end
+  }
+end
+%    \end{macrocode}
+% \begin{markdown}
+%
+%#### Content Blocks
+%
+% The \luamdef{extensions.content_blocks} function implements the iA\,Writer
+% content blocks syntax extension. The `language_map` parameter specifies
+% the filename of the \acro{JSON} file that maps filename extensions to
+% programming language names.
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions.content_blocks = function(language_map)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% The \luamdef{languages_json} table maps programming language filename
+% extensions to fence infostrings. All `language_map` files located by the
+% KPathSea library are loaded into a chain of tables. \luamref{languages_json}
+% corresponds to the first table and is chained with the rest via Lua
+% metatables.
+%
+% \end{markdown}
+%  \begin{macrocode}
+  local languages_json = (function()
+    local ran_ok, kpse = pcall(require, "kpse")
+    if ran_ok then
+      kpse.set_program_name("luatex")
+%    \end{macrocode}
+% \begin{markdown}
+%
+% If the KPathSea library is unavailable, perhaps because we are using
+% LuaMeta\TeX, we will only locate the `options.`\luamref{contentBlocksLanguageMap}
+% in the current working directory:
+%
+% \end{markdown}
+%  \begin{macrocode}
+    else
+      kpse = {lookup=function(filename, options) return filename end}
+    end
+    local base, prev, curr
+    for _, filename in ipairs{kpse.lookup(language_map, { all=true })} do
+      local file = io.open(filename, "r")
+      if not file then goto continue end
+      json = file:read("*all"):gsub('("[^\n]-"):','[%1]=')
+      curr = (function()
+        local _ENV={ json=json, load=load } -- run in sandbox
+        return load("return "..json)()
+      end)()
+      if type(curr) == "table" then
+        if base == nil then
+          base = curr
+        else
+          setmetatable(prev, { __index = curr })
+        end
+        prev = curr
+      end
+      ::continue::
+    end
+    return base or {}
+  end)()
+
+  return {
+    extend_writer = function(self)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamdef{writer->contentblock} as a function that will transform an
+% input iA\,Writer content block to the output format, where `src`
+% corresponds to the \acro{uri} prefix, `suf` to the \acro{uri} extension,
+% `type` to the type of the content block (`localfile` or `onlineimage`),
+% and `tit` to the title of the content block.
+%
+% \end{markdown}
+%  \begin{macrocode}
+      function self.contentblock(src,suf,type,tit)
+        if not self.is_writing then return "" end
+        src = src.."."..suf
+        suf = suf:lower()
+        if type == "onlineimage" then
+          return {"\\markdownRendererContentBlockOnlineImage{",suf,"}",
+                                 "{",self.string(src),"}",
+                                 "{",self.uri(src),"}",
+                                 "{",self.string(tit or ""),"}"}
+        elseif languages_json[suf] then
+          return {"\\markdownRendererContentBlockCode{",suf,"}",
+                                 "{",self.string(languages_json[suf]),"}",
+                                 "{",self.string(src),"}",
+                                 "{",self.uri(src),"}",
+                                 "{",self.string(tit or ""),"}"}
+        else
+          return {"\\markdownRendererContentBlock{",suf,"}",
+                                 "{",self.string(src),"}",
+                                 "{",self.uri(src),"}",
+                                 "{",self.string(tit or ""),"}"}
+        end
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local contentblock_tail
+                    = parsers.optionaltitle
+                    * (parsers.newline + parsers.eof)
+
+      -- case insensitive online image suffix:
+      local onlineimagesuffix
+                    = (function(...)
+                        local parser = nil
+                        for _, suffix in ipairs({...}) do
+                          local pattern=nil
+                          for i=1,#suffix do
+                            local char=suffix:sub(i,i)
+                            char = S(char:lower()..char:upper())
+                            if pattern == nil then
+                              pattern = char
+                            else
+                              pattern = pattern * char
+                            end
+                          end
+                          if parser == nil then
+                            parser = pattern
+                          else
+                            parser = parser + pattern
+                          end
+                        end
+                        return parser
+                      end)("png", "jpg", "jpeg", "gif", "tif", "tiff")
+
+      -- online image url for iA Writer content blocks with mandatory suffix,
+      -- allowing nested brackets:
+      local onlineimageurl
+                    = (parsers.less
+                      * Cs((parsers.anyescaped
+                           - parsers.more
+                           - #(parsers.period
+                              * onlineimagesuffix
+                              * parsers.more
+                              * contentblock_tail))^0)
+                      * parsers.period
+                      * Cs(onlineimagesuffix)
+                      * parsers.more
+                      + (Cs((parsers.inparens
+                            + (parsers.anyescaped
+                              - parsers.spacing
+                              - parsers.rparent
+                              - #(parsers.period
+                                 * onlineimagesuffix
+                                 * contentblock_tail)))^0)
+                        * parsers.period
+                        * Cs(onlineimagesuffix))
+                      ) * Cc("onlineimage")
+
+      -- filename for iA Writer content blocks with mandatory suffix:
+      local localfilepath
+                    = parsers.slash
+                    * Cs((parsers.anyescaped
+                         - parsers.tab
+                         - parsers.newline
+                         - #(parsers.period
+                            * parsers.alphanumeric^1
+                            * contentblock_tail))^1)
+                    * parsers.period
+                    * Cs(parsers.alphanumeric^1)
+                    * Cc("localfile")
+
+      local ContentBlock
+                    = parsers.leader
+                    * (localfilepath + onlineimageurl)
+                    * contentblock_tail
+                    / writer.contentblock
+
+      syntax.ContentBlock = ContentBlock
+    end
+  }
+end
+%    \end{macrocode}
+% \begin{markdown}
+%
+%#### Definition Lists
+%
+% The \luamdef{extensions.definition_lists} function implements the definition
+% list syntax extension. If the `tight_lists` parameter is `true`, tight lists
+% will produce special right item renderers.
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions.definition_lists = function(tight_lists)
+  return {
+    extend_writer = function(self)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamdef{writer->definitionlist} as a function that will transform an
+% input definition list to the output format, where `items` is an array of
+% tables, each of the form `{ term = t, definitions = defs }`, where `t`
+% is a term and `defs` is an array of definitions. `tight` specifies,
+% whether the list is tight or not.
+%
+% \end{markdown}
+%  \begin{macrocode}
+      local function dlitem(term, defs)
+        local retVal = {"\\markdownRendererDlItem{",term,"}"}
+        for _, def in ipairs(defs) do
+          retVal[#retVal+1] = {"\\markdownRendererDlDefinitionBegin ",def,
+                               "\\markdownRendererDlDefinitionEnd "}
+        end
+        retVal[#retVal+1] = "\\markdownRendererDlItemEnd "
+        return retVal
+      end
+
+      function self.definitionlist(items,tight)
+        if not self.is_writing then return "" end
+        local buffer = {}
+        for _,item in ipairs(items) do
+          buffer[#buffer + 1] = dlitem(item.term, item.definitions)
+        end
+        if tight and tight_lists then
+          return {"\\markdownRendererDlBeginTight\n", buffer,
+            "\n\\markdownRendererDlEndTight"}
+        else
+          return {"\\markdownRendererDlBegin\n", buffer,
+            "\n\\markdownRendererDlEnd"}
+        end
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local defstartchar = S("~:")
+
+      local defstart = ( defstartchar * #parsers.spacing
+                                      * (parsers.tab + parsers.space^-3)
+                       + parsers.space * defstartchar * #parsers.spacing
+                                       * (parsers.tab + parsers.space^-2)
+                       + parsers.space * parsers.space * defstartchar
+                                       * #parsers.spacing
+                                       * (parsers.tab + parsers.space^-1)
+                       + parsers.space * parsers.space * parsers.space
+                                       * defstartchar * #parsers.spacing
+                       )
+
+      local dlchunk = Cs(parsers.line * (parsers.indentedline - parsers.blankline)^0)
+
+      local function definition_list_item(term, defs, tight)
+        return { term = self.parser_functions.parse_inlines(term),
+                 definitions = defs }
+      end
+
+      local DefinitionListItemLoose
+                    = C(parsers.line) * parsers.skipblanklines
+                    * Ct((defstart
+                         * parsers.indented_blocks(dlchunk)
+                         / self.parser_functions.parse_blocks_nested)^1)
+                    * Cc(false) / definition_list_item
+
+      local DefinitionListItemTight
+                    = C(parsers.line)
+                    * Ct((defstart * dlchunk
+                         / self.parser_functions.parse_blocks_nested)^1)
+                    * Cc(true) / definition_list_item
+
+      local DefinitionList
+                    = ( Ct(DefinitionListItemLoose^1) * Cc(false)
+                      + Ct(DefinitionListItemTight^1)
+                      * (parsers.skipblanklines
+                        * -DefinitionListItemLoose * Cc(true))
+                      ) / writer.definitionlist
+
+      syntax.DefinitionList = DefinitionList
+    end
+  }
+end
+%    \end{macrocode}
+% \begin{markdown}
+%
+%#### Fenced Code
+%
+% The \luamdef{extensions.fenced_code} function implements the commonmark
+% fenced code block syntax extension. When the `blank_before_code_fence`
+% parameter is `true`, the syntax extension requires between a paragraph and
+% the following fenced code block.
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions.fenced_code = function(blank_before_code_fence)
+  return {
+    extend_writer = function(self)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamdef{writer->codeFence} as a function that will transform an
+% input fenced code block `s` with the infostring `i` to the output
+% format.
+%
+% \end{markdown}
+%  \begin{macrocode}
+      function self.fencedCode(i, s)
+        if not self.is_writing then return "" end
+        s = string.gsub(s, '[\r\n%s]*$', '')
+        local name = util.cache(self.cacheDir, s, nil, nil, ".verbatim")
+        return {"\\markdownRendererInputFencedCode{",name,"}{",i,"}"}
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local function captures_geq_length(s,i,a,b)
+        return #a >= #b and i
+      end
+
+      local infostring   = (parsers.linechar - (parsers.backtick
+                         + parsers.space^1 * (parsers.newline + parsers.eof)))^0
+
+      local fenceindent
+      local fencehead    = function(char)
+        return             C(parsers.nonindentspace) / function(s) fenceindent = #s end
+                         * Cg(char^3, "fencelength")
+                         * parsers.optionalspace * C(infostring)
+                         * parsers.optionalspace * (parsers.newline + parsers.eof)
+      end
+
+      local fencetail    = function(char)
+        return             parsers.nonindentspace
+                         * Cmt(C(char^3) * Cb("fencelength"), captures_geq_length)
+                         * parsers.optionalspace * (parsers.newline + parsers.eof)
+                         + parsers.eof
+      end
+
+      local fencedline   = function(char)
+        return             C(parsers.line - fencetail(char))
+                         / function(s)
+                             i = 1
+                             remaining = fenceindent
+                             while true do
+                               c = s:sub(i, i)
+                               if c == " " and remaining > 0 then
+                                 remaining = remaining - 1
+                                 i = i + 1
+                               elseif c == "\t" and remaining > 3 then
+                                 remaining = remaining - 4
+                                 i = i + 1
+                               else
+                                 break
+                               end
+                             end
+                             return s:sub(i)
+                           end
+      end
+
+      local TildeFencedCode
+             = fencehead(parsers.tilde)
+             * Cs(fencedline(parsers.tilde)^0)
+             * fencetail(parsers.tilde)
+
+      local BacktickFencedCode
+             = fencehead(parsers.backtick)
+             * Cs(fencedline(parsers.backtick)^0)
+             * fencetail(parsers.backtick)
+
+      local FencedCode = (TildeFencedCode
+                           + BacktickFencedCode)
+                         / function(infostring, code)
+                             return writer.fencedCode(writer.string(infostring),
+                                                      self.expandtabs(code))
+                           end
+
+      syntax.FencedCode = FencedCode
+
+      if blank_before_code_fence then
+        fencestart = parsers.fail
+      else
+        fencestart = fencehead(parsers.backtick)
+                   + fencehead(parsers.tilde)
+      end
+
+      parsers.EndlineExceptions = parsers.EndlineExceptions + fencestart
+      syntax.EndlineExceptions = parsers.EndlineExceptions
+    end
+  }
+end
+%    \end{macrocode}
+% \begin{markdown}
+%
+%#### Footnotes
+%
+% The \luamdef{extensions.footnotes} function implements the Pandoc footnote
+% and inline footnote syntax extensions. When the `footnote` parameter is
+% `true`, the Pandoc footnote syntax extension will be enabled.  When the
+% `inline_footnotes` parameter is `true`, the Pandoc inline footnote syntax
+% extension will be enabled.
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions.footnotes = function(footnotes, inline_footnotes)
+  assert(footnotes or inline_footnotes)
+  return {
+    extend_writer = function(self)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamdef{writer->note} as a function that will transform an
+% input footnote `s` to the output format.
+%
+% \end{markdown}
+%  \begin{macrocode}
+      function self.note(s)
+        return {"\\markdownRendererFootnote{",s,"}"}
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      if footnotes then
+        local function strip_first_char(s)
+          return s:sub(2)
+        end
+
+        local RawNoteRef
+                      = #(parsers.lbracket * parsers.circumflex)
+                      * parsers.tag / strip_first_char
+
+        local rawnotes = {}
+
+        -- like indirect_link
+        local function lookup_note(ref)
+          return writer.defer_call(function()
+            local found = rawnotes[self.normalize_tag(ref)]
+            if found then
+              return writer.note(
+                self.parser_functions.parse_blocks_nested(found))
+            else
+              return {"[",
+                self.parser_functions.parse_inlines("^" .. ref), "]"}
+            end
+          end)
+        end
+
+        local function register_note(ref,rawnote)
+          rawnotes[self.normalize_tag(ref)] = rawnote
+          return ""
+        end
+
+        local NoteRef = RawNoteRef / lookup_note
+
+        local NoteBlock
+                    = parsers.leader * RawNoteRef * parsers.colon
+                    * parsers.spnl * parsers.indented_blocks(parsers.chunk)
+                    / register_note
+
+        parsers.Blank = NoteBlock + parsers.Blank
+        syntax.Blank = parsers.Blank
+
+        syntax.NoteRef = NoteRef
+      end
+      if inline_footnotes then
+        local InlineNote
+                    = parsers.circumflex
+                    * (parsers.tag / self.parser_functions.parse_inlines_no_inline_note)
+                    / writer.note
+        syntax.InlineNote = InlineNote
+      end
+    end
+  }
+end
+%    \end{macrocode}
+% \begin{markdown}
+%
+%#### Header Attributes
+%
+% The \luamdef{extensions.header_attributes} function implements a syntax
+% extension that enables the assignment of HTML attributes to headings.
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions.header_attributes = function()
+  return {
+    extend_writer = function(self)
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      parsers.AtxHeading = Cg(parsers.HeadingStart,"level")
+                         * parsers.optionalspace
+                         * (C(((parsers.linechar
+                               - ((parsers.hash^1
+                                  * parsers.optionalspace
+                                  * parsers.HeadingAttributes^-1
+                                  + parsers.HeadingAttributes)
+                                 * parsers.optionalspace
+                                 * parsers.newline))
+                              * (parsers.linechar
+                                - parsers.hash
+                                - parsers.lbrace)^0)^1)
+                             / self.parser_functions.parse_inlines)
+                         * Cg(Ct(parsers.newline
+                                + (parsers.hash^1
+                                  * parsers.optionalspace
+                                  * parsers.HeadingAttributes^-1
+                                  + parsers.HeadingAttributes)
+                                * parsers.optionalspace
+                                * parsers.newline), "attributes")
+                         * Cb("level")
+                         * Cb("attributes")
+                         / writer.heading
+
+      parsers.SetextHeading = #(parsers.line * S("=-"))
+                            * (C(((parsers.linechar
+                                  - (parsers.HeadingAttributes
+                                    * parsers.optionalspace
+                                    * parsers.newline))
+                                 * (parsers.linechar
+                                   - parsers.lbrace)^0)^1)
+                                / self.parser_functions.parse_inlines)
+                            * Cg(Ct(parsers.newline
+                                   + (parsers.HeadingAttributes
+                                     * parsers.optionalspace
+                                     * parsers.newline)), "attributes")
+                            * parsers.HeadingLevel
+                            * Cb("attributes")
+                            * parsers.optionalspace
+                            * parsers.newline
+                            / writer.heading
+
+      parsers.Heading = parsers.AtxHeading + parsers.SetextHeading
+      syntax.Heading = parsers.Heading
+    end
+  }
+end
+%    \end{macrocode}
+% \begin{markdown}
+%
+%#### YAML Metadata
+%
+% The \luamdef{extensions.jekyll_data} function implements the Pandoc
+% `yaml_metadata_block` syntax extension for entering metadata in \acro{yaml}.
+% When the `expect_jekyll_data` is `true`, then a markdown document may
+% begin directly with \acro{yaml} metadata and may contain nothing but
+% \acro{yaml} metadata
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions.jekyll_data = function(expect_jekyll_data)
+  return {
+    extend_writer = function(self)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamdef{writer->jekyllData} as a function that will transform an
+% input \acro{yaml} table `d` to the output format. The table is the value for
+% the key `p` in the parent table; if `p` is nil, then the table has no parent.
+% All scalar keys and values encountered in the table will be cast to a string
+% following \acro{yaml} serialization rules. String values will also be
+% transformed using the function `t`.
+%
+% \end{markdown}
+%  \begin{macrocode}
+      function self.jekyllData(d, t, p)
+        if not self.is_writing then return "" end
+
+        local buf = {}
+
+        local keys = {}
+        for k, _ in pairs(d) do
+          table.insert(keys, k)
+        end
+        table.sort(keys)
+
+        if not p then
+          table.insert(buf, "\\markdownRendererJekyllDataBegin")
+        end
+
+        if #d > 0 then
+            table.insert(buf, "\\markdownRendererJekyllDataSequenceBegin{")
+            table.insert(buf, self.uri(p or "null"))
+            table.insert(buf, "}{")
+            table.insert(buf, #keys)
+            table.insert(buf, "}")
+        else
+            table.insert(buf, "\\markdownRendererJekyllDataMappingBegin{")
+            table.insert(buf, self.uri(p or "null"))
+            table.insert(buf, "}{")
+            table.insert(buf, #keys)
+            table.insert(buf, "}")
+        end
+
+        for _, k in ipairs(keys) do
+          local v = d[k]
+          local typ = type(v)
+          k = tostring(k or "null")
+          if typ == "table" and next(v) ~= nil then
+            table.insert(
+              buf,
+              self.jekyllData(v, t, k)
+            )
+          else
+            k = self.uri(k)
+            v = tostring(v)
+            if typ == "boolean" then
+              table.insert(buf, "\\markdownRendererJekyllDataBoolean{")
+              table.insert(buf, k)
+              table.insert(buf, "}{")
+              table.insert(buf, v)
+              table.insert(buf, "}")
+            elseif typ == "number" then
+              table.insert(buf, "\\markdownRendererJekyllDataNumber{")
+              table.insert(buf, k)
+              table.insert(buf, "}{")
+              table.insert(buf, v)
+              table.insert(buf, "}")
+            elseif typ == "string" then
+              table.insert(buf, "\\markdownRendererJekyllDataString{")
+              table.insert(buf, k)
+              table.insert(buf, "}{")
+              table.insert(buf, t(v))
+              table.insert(buf, "}")
+            elseif typ == "table" then
+              table.insert(buf, "\\markdownRendererJekyllDataEmpty{")
+              table.insert(buf, k)
+              table.insert(buf, "}")
+            else
+              error(format("Unexpected type %s for value of " ..
+                           "YAML key %s", typ, k))
+            end
+          end
+        end
+
+        if #d > 0 then
+          table.insert(buf, "\\markdownRendererJekyllDataSequenceEnd")
+        else
+          table.insert(buf, "\\markdownRendererJekyllDataMappingEnd")
+        end
+
+        if not p then
+          table.insert(buf, "\\markdownRendererJekyllDataEnd")
+        end
+
+        return buf
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local JekyllData
+                    = Cmt( C((parsers.line - P("---") - P("..."))^0)
+                         , function(s, i, text)
+                             local data
+                             local ran_ok, error = pcall(function()
+                               local tinyyaml = require("markdown-tinyyaml")
+                               data = tinyyaml.parse(text, {timestamps=false})
+                             end)
+                             if ran_ok and data ~= nil then
+                               return true, writer.jekyllData(data, function(s)
+                                 return self.parser_functions.parse_blocks_nested(s)
+                               end, nil)
+                             else
+                               return false
+                             end
+                           end
+                         )
+
+      local UnexpectedJekyllData
+                    = P("---")
+                    * parsers.blankline / 0
+                    * #(-parsers.blankline)  -- if followed by blank, it's an hrule
+                    * JekyllData
+                    * (P("---") + P("..."))
+
+      local ExpectedJekyllData
+                    = ( P("---")
+                      * parsers.blankline / 0
+                      * #(-parsers.blankline)  -- if followed by blank, it's an hrule
+                      )^-1
+                    * JekyllData
+                    * (P("---") + P("..."))^-1
+
+      syntax.UnexpectedJekyllData = UnexpectedJekyllData
+      if expect_jekyll_data then
+        syntax.ExpectedJekyllData = ExpectedJekyllData
+      end
+    end
+  }
+end
+%    \end{macrocode}
+% \begin{markdown}
+%
+%#### Pipe Tables
+%
+% The \luamdef{extensions.pipe_table} function implements the \acro{PHP}
+% Markdown table syntax extension (affectionately known as pipe tables). When
+% the parameter `table_captions` is `true`, the function also implements the
+% Pandoc `table_captions` syntax extension for table captions.
+%
+% \end{markdown}
+%  \begin{macrocode}
+M.extensions.pipe_tables = function(table_captions)
+
+  local function make_pipe_table_rectangular(rows)
+    local num_columns = #rows[2]
+    local rectangular_rows = {}
+    for i = 1, #rows do
+      local row = rows[i]
+      local rectangular_row = {}
+      for j = 1, num_columns do
+        rectangular_row[j] = row[j] or ""
+      end
+      table.insert(rectangular_rows, rectangular_row)
+    end
+    return rectangular_rows
+  end
+
+  local function pipe_table_row(allow_empty_first_column
+                               , nonempty_column
+                               , column_separator
+                               , column)
+    local row_beginning
+    if allow_empty_first_column then
+      row_beginning = -- empty first column
+                      #(parsers.spacechar^4
+                       * column_separator)
+                    * parsers.optionalspace
+                    * column
+                    * parsers.optionalspace
+                    -- non-empty first column
+                    + parsers.nonindentspace
+                    * nonempty_column^-1
+                    * parsers.optionalspace
+    else
+      row_beginning = parsers.nonindentspace
+                    * nonempty_column^-1
+                    * parsers.optionalspace
+    end
+
+    return Ct(row_beginning
+             * (-- single column with no leading pipes
+                #(column_separator
+                 * parsers.optionalspace
+                 * parsers.newline)
+               * column_separator
+               * parsers.optionalspace
+               -- single column with leading pipes or
+               -- more than a single column
+               + (column_separator
+                 * parsers.optionalspace
+                 * column
+                 * parsers.optionalspace)^1
+               * (column_separator
+                 * parsers.optionalspace)^-1))
+  end
+
+  return {
+    extend_writer = function(self)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamdef{writer->table} as a function that will transform an input
+% table to the output format, where `rows` is a sequence of columns and a
+% column is a sequence of cell texts.
+%
+% \end{markdown}
+%  \begin{macrocode}
+      function self.table(rows, caption)
+        if not self.is_writing then return "" end
+        local buffer = {"\\markdownRendererTable{",
+          caption or "", "}{", #rows - 1, "}{", #rows[1], "}"}
+        local temp = rows[2] -- put alignments on the first row
+        rows[2] = rows[1]
+        rows[1] = temp
+        for i, row in ipairs(rows) do
+          table.insert(buffer, "{")
+          for _, column in ipairs(row) do
+            if i > 1 then -- do not use braces for alignments
+              table.insert(buffer, "{")
+            end
+            table.insert(buffer, column)
+            if i > 1 then
+              table.insert(buffer, "}")
+            end
+          end
+          table.insert(buffer, "}")
+        end
+        return buffer
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local table_hline_separator = parsers.pipe + parsers.plus
+
+      local table_hline_column = (parsers.dash
+                                 - #(parsers.dash
+                                    * (parsers.spacechar
+                                      + table_hline_separator
+                                      + parsers.newline)))^1
+                               * (parsers.colon * Cc("r")
+                                 + parsers.dash * Cc("d"))
+                               + parsers.colon
+                               * (parsers.dash
+                                 - #(parsers.dash
+                                    * (parsers.spacechar
+                                      + table_hline_separator
+                                      + parsers.newline)))^1
+                               * (parsers.colon * Cc("c")
+                                 + parsers.dash * Cc("l"))
+
+      local table_hline = pipe_table_row(false
+                                        , table_hline_column
+                                        , table_hline_separator
+                                        , table_hline_column)
+
+      local table_caption_beginning = parsers.skipblanklines
+                                    * parsers.nonindentspace
+                                    * (P("Table")^-1 * parsers.colon)
+                                    * parsers.optionalspace
+
+      local table_row = pipe_table_row(true
+                                      , (C((parsers.linechar - parsers.pipe)^1)
+                                        / self.parser_functions.parse_inlines)
+                                      , parsers.pipe
+                                      , (C((parsers.linechar - parsers.pipe)^0)
+                                        / self.parser_functions.parse_inlines))
+
+      local table_caption
+      if table_captions then
+        table_caption = #table_caption_beginning
+                      * table_caption_beginning
+                      * Ct(parsers.IndentedInline^1)
+                      * parsers.newline
+      else
+        table_caption = parsers.fail
+      end
+
+      local PipeTable = Ct(table_row * parsers.newline
+                        * table_hline
+                        * (parsers.newline * table_row)^0)
+                      / make_pipe_table_rectangular
+                      * table_caption^-1
+                      / writer.table
+
+      syntax.PipeTable = PipeTable
+    end
+  }
+end
+%    \end{macrocode}
+% \begin{markdown}
+%
 %### Conversion from Markdown to Plain \TeX{}
+%
 % The \luamref{new} method returns the \luamref{reader->convert} function of a reader
 % object associated with the Lua interface options (see Section
 % <#sec:luaoptions>) `options` and with a writer object associated with
@@ -21015,8 +21415,75 @@
 % \end{markdown}
 %  \begin{macrocode}
 function M.new(options)
+%    \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Make the `options` table inherit from the \luamref{defaultOptions} table.
+%
+% \end{markdown}
+%  \begin{macrocode}
+  options = options or {}
+  setmetatable(options, { __index = function (_, key)
+    return defaultOptions[key] end })
+% \par
+% \begin{markdown}
+%
+% Apply syntax extensions based on `options`.
+%
+% \end{markdown}
+%  \begin{macrocode}
+  extensions = {}
+
+  if options.citations then
+    citations_extension = M.extensions.citations(options.citationNbsps)
+    table.insert(extensions, citations_extension)
+  end
+
+  if options.contentBlocks then
+    content_blocks_extension = M.extensions.content_blocks(
+      options.contentBlocksLanguageMap)
+    table.insert(extensions, content_blocks_extension)
+  end
+
+  if options.definitionLists then
+    definition_lists_extension = M.extensions.definition_lists(
+      options.tightLists)
+    table.insert(extensions, definition_lists_extension)
+  end
+
+  if options.fencedCode then
+    fenced_code_extension = M.extensions.fenced_code(
+      options.blankBeforeCodeFence)
+    table.insert(extensions, fenced_code_extension)
+  end
+
+  if options.footnotes or options.inlineFootnotes then
+    footnotes_extension = M.extensions.footnotes(
+      options.footnotes, options.inlineFootnotes)
+    table.insert(extensions, footnotes_extension)
+  end
+
+  if options.headerAttributes then
+    header_attributes_extension = M.extensions.header_attributes()
+    table.insert(extensions, header_attributes_extension)
+  end
+
+  if options.jekyllData then
+    jekyll_data_extension = M.extensions.jekyll_data(
+      options.expectJekyllData)
+    table.insert(extensions, jekyll_data_extension)
+  end
+
+  if options.pipeTables then
+    pipe_tables_extension = M.extensions.pipe_tables(
+      options.tableCaptions)
+    table.insert(extensions, pipe_tables_extension)
+  end
+
   local writer = M.writer.new(options)
-  local reader = M.reader.new(writer, options)
+  local reader = M.reader.new(writer, options, extensions)
+
   return reader.convert
 end
 
@@ -21126,23 +21593,6 @@
 % \par
 % \begin{markdown}
 %
-%### Finalizing and Freezing the Cache
-%
-% When the \mref{markdownOptionFinalizeCache} option is enabled, then the
-% \mdef{markdownFrozenCacheCounter} counter is used to enumerate the markdown
-% documents using the Lua interface \Opt{frozenCacheCounter} option.
-%
-% When the \mref{markdownOptionFrozenCache} option is enabled, then the
-% \mref{markdownFrozenCacheCounter} counter is used to render markdown documents
-% from the frozen cache without invoking Lua.
-%
-% \end{markdown}
-%  \begin{macrocode}
-\newcount\markdownFrozenCacheCounter
-%    \end{macrocode}
-% \par
-% \begin{markdown}
-%
 %### Token Renderer Prototypes {#textokenrendererprototypes}
 %
 % The following definitions should be considered placeholder.
@@ -21279,7 +21729,7 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-      \tl_if_eq:NNTF
+      \str_if_eq:NNTF
         \l_tmpa_tl
         \c_@@_jekyll_data_sequence_tl
         {
@@ -21466,10 +21916,6 @@
 %  \begin{macrocode}
 \ExplSyntaxOn
 \tl_new:N \g_@@_formatted_lua_options_tl
-\tl_const:Nn \c_@@_lua_option_type_boolean { boolean }
-\tl_const:Nn \c_@@_lua_option_type_counter { counter }
-\tl_const:Nn \c_@@_lua_option_type_number  { number  }
-\tl_const:Nn \c_@@_lua_option_type_string  { string  }
 \cs_new:Nn \@@_format_lua_options:
   {
     \tl_gclear:N
@@ -21480,111 +21926,38 @@
   }
 \cs_new:Nn \@@_format_lua_option:n
   {
-    \@@_typecheck_lua_option:n { #1 }
-    \tl_set:Nn
-      \l_tmpb_tl
-%     TODO: Replace with \str_uppercase:n in TeX Live 2020.
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
-      \l_tmpa_tl
-      {
-        markdownOption
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
-      }
-    \prop_get:NnN
-      \g_@@_lua_option_types_prop
+    \@@_typecheck_option:n
       { #1 }
-      \l_tmpb_tl
-    \cs_if_free:cTF
-      { \l_tmpa_tl }
-      { }
-      {
-        \tl_case:NnF
-          \l_tmpb_tl
-          {
-            \c_@@_lua_option_type_string
-              {
-                \tl_gput_right:Nx
-                  \g_@@_formatted_lua_options_tl
-                  { #1~=~"     \cs:w \l_tmpa_tl \cs_end: ",~ }
-              }
-            \c_@@_lua_option_type_counter
-              {
-                \tl_gput_right:Nx
-                  \g_@@_formatted_lua_options_tl
-                  { #1~=~ \the \cs:w \l_tmpa_tl \cs_end: ,~ }
-              }
-          }
-          {
-            \tl_gput_right:Nx
-              \g_@@_formatted_lua_options_tl
-              {     #1~=~      \cs:w \l_tmpa_tl \cs_end: ,~ }
-          }
-      }
-  }
-\msg_new:nnn
-  { markdown }
-  { undefined-lua-option }
-  {
-    Lua~option~#1~is~undefined.
-  }
-\msg_new:nnn
-  { markdown }
-  { failed-typecheck-for-boolean-lua-option }
-  {
-    Lua~option~#1~has~value~#2,~
-    but~a~boolean~(true~or~false)~was~expected.
-  }
-\cs_new:Nn \@@_typecheck_lua_option:n
-  {
-    \tl_set:Nn
-      \l_tmpb_tl
-%     TODO: Replace with \str_uppercase:n in TeX Live 2020.
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
+    \@@_get_option_type:nN
+      { #1 }
       \l_tmpa_tl
+    \bool_if:nTF
       {
-        markdownOption
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
+        \str_if_eq_p:VV
+          \l_tmpa_tl
+          \c_@@_option_type_boolean_tl ||
+        \str_if_eq_p:VV
+          \l_tmpa_tl
+          \c_@@_option_type_number_tl ||
+        \str_if_eq_p:VV
+          \l_tmpa_tl
+          \c_@@_option_type_counter_tl
       }
-    \prop_get:NnNTF
-      \g_@@_lua_option_types_prop
-      { #1 }
-      \l_tmpb_tl
       {
-        \cs_if_free:cTF
-          { \l_tmpa_tl }
-          { }
-          {
-            \tl_case:Nn
-              \l_tmpb_tl
-              {
-                \c_@@_lua_option_type_boolean
-                  {
-                    \tl_case:cnF
-                      { \l_tmpa_tl }
-                      {
-                        \c_@@_lua_option_value_true  { }
-                        \c_@@_lua_option_value_false { }
-                      }
-                      {
-                        \msg_error:nnxx
-                          { markdown }
-                          { failed-typecheck-for-boolean-lua-option }
-                          { #1 }
-                          { \l_tmpa_tl }
-                      }
-                  }
-              }
-          }
+        \@@_get_option_value:nN
+          { #1 }
+          \l_tmpa_tl
+        \tl_gput_right:Nx
+          \g_@@_formatted_lua_options_tl
+          { #1~=~  \l_tmpa_tl   ,~ }
       }
       {
-        \msg_error:nnn
-          { markdown }
-          { undefined-lua-option }
+        \@@_get_option_value:nN
           { #1 }
+          \l_tmpa_tl
+        \tl_gput_right:Nx
+          \g_@@_formatted_lua_options_tl
+          { #1~=~ " \l_tmpa_tl " ,~ }
       }
   }
 \let\markdownPrepareLuaOptions=\@@_format_lua_options:
@@ -21636,40 +22009,38 @@
 % \end{markdown}
 %  \begin{macrocode}
 \ExplSyntaxOn
-\tl_const:Nn \c_@@_lua_option_value_true  { true  }
-\tl_const:Nn \c_@@_lua_option_value_false { false }
-\cs_new:Nn \@@_if_option:nTF
+\cs_new:Nn
+  \@@_if_option:nTF
   {
-    \tl_set:Nn
-      \l_tmpb_tl
-%     TODO: Replace with \str_uppercase:n in TeX Live 2020.
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
+    \@@_get_option_type:nN
+      { #1 }
       \l_tmpa_tl
+    \str_if_eq:NNF
+      \l_tmpa_tl
+      \c_@@_option_type_boolean_tl
       {
-        markdownOption
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
-      }
-    \cs_if_free:cTF
-      { \l_tmpa_tl }
-      {
-        \prop_get:NnN
-          \g_@@_default_lua_options_prop
+        \msg_error:nnxx
+          { @@ }
+          { expected-boolean-option }
           { #1 }
-          \l_tmpb_tl
+          { \l_tmpa_tl }
       }
-      {
-        \tl_set:Nf
-          \l_tmpb_tl
-          { \cs:w \l_tmpa_tl \cs_end: }
-      }
-    \tl_if_eq:NNTF
-      \l_tmpb_tl
-      \c_@@_lua_option_value_true
+    \@@_get_option_value:nN
+      { #1 }
+      \l_tmpa_tl
+    \str_if_eq:NNTF
+      \l_tmpa_tl
+      \c_@@_option_value_true_tl
       { #2 }
       { #3 }
   }
+\msg_new:nnn
+  { @@ }
+  { expected-boolean-option }
+  {
+    Option~#1~has~type~#2,~
+    but~a~boolean~was~expected.
+  }
 \let\markdownIfOption=\@@_if_option:nTF
 \ExplSyntaxOff
 %    \end{macrocode}
@@ -21857,6 +22228,26 @@
 % \par
 % \begin{markdown}
 %
+% The following two sections of the implementation have been deprecated and
+% will be removed in Markdown 3.0.0. The code that corresponds to
+% \mref{markdownMode} value of `3` will be the only implementation.
+%
+% \end{markdown}
+%  \begin{macrocode}
+\ExplSyntaxOn
+\int_compare:nT
+  { \markdownMode = 3 }
+  {
+    \markdownInfo{Using~mode~3:~The~lt3luabridge~package}
+    \file_input:n { lt3luabridge.tex }
+    \cs_new:Npn
+      \markdownLuaExecute
+      { \luabridgeExecute }
+  }
+\ExplSyntaxOff
+%    \end{macrocode}
+% \begin{markdown}
+%
 %### Lua Shell Escape Bridge {#luabridge}
 %
 % The following \TeX{} code is intended for \TeX{} engines that do not provide
@@ -21875,9 +22266,11 @@
 %  \begin{macrocode}
 \ifnum\markdownMode<2\relax
 \ifnum\markdownMode=0\relax
-  \markdownInfo{Using mode 0: Shell escape via write18}%
+  \markdownWarning{Using mode 0: Shell escape via write18
+                   (deprecated, to be removed in Markdown 3.0.0)}%
 \else
-  \markdownInfo{Using mode 1: Shell escape via os.execute}%
+  \markdownWarning{Using mode 1: Shell escape via os.execute
+                   (deprecated, to be removed in Markdown 3.0.0)}%
 \fi
 %    \end{macrocode}
 % \par
@@ -22037,8 +22430,10 @@
 %
 % \end{markdown}
 %  \begin{macrocode}
-\else
-\markdownInfo{Using mode 2: Direct Lua access}%
+\fi
+\ifnum\markdownMode=2\relax
+  \markdownWarning{Using mode 2: Direct Lua access
+                   (deprecated, to be removed in Markdown 3.0.0)}%
 %    \end{macrocode}
 % \par
 % \begin{markdown}
@@ -22109,19 +22504,19 @@
 % \begin{markdown}
 % If we are reading from the frozen cache, input it, expand the corresponding
 % `\markdownFrozenCache`\meta{number} macro, and increment
-% \mref{markdownFrozenCacheCounter}.
+% \Opt{frozenCacheCounter}.
 % \end{markdown}
 %  \begin{macrocode}
     |markdownIfOption{frozenCache}{%
-      |ifnum|markdownFrozenCacheCounter=0|relax
+      |ifnum|markdownOptionFrozenCacheCounter=0|relax
         |markdownInfo{Reading frozen cache from
           "|markdownOptionFrozenCacheFileName"}%
         |input|markdownOptionFrozenCacheFileName|relax
       |fi
       |markdownInfo{Including markdown document number
-        "|the|markdownFrozenCacheCounter" from frozen cache}%
-      |csname markdownFrozenCache|the|markdownFrozenCacheCounter|endcsname
-      |global|advance|markdownFrozenCacheCounter by 1|relax
+        "|the|markdownOptionFrozenCacheCounter" from frozen cache}%
+      |csname markdownFrozenCache|the|markdownOptionFrozenCacheCounter|endcsname
+      |global|advance|markdownOptionFrozenCacheCounter by 1|relax
     }{%
       |markdownInfo{Including markdown document "#1"}%
 %    \end{macrocode}
@@ -22150,12 +22545,11 @@
         print(convert(input:gsub("\r\n?", "\n") .. "\n"))}%
 %    \end{macrocode}
 % \begin{markdown}
-% If we are finalizing the frozen cache, increment
-% \mref{markdownFrozenCacheCounter}.
+% If we are finalizing the frozen cache, increment \Opt{frozenCacheCounter}.
 % \end{markdown}
 %  \begin{macrocode}
       |markdownIfOption{finalizeCache}{%
-        |global|advance|markdownFrozenCacheCounter by 1|relax
+        |global|advance|markdownOptionFrozenCacheCounter by 1|relax
       }%
     }%
     |endgroup
@@ -22177,16 +22571,8 @@
 % format~[@latex17, Section 9]. As a consequence, we can directly reuse the
 % existing plain \TeX{} implementation.
 %
-% The \LaTeX{} implementation redefines the plain \TeX{} logging macros (see
-% Section <#sec:texinterfacelogging>) to use the \LaTeX{} \mref{PackageInfo},
-% \mref{PackageWarning}, and \mref{PackageError} macros.
-%
 % \end{markdown}
 %  \begin{macrocode}
-\newcommand\markdownInfo[1]{\PackageInfo{markdown}{#1}}%
-\newcommand\markdownWarning[1]{\PackageWarning{markdown}{#1}}%
-\newcommand\markdownError[2]{\PackageError{markdown}{#1}{#2.}}%
-\input markdown/markdown
 \def\markdownVersionSpace{ }%
 \ProvidesPackage{markdown}[\markdownLastModified\markdownVersionSpace v%
   \markdownVersion\markdownVersionSpace markdown renderer]%
@@ -22579,7 +22965,7 @@
 % it will take effect.
 % \end{markdown}
 %  \begin{macrocode}
-\ifmarkdownLaTeXPlain\else
+\markdownIfOption{plain}{\iffalse}{\iftrue}
 %    \end{macrocode}
 % \par
 % \begin{markdown}%
@@ -23182,16 +23568,34 @@
 % reference. Otherwise, we assume that it is an absolute URL.
 % \end{markdown}
 %  \begin{macrocode}
+  \tl_set:Nn
+    \l_tmpa_tl
+    { #2 }
+  \tl_trim_spaces:N
+    \l_tmpa_tl
   \tl_set:Nx
-    \l_tmpa_tl
-    { \str_range:nnn { #2 } { 1 } { 1 } }
+    \l_tmpb_tl
+    {
+      \tl_range:Nnn
+        \l_tmpa_tl
+        { 1 }
+        { 1 }
+    }
   \str_if_eq:NNTF
-    \l_tmpa_tl
+    \l_tmpb_tl
     \c_hash_str
     {
-      \exp_args:No
+      \tl_set:Nx
+        \l_tmpb_tl
+        {
+          \tl_range:Nnn
+            \l_tmpa_tl
+            { 2 }
+            { -1 }
+        }
+      \exp_args:NV
         \ref
-        { \str_range:nnn { #2 } { 2 } { -1 } }
+        \l_tmpb_tl
     }{
       \url { #2 }
     }

Modified: trunk/Master/texmf-dist/tex/context/third/markdown/t-markdown.tex
===================================================================
--- trunk/Master/texmf-dist/tex/context/third/markdown/t-markdown.tex	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/tex/context/third/markdown/t-markdown.tex	2022-06-29 20:10:26 UTC (rev 63757)
@@ -6,18 +6,15 @@
 %%
 %% markdown.dtx  (with options: `context')
 %% 
-%% Copyright (C) 2016-2021 Vít Novotný
+%% Copyright (C) 2016-2022 Vít Novotný
 %% 
 %% This work may be distributed and/or modified under the
-%% conditions of the LaTeX Project Public License, either version 1.3
+%% conditions of the LaTeX Project Public License, either version 1.3c
 %% of this license or (at your option) any later version.
 %% The latest version of this license is in
 %% 
 %%    http://www.latex-project.org/lppl.txt
 %% 
-%% and version 1.3 or later is part of all distributions of LaTeX
-%% version 2005/12/01 or later.
-%% 
 %% This work has the LPPL maintenance status `maintained'.
 %% The Current Maintainer of this work is Vít Novotný.
 %% 

Modified: trunk/Master/texmf-dist/tex/generic/markdown/markdown.tex
===================================================================
--- trunk/Master/texmf-dist/tex/generic/markdown/markdown.tex	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/tex/generic/markdown/markdown.tex	2022-06-29 20:10:26 UTC (rev 63757)
@@ -6,18 +6,15 @@
 %%
 %% markdown.dtx  (with options: `tex')
 %% 
-%% Copyright (C) 2016-2021 Vít Novotný
+%% Copyright (C) 2016-2022 Vít Novotný
 %% 
 %% This work may be distributed and/or modified under the
-%% conditions of the LaTeX Project Public License, either version 1.3
+%% conditions of the LaTeX Project Public License, either version 1.3c
 %% of this license or (at your option) any later version.
 %% The latest version of this license is in
 %% 
 %%    http://www.latex-project.org/lppl.txt
 %% 
-%% and version 1.3 or later is part of all distributions of LaTeX
-%% version 2005/12/01 or later.
-%% 
 %% This work has the LPPL maintenance status `maintained'.
 %% The Current Maintainer of this work is Vít Novotný.
 %% 
@@ -52,471 +49,536 @@
 \seq_new:N \g__markdown_lua_options_seq
 \prop_new:N \g__markdown_lua_option_types_prop
 \prop_new:N \g__markdown_default_lua_options_prop
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\seq_new:N \g__markdown_option_layers_seq
+\tl_const:Nn \c__markdown_option_layer_lua_tl { lua }
+\seq_put_right:NV \g__markdown_option_layers_seq \c__markdown_option_layer_lua_tl
+\cs_new:Nn
+  \__markdown_add_lua_option:nnn
+  {
+    \__markdown_add_option:Vnnn
+      \c__markdown_option_layer_lua_tl
+      { #1 }
+      { #2 }
+      { #3 }
+  }
+\cs_new:Nn
+  \__markdown_add_option:nnnn
+  {
+    \seq_put_right:cn
+      { g__markdown_ #1 _options_seq }
+      { #2 }
+    \prop_put:cnn
+      { g__markdown_ #1 _option_types_prop }
+      { #2 }
+      { #3 }
+    \prop_put:cnn
+      { g__markdown_default_ #1 _options_prop }
+      { #2 }
+      { #4 }
+    \__markdown_typecheck_option:n
+      { #2 }
+  }
+\cs_generate_variant:Nn
+  \__markdown_add_option:nnnn
+  { Vnnn }
+\tl_const:Nn \c__markdown_option_value_true_tl  { true  }
+\tl_const:Nn \c__markdown_option_value_false_tl { false }
+\cs_new:Nn \__markdown_typecheck_option:n
+  {
+    \__markdown_get_option_type:nN
+      { #1 }
+      \l_tmpa_tl
+    \str_case_e:Vn
+      \l_tmpa_tl
+      {
+        { \c__markdown_option_type_boolean_tl }
+          {
+            \__markdown_get_option_value:nN
+              { #1 }
+              \l_tmpa_tl
+            \bool_if:nF
+              {
+                \str_if_eq_p:VV
+                  \l_tmpa_tl
+                  \c__markdown_option_value_true_tl ||
+                \str_if_eq_p:VV
+                  \l_tmpa_tl
+                  \c__markdown_option_value_false_tl
+              }
+              {
+                \msg_error:nnnV
+                  { __markdown }
+                  { failed-typecheck-for-boolean-option }
+                  { #1 }
+                  \l_tmpa_tl
+              }
+          }
+      }
+  }
+\msg_new:nnn
+  { __markdown }
+  { failed-typecheck-for-boolean-option }
+  {
+    Option~#1~has~value~#2,~
+    but~a~boolean~(true~or~false)~was~expected.
+  }
+\cs_generate_variant:Nn
+  \str_case_e:nn
+  { Vn }
+\cs_generate_variant:Nn
+  \msg_error:nnnn
+  { nnnV }
+\seq_new:N \g__markdown_option_types_seq
+\tl_const:Nn \c__markdown_option_type_counter_tl { counter }
+\seq_put_right:NV \g__markdown_option_types_seq \c__markdown_option_type_counter_tl
+\tl_const:Nn \c__markdown_option_type_boolean_tl { boolean }
+\seq_put_right:NV \g__markdown_option_types_seq \c__markdown_option_type_boolean_tl
+\tl_const:Nn \c__markdown_option_type_number_tl  { number  }
+\seq_put_right:NV \g__markdown_option_types_seq \c__markdown_option_type_number_tl
+\tl_const:Nn \c__markdown_option_type_path_tl    { path    }
+\seq_put_right:NV \g__markdown_option_types_seq \c__markdown_option_type_path_tl
+\tl_const:Nn \c__markdown_option_type_slice_tl   { slice   }
+\seq_put_right:NV \g__markdown_option_types_seq \c__markdown_option_type_slice_tl
+\tl_const:Nn \c__markdown_option_type_string_tl  { string  }
+\seq_put_right:NV \g__markdown_option_types_seq \c__markdown_option_type_string_tl
+\cs_new:Nn
+  \__markdown_get_option_type:nN
+  {
+    \bool_set_false:N
+      \l_tmpa_bool
+    \seq_map_inline:Nn
+      \g__markdown_option_layers_seq
+      {
+        \prop_get:cnNT
+          { g__markdown_ ##1 _option_types_prop }
+          { #1 }
+          \l_tmpa_tl
+          {
+            \bool_set_true:N
+              \l_tmpa_bool
+            \seq_map_break:
+          }
+      }
+    \bool_if:nF
+      \l_tmpa_bool
+      {
+        \msg_error:nnn
+          { __markdown }
+          { undefined-option }
+          { #1 }
+      }
+    \seq_if_in:NVF
+      \g__markdown_option_types_seq
+      \l_tmpa_tl
+      {
+        \msg_error:nnnV
+          { __markdown }
+          { unknown-option-type }
+          { #1 }
+          \l_tmpa_tl
+      }
+    \tl_set_eq:NN
+      #2
+      \l_tmpa_tl
+  }
+\msg_new:nnn
+  { __markdown }
+  { unknown-option-type }
+  {
+    Option~#1~has~unknown~type~#2.
+  }
+\msg_new:nnn
+  { __markdown }
+  { undefined-option }
+  {
+    Option~#1~is~undefined.
+  }
+\cs_new:Nn
+  \__markdown_get_default_option_value:nN
+  {
+    \bool_set_false:N
+      \l_tmpa_bool
+    \seq_map_inline:Nn
+      \g__markdown_option_layers_seq
+      {
+        \prop_get:cnNT
+          { g__markdown_default_ ##1 _options_prop }
+          { #1 }
+          #2
+          {
+            \bool_set_true:N
+              \l_tmpa_bool
+            \seq_map_break:
+          }
+      }
+    \bool_if:nF
+      \l_tmpa_bool
+      {
+        \msg_error:nnn
+          { __markdown }
+          { undefined-option }
+          { #1 }
+      }
+  }
+\cs_new:Nn
+  \__markdown_get_option_value:nN
+  {
+    \__markdown_option_tl_to_csname:nN
+      { #1 }
+      \l_tmpa_tl
+    \cs_if_free:cTF
+      { \l_tmpa_tl }
+      {
+        \__markdown_get_default_option_value:nN
+          { #1 }
+          #2
+      }
+      {
+        \__markdown_get_option_type:nN
+          { #1 }
+          \l_tmpa_tl
+        \str_if_eq:NNTF
+          \c__markdown_option_type_counter_tl
+          \l_tmpa_tl
+          {
+            \__markdown_option_tl_to_csname:nN
+              { #1 }
+              \l_tmpa_tl
+            \tl_set:Nx
+              #2
+              { \the \cs:w \l_tmpa_tl \cs_end: }
+          }
+          {
+            \__markdown_option_tl_to_csname:nN
+              { #1 }
+              \l_tmpa_tl
+            \tl_set:Nv
+              #2
+              { \l_tmpa_tl }
+          }
+      }
+  }
+\cs_new:Nn \__markdown_option_tl_to_csname:nN
+  {
+    \tl_set:Nn
+      \l_tmpa_tl
+      { \str_upper_case:n { #1 } }
+    \tl_set:Nx
+      #2
+      {
+        markdownOption
+        \tl_head:f { \l_tmpa_tl }
+        \tl_tail:n { #1 }
+      }
+  }
+\__markdown_add_lua_option:nnn
   { cacheDir }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { cacheDir }
-  { string }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { cacheDir }
-  { . }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+  { path }
+  { \markdownOptionOutputDir / _markdown_\jobname }
+\__markdown_add_lua_option:nnn
   { frozenCacheFileName }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { frozenCacheFileName }
-  { string }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { frozenCacheFileName }
-  { frozenCache.tex }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+  { path }
+  { \markdownOptionCacheDir / frozenCache.tex }
+\__markdown_add_lua_option:nnn
   { blankBeforeBlockquote }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { blankBeforeBlockquote }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { blankBeforeBlockquote }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { blankBeforeCodeFence }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { blankBeforeCodeFence }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { blankBeforeCodeFence }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { blankBeforeHeading }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { blankBeforeHeading }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { blankBeforeHeading }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { breakableBlockquotes }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { breakableBlockquotes }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { breakableBlockquotes }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { citationNbsps }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { citationNbsps }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { citationNbsps }
   { true }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { citations }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { citations }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { citations }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { codeSpans }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { codeSpans }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { codeSpans }
   { true }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { contentBlocks }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { contentBlocks }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { contentBlocks }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { contentBlocksLanguageMap }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { contentBlocksLanguageMap }
-  { string }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { contentBlocksLanguageMap }
+  { path }
   { markdown-languages.json }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { definitionLists }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { definitionLists }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { definitionLists }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { eagerCache }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { eagerCache }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { eagerCache }
   { true }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { expectJekyllData }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { expectJekyllData }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { expectJekyllData }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { fencedCode }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { fencedCode }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { fencedCode }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { finalizeCache }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { finalizeCache }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { finalizeCache }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { footnotes }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { footnotes }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { footnotes }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { frozenCacheCounter }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { frozenCacheCounter }
   { counter }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { frozenCacheCounter }
   { 0 }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { hardLineBreaks }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { hardLineBreaks }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { hardLineBreaks }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { hashEnumerators }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { hashEnumerators }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { hashEnumerators }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { headerAttributes }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { headerAttributes }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { headerAttributes }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { html }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { html }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { html }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { hybrid }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { hybrid }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { hybrid }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { inlineFootnotes }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { inlineFootnotes }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { inlineFootnotes }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { jekyllData }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { jekyllData }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { jekyllData }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { pipeTables }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { pipeTables }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { pipeTables }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { preserveTabs }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { preserveTabs }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { preserveTabs }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { relativeReferences }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { relativeReferences }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { relativeReferences }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { shiftHeadings }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { shiftHeadings }
   { number }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { shiftHeadings }
   { 0 }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { slice }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
   { slice }
-  { string }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { slice }
   { ^~$ }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { smartEllipses }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { smartEllipses }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { smartEllipses }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { startNumber }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { startNumber }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { startNumber }
   { true }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { stripIndent }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { stripIndent }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { stripIndent }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { tableCaptions }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { tableCaptions }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { tableCaptions }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { taskLists }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { taskLists }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { taskLists }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { texComments }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { texComments }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { texComments }
   { false }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { tightLists }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { tightLists }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { tightLists }
   { true }
-\seq_put_right:Nn
-  \g__markdown_lua_options_seq
+\__markdown_add_lua_option:nnn
   { underscores }
-\prop_put:Nnn
-  \g__markdown_lua_option_types_prop
-  { underscores }
   { boolean }
-\prop_put:Nnn
-  \g__markdown_default_lua_options_prop
-  { underscores }
   { true }
 \ExplSyntaxOff
-\def\markdownLastModified{2022/05/31}%
-\def\markdownVersion{2.15.2-0-gb238dbc}%
+\def\markdownLastModified{2022/06/27}%
+\def\markdownVersion{2.15.3-0-g7c8e03d}%
 \let\markdownBegin\relax
 \let\markdownEnd\relax
 \let\markdownInput\relax
-\let\markdownOptionFinalizeCache\undefined
-\def\markdownOptionHelperScriptFileName{\jobname.markdown.lua}%
-\def\markdownOptionInputTempFileName{\jobname.markdown.in}%
-\def\markdownOptionOutputTempFileName{\jobname.markdown.out}%
-\def\markdownOptionErrorTempFileName{\jobname.markdown.err}%
-\def\markdownOptionOutputDir{.}%
-\def\markdownOptionCacheDir{\markdownOptionOutputDir/_markdown_\jobname}%
-\def\markdownOptionFrozenCacheFileName{\markdownOptionCacheDir/frozenCache.tex}
-\let\markdownOptionBlankBeforeBlockquote\undefined
-\let\markdownOptionBlankBeforeCodeFence\undefined
-\let\markdownOptionBlankBeforeHeading\undefined
-\let\markdownOptionBreakableBlockquotes\undefined
-\let\markdownOptionCitations\undefined
-\let\markdownOptionCitationNbsps\undefined
-\let\markdownOptionContentBlocks\undefined
-\let\markdownOptionContentBlocksLanguageMap\undefined
-\let\markdownOptionDefinitionLists\undefined
-\let\markdownOptionEagerCache\undefined
-\let\markdownOptionFootnotes\undefined
-\let\markdownOptionFencedCode\undefined
-\let\markdownOptionHardLineBreaks\undefined
-\let\markdownOptionHashEnumerators\undefined
-\let\markdownOptionHeaderAttributes\undefined
-\let\markdownOptionHtml\undefined
-\let\markdownOptionHybrid\undefined
-\let\markdownOptionInlineFootnotes\undefined
-\let\markdownOptionJekyllData\undefined
-\let\markdownOptionPipeTables\undefined
-\let\markdownOptionPreserveTabs\undefined
-\let\markdownOptionRelativeReferences\undefined
-\let\markdownOptionShiftHeadings\undefined
-\let\markdownOptionSlice\undefined
-\let\markdownOptionSmartEllipses\undefined
-\let\markdownOptionStartNumber\undefined
-\let\markdownOptionStripIndent\undefined
-\let\markdownOptionTableCaptions\undefined
-\let\markdownOptionTaskLists\undefined
-\let\markdownOptionTexComments\undefined
-\let\markdownOptionTightLists\undefined
-\def\markdownOptionStripPercentSigns{false}%
 \ExplSyntaxOn
+\seq_new:N \g__markdown_plain_tex_options_seq
+\prop_new:N \g__markdown_plain_tex_option_types_prop
+\prop_new:N \g__markdown_default_plain_tex_options_prop
+\tl_const:Nn \c__markdown_option_layer_plain_tex_tl { plain_tex }
+\seq_put_right:NV \g__markdown_option_layers_seq \c__markdown_option_layer_plain_tex_tl
+\cs_new:Nn
+  \__markdown_add_plain_tex_option:nnn
+  {
+    \__markdown_add_option:Vnnn
+      \c__markdown_option_layer_plain_tex_tl
+      { #1 }
+      { #2 }
+      { #3 }
+  }
+\__markdown_add_plain_tex_option:nnn
+  { frozenCache }
+  { boolean }
+  { false }
+\__markdown_add_plain_tex_option:nnn
+  { helperScriptFileName }
+  { path }
+  { \jobname.markdown.lua }
+\str_new:N
+  \g_luabridge_helper_script_filename_str
+\tl_gset:Nn
+  \g_luabridge_helper_script_filename_str
+  { \markdownOptionHelperScriptFileName }
+\__markdown_add_plain_tex_option:nnn
+  { inputTempFileName }
+  { path }
+  { \jobname.markdown.in }
+\__markdown_add_plain_tex_option:nnn
+  { outputTempFileName }
+  { path }
+  { \jobname.markdown.out }
+\str_new:N
+  \g_luabridge_standard_output_filename_str
+\tl_gset:Nn
+  \g_luabridge_standard_output_filename_str
+  { \markdownOptionOutputTempFileName }
+\__markdown_add_plain_tex_option:nnn
+  { errorTempFileName }
+  { path }
+  { \jobname.markdown.err }
+\str_new:N
+  \g_luabridge_error_output_filename_str
+\tl_gset:Nn
+  \g_luabridge_error_output_filename_str
+  { \markdownOptionErrorTempFileName }
+\__markdown_add_plain_tex_option:nnn
+  { outputDir }
+  { path }
+  { . }
+\str_new:N
+  \g_luabridge_output_dirname_str
+\tl_gset:Nn
+  \g_luabridge_output_dirname_str
+  { \markdownOptionOutputDir }
+\cs_new:Nn \__markdown_plain_tex_define_option_commands:
+  {
+    \seq_map_inline:Nn
+      \g__markdown_option_layers_seq
+      {
+        \seq_map_inline:cn
+          { g__markdown_ ##1 _options_seq }
+          {
+              \__markdown_plain_tex_define_option_command:n
+                { ####1 }
+          }
+      }
+  }
+\cs_new:Nn \__markdown_plain_tex_define_option_command:n
+  {
+    \__markdown_get_default_option_value:nN
+      { #1 }
+      \l_tmpa_tl
+    \__markdown_set_option_value:nV
+      { #1 }
+      \l_tmpa_tl
+  }
+\cs_new:Nn
+  \__markdown_set_option_value:nn
+  {
+    \__markdown_define_option:n
+      { #1 }
+    \__markdown_get_option_type:nN
+      { #1 }
+      \l_tmpa_tl
+    \str_if_eq:NNTF
+      \c__markdown_option_type_counter_tl
+      \l_tmpa_tl
+      {
+        \__markdown_option_tl_to_csname:nN
+          { #1 }
+          \l_tmpa_tl
+        \int_gset:cn
+          { \l_tmpa_tl }
+          { #2 }
+      }
+      {
+        \__markdown_option_tl_to_csname:nN
+          { #1 }
+          \l_tmpa_tl
+        \cs_set:cpn
+          { \l_tmpa_tl }
+          { #2 }
+      }
+  }
+\cs_generate_variant:Nn
+  \__markdown_set_option_value:nn
+  { nV }
+\cs_new:Nn
+  \__markdown_define_option:n
+  {
+    \__markdown_option_tl_to_csname:nN
+      { #1 }
+      \l_tmpa_tl
+    \cs_if_free:cT
+      { \l_tmpa_tl }
+      {
+        \__markdown_get_option_type:nN
+          { #1 }
+          \l_tmpb_tl
+        \str_if_eq:NNT
+          \c__markdown_option_type_counter_tl
+          \l_tmpb_tl
+          {
+            \__markdown_option_tl_to_csname:nN
+              { #1 }
+              \l_tmpa_tl
+            \int_new:c
+              { \l_tmpa_tl }
+          }
+      }
+  }
+\__markdown_plain_tex_define_option_commands:
+\seq_put_right:Nn
+  \g__markdown_plain_tex_options_seq
+  { stripPercentSigns }
+\prop_put:Nnn
+  \g__markdown_plain_tex_option_types_prop
+  { stripPercentSigns }
+  { boolean }
+\prop_put:Nnx
+  \g__markdown_default_plain_tex_options_prop
+  { stripPercentSigns }
+  { false }
+\ExplSyntaxOff
+\ExplSyntaxOn
 \seq_new:N \g__markdown_renderers_seq
 \prop_new:N \g__markdown_renderer_arities_prop
 \ExplSyntaxOff
@@ -1449,84 +1511,57 @@
   { markdown/jekyllData }
   { }
 \ExplSyntaxOff
-\def\markdownRendererAttributeIdentifierPrototype#1{}%
-\def\markdownRendererAttributeClassNamePrototype#1{}%
-\def\markdownRendererAttributeKeyValuePrototype#1#2{}%
-\def\markdownRendererDocumentBeginPrototype{}%
-\def\markdownRendererDocumentEndPrototype{}%
-\def\markdownRendererInterblockSeparatorPrototype{}%
-\def\markdownRendererLineBreakPrototype{}%
-\def\markdownRendererEllipsisPrototype{}%
-\def\markdownRendererHeaderAttributeContextBeginPrototype{}%
-\def\markdownRendererHeaderAttributeContextEndPrototype{}%
-\def\markdownRendererNbspPrototype{}%
-\def\markdownRendererLeftBracePrototype{}%
-\def\markdownRendererRightBracePrototype{}%
-\def\markdownRendererDollarSignPrototype{}%
-\def\markdownRendererPercentSignPrototype{}%
-\def\markdownRendererAmpersandPrototype{}%
-\def\markdownRendererUnderscorePrototype{}%
-\def\markdownRendererHashPrototype{}%
-\def\markdownRendererCircumflexPrototype{}%
-\def\markdownRendererBackslashPrototype{}%
-\def\markdownRendererTildePrototype{}%
-\def\markdownRendererPipePrototype{}%
-\def\markdownRendererCodeSpanPrototype#1{}%
-\def\markdownRendererLinkPrototype#1#2#3#4{}%
-\def\markdownRendererImagePrototype#1#2#3#4{}%
-\def\markdownRendererContentBlockPrototype#1#2#3#4{}%
-\def\markdownRendererContentBlockOnlineImagePrototype#1#2#3#4{}%
-\def\markdownRendererContentBlockCodePrototype#1#2#3#4#5{}%
-\def\markdownRendererUlBeginPrototype{}%
-\def\markdownRendererUlBeginTightPrototype{}%
-\def\markdownRendererUlItemPrototype{}%
-\def\markdownRendererUlItemEndPrototype{}%
-\def\markdownRendererUlEndPrototype{}%
-\def\markdownRendererUlEndTightPrototype{}%
-\def\markdownRendererOlBeginPrototype{}%
-\def\markdownRendererOlBeginTightPrototype{}%
-\def\markdownRendererOlItemPrototype{}%
-\def\markdownRendererOlItemWithNumberPrototype#1{}%
-\def\markdownRendererOlItemEndPrototype{}%
-\def\markdownRendererOlEndPrototype{}%
-\def\markdownRendererOlEndTightPrototype{}%
-\def\markdownRendererDlBeginPrototype{}%
-\def\markdownRendererDlBeginTightPrototype{}%
-\def\markdownRendererDlItemPrototype#1{}%
-\def\markdownRendererDlItemEndPrototype{}%
-\def\markdownRendererDlDefinitionBeginPrototype{}%
-\def\markdownRendererDlDefinitionEndPrototype{}%
-\def\markdownRendererDlEndPrototype{}%
-\def\markdownRendererDlEndTightPrototype{}%
-\def\markdownRendererEmphasisPrototype#1{}%
-\def\markdownRendererStrongEmphasisPrototype#1{}%
-\def\markdownRendererBlockQuoteBeginPrototype{}%
-\def\markdownRendererBlockQuoteEndPrototype{}%
-\def\markdownRendererInputVerbatimPrototype#1{}%
-\def\markdownRendererInputFencedCodePrototype#1#2{}%
-\def\markdownRendererJekyllDataBeginPrototype{}%
-\def\markdownRendererJekyllDataEndPrototype{}%
-\def\markdownRendererHeadingOnePrototype#1{}%
-\def\markdownRendererHeadingTwoPrototype#1{}%
-\def\markdownRendererHeadingThreePrototype#1{}%
-\def\markdownRendererHeadingFourPrototype#1{}%
-\def\markdownRendererHeadingFivePrototype#1{}%
-\def\markdownRendererHeadingSixPrototype#1{}%
-\def\markdownRendererHorizontalRulePrototype{}%
-\def\markdownRendererFootnotePrototype#1{}%
-\def\markdownRendererCitePrototype#1{}%
-\def\markdownRendererTextCitePrototype#1{}%
-\def\markdownRendererTablePrototype#1#2#3{}%
-\def\markdownRendererInlineHtmlCommentPrototype#1{}%
-\let\markdownRendererBlockHtmlCommentBeginPrototype=\iffalse
-\let\markdownRendererBlockHtmlCommentBegin=\iffalse
-\let\markdownRendererBlockHtmlCommentEndPrototype=\fi
-\let\markdownRendererBlockHtmlCommentEnd=\fi
-\def\markdownRendererInlineHtmlTagPrototype#1{}%
-\def\markdownRendererInputBlockHtmlElementPrototype#1{}%
-\def\markdownRendererTickedBoxPrototype{}%
-\def\markdownRendererHalfTickedBoxPrototype{}%
-\def\markdownRendererUntickedBoxPrototype{}%
+\ExplSyntaxOn
+\cs_new:Nn \__markdown_plaintex_define_renderer_prototypes:
+  {
+    \seq_map_function:NN
+      \g__markdown_renderers_seq
+      \__markdown_plaintex_define_renderer_prototype:n
+    \let\markdownRendererBlockHtmlCommentBeginPrototype=\iffalse
+    \let\markdownRendererBlockHtmlCommentBegin=\iffalse
+    \let\markdownRendererBlockHtmlCommentEndPrototype=\fi
+    \let\markdownRendererBlockHtmlCommentEnd=\fi
+  }
+\cs_new:Nn \__markdown_plaintex_define_renderer_prototype:n
+  {
+    \__markdown_renderer_prototype_tl_to_csname:nN
+      { #1 }
+      \l_tmpa_tl
+    \prop_get:NnN
+      \g__markdown_renderer_arities_prop
+      { #1 }
+      \l_tmpb_tl
+    \__markdown_plaintex_define_renderer_prototype:cV
+      { \l_tmpa_tl }
+      \l_tmpb_tl
+  }
+\cs_new:Nn \__markdown_renderer_prototype_tl_to_csname:nN
+  {
+    \tl_set:Nn
+      \l_tmpa_tl
+      { \str_upper_case:n { #1 } }
+    \tl_set:Nx
+      #2
+      {
+        markdownRenderer
+        \tl_head:f { \l_tmpa_tl }
+        \tl_tail:n { #1 }
+        Prototype
+      }
+  }
+\cs_new:Nn \__markdown_plaintex_define_renderer_prototype:Nn
+  {
+    \cs_generate_from_arg_count:NNnn
+      #1
+      \cs_set:Npn
+      { #2 }
+      { }
+  }
+\cs_generate_variant:Nn
+  \__markdown_plaintex_define_renderer_prototype:Nn
+  { cV }
+\__markdown_plaintex_define_renderer_prototypes:
+\ExplSyntaxOff
 \let\markdownMakeOther\relax
 \let\markdownReadAndConvert\relax
 \begingroup
@@ -1535,13 +1570,42 @@
     |markdownReadAndConvert{\markdownEnd}%
                            {|markdownEnd}}%
 |endgroup
-\ifx\markdownMode\undefined
-  \ifx\directlua\undefined
-    \def\markdownMode{0}%
-  \else
-    \def\markdownMode{2}%
-  \fi
-\fi
+\ExplSyntaxOn
+\cs_if_exist:NF
+  \markdownMode
+  {
+    \file_if_exist:nTF
+      { lt3luabridge.tex }
+      {
+        \cs_new:Npn
+          \markdownMode
+          { 3 }
+      }
+      {
+        \cs_if_exist:NTF
+          \directlua
+          {
+            \cs_new:Npn
+              \markdownMode
+              { 2 }
+          }
+          {
+            \cs_new:Npn
+              \markdownMode
+              { 0 }
+          }
+      }
+  }
+\int_compare:nF
+  { \markdownMode = 3 }
+  {
+    \int_new:N
+      \g_luabridge_method_int
+    \int_gset:Nn
+      \g_luabridge_method_int
+      { \markdownMode }
+  }
+\ExplSyntaxOff
 \def\markdownLuaRegisterIBCallback#1{\relax}%
 \def\markdownLuaUnregisterIBCallback#1{\relax}%
 \ifx\markdownInfo\undefined
@@ -1557,7 +1621,6 @@
     \errhelp{#2.}%
     \errmessage{(l.\the\inputlineno) markdown.tex error: #1}}%
 \fi
-\newcount\markdownFrozenCacheCounter
 \def\markdownRendererInterblockSeparatorPrototype{\par}%
 \def\markdownRendererLineBreakPrototype{\hfil\break}%
 \let\markdownRendererEllipsisPrototype\dots
@@ -1638,7 +1701,7 @@
         \seq_get_right:NN
           \g__markdown_jekyll_data_datatypes_seq
           \l_tmpa_tl
-      \tl_if_eq:NNTF
+      \str_if_eq:NNTF
         \l_tmpa_tl
         \c__markdown_jekyll_data_sequence_tl
         {
@@ -1746,10 +1809,6 @@
 \ExplSyntaxOff
 \ExplSyntaxOn
 \tl_new:N \g__markdown_formatted_lua_options_tl
-\tl_const:Nn \c__markdown_lua_option_type_boolean { boolean }
-\tl_const:Nn \c__markdown_lua_option_type_counter { counter }
-\tl_const:Nn \c__markdown_lua_option_type_number  { number  }
-\tl_const:Nn \c__markdown_lua_option_type_string  { string  }
 \cs_new:Nn \__markdown_format_lua_options:
   {
     \tl_gclear:N
@@ -1760,109 +1819,38 @@
   }
 \cs_new:Nn \__markdown_format_lua_option:n
   {
-    \__markdown_typecheck_lua_option:n { #1 }
-    \tl_set:Nn
-      \l_tmpb_tl
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
-      \l_tmpa_tl
-      {
-        markdownOption
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
-      }
-    \prop_get:NnN
-      \g__markdown_lua_option_types_prop
+    \__markdown_typecheck_option:n
       { #1 }
-      \l_tmpb_tl
-    \cs_if_free:cTF
-      { \l_tmpa_tl }
-      { }
-      {
-        \tl_case:NnF
-          \l_tmpb_tl
-          {
-            \c__markdown_lua_option_type_string
-              {
-                \tl_gput_right:Nx
-                  \g__markdown_formatted_lua_options_tl
-                  { #1~=~"     \cs:w \l_tmpa_tl \cs_end: ",~ }
-              }
-            \c__markdown_lua_option_type_counter
-              {
-                \tl_gput_right:Nx
-                  \g__markdown_formatted_lua_options_tl
-                  { #1~=~ \the \cs:w \l_tmpa_tl \cs_end: ,~ }
-              }
-          }
-          {
-            \tl_gput_right:Nx
-              \g__markdown_formatted_lua_options_tl
-              {     #1~=~      \cs:w \l_tmpa_tl \cs_end: ,~ }
-          }
-      }
-  }
-\msg_new:nnn
-  { markdown }
-  { undefined-lua-option }
-  {
-    Lua~option~#1~is~undefined.
-  }
-\msg_new:nnn
-  { markdown }
-  { failed-typecheck-for-boolean-lua-option }
-  {
-    Lua~option~#1~has~value~#2,~
-    but~a~boolean~(true~or~false)~was~expected.
-  }
-\cs_new:Nn \__markdown_typecheck_lua_option:n
-  {
-    \tl_set:Nn
-      \l_tmpb_tl
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
+    \__markdown_get_option_type:nN
+      { #1 }
       \l_tmpa_tl
+    \bool_if:nTF
       {
-        markdownOption
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
+        \str_if_eq_p:VV
+          \l_tmpa_tl
+          \c__markdown_option_type_boolean_tl ||
+        \str_if_eq_p:VV
+          \l_tmpa_tl
+          \c__markdown_option_type_number_tl ||
+        \str_if_eq_p:VV
+          \l_tmpa_tl
+          \c__markdown_option_type_counter_tl
       }
-    \prop_get:NnNTF
-      \g__markdown_lua_option_types_prop
-      { #1 }
-      \l_tmpb_tl
       {
-        \cs_if_free:cTF
-          { \l_tmpa_tl }
-          { }
-          {
-            \tl_case:Nn
-              \l_tmpb_tl
-              {
-                \c__markdown_lua_option_type_boolean
-                  {
-                    \tl_case:cnF
-                      { \l_tmpa_tl }
-                      {
-                        \c__markdown_lua_option_value_true  { }
-                        \c__markdown_lua_option_value_false { }
-                      }
-                      {
-                        \msg_error:nnxx
-                          { markdown }
-                          { failed-typecheck-for-boolean-lua-option }
-                          { #1 }
-                          { \l_tmpa_tl }
-                      }
-                  }
-              }
-          }
+        \__markdown_get_option_value:nN
+          { #1 }
+          \l_tmpa_tl
+        \tl_gput_right:Nx
+          \g__markdown_formatted_lua_options_tl
+          { #1~=~  \l_tmpa_tl   ,~ }
       }
       {
-        \msg_error:nnn
-          { markdown }
-          { undefined-lua-option }
+        \__markdown_get_option_value:nN
           { #1 }
+          \l_tmpa_tl
+        \tl_gput_right:Nx
+          \g__markdown_formatted_lua_options_tl
+          { #1~=~ " \l_tmpa_tl " ,~ }
       }
   }
 \let\markdownPrepareLuaOptions=\__markdown_format_lua_options:
@@ -1878,39 +1866,38 @@
   local convert = md.new(\markdownLuaOptions)
 }%
 \ExplSyntaxOn
-\tl_const:Nn \c__markdown_lua_option_value_true  { true  }
-\tl_const:Nn \c__markdown_lua_option_value_false { false }
-\cs_new:Nn \__markdown_if_option:nTF
+\cs_new:Nn
+  \__markdown_if_option:nTF
   {
-    \tl_set:Nn
-      \l_tmpb_tl
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
+    \__markdown_get_option_type:nN
+      { #1 }
       \l_tmpa_tl
+    \str_if_eq:NNF
+      \l_tmpa_tl
+      \c__markdown_option_type_boolean_tl
       {
-        markdownOption
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
-      }
-    \cs_if_free:cTF
-      { \l_tmpa_tl }
-      {
-        \prop_get:NnN
-          \g__markdown_default_lua_options_prop
+        \msg_error:nnxx
+          { __markdown }
+          { expected-boolean-option }
           { #1 }
-          \l_tmpb_tl
+          { \l_tmpa_tl }
       }
-      {
-        \tl_set:Nf
-          \l_tmpb_tl
-          { \cs:w \l_tmpa_tl \cs_end: }
-      }
-    \tl_if_eq:NNTF
-      \l_tmpb_tl
-      \c__markdown_lua_option_value_true
+    \__markdown_get_option_value:nN
+      { #1 }
+      \l_tmpa_tl
+    \str_if_eq:NNTF
+      \l_tmpa_tl
+      \c__markdown_option_value_true_tl
       { #2 }
       { #3 }
   }
+\msg_new:nnn
+  { __markdown }
+  { expected-boolean-option }
+  {
+    Option~#1~has~type~#2,~
+    but~a~boolean~was~expected.
+  }
 \let\markdownIfOption=\__markdown_if_option:nTF
 \ExplSyntaxOff
 \csname newread\endcsname\markdownInputFileStream
@@ -1982,11 +1969,24 @@
       ^^M}@
     ^^M}@
 |endgroup
+\ExplSyntaxOn
+\int_compare:nT
+  { \markdownMode = 3 }
+  {
+    \markdownInfo{Using~mode~3:~The~lt3luabridge~package}
+    \file_input:n { lt3luabridge.tex }
+    \cs_new:Npn
+      \markdownLuaExecute
+      { \luabridgeExecute }
+  }
+\ExplSyntaxOff
 \ifnum\markdownMode<2\relax
 \ifnum\markdownMode=0\relax
-  \markdownInfo{Using mode 0: Shell escape via write18}%
+  \markdownWarning{Using mode 0: Shell escape via write18
+                   (deprecated, to be removed in Markdown 3.0.0)}%
 \else
-  \markdownInfo{Using mode 1: Shell escape via os.execute}%
+  \markdownWarning{Using mode 1: Shell escape via os.execute
+                   (deprecated, to be removed in Markdown 3.0.0)}%
 \fi
 \ifx\pdfshellescape\undefined
   \ifx\shellescape\undefined
@@ -2053,8 +2053,10 @@
       /|markdownOptionOutputTempFileName"}%
     |input|markdownOptionOutputTempFileName|relax}%
 |endgroup
-\else
-\markdownInfo{Using mode 2: Direct Lua access}%
+\fi
+\ifnum\markdownMode=2\relax
+  \markdownWarning{Using mode 2: Direct Lua access
+                   (deprecated, to be removed in Markdown 3.0.0)}%
 \begingroup
   \catcode`|=0%
   \catcode`\\=12%
@@ -2079,15 +2081,15 @@
     |begingroup
     |catcode`|%=12
     |markdownIfOption{frozenCache}{%
-      |ifnum|markdownFrozenCacheCounter=0|relax
+      |ifnum|markdownOptionFrozenCacheCounter=0|relax
         |markdownInfo{Reading frozen cache from
           "|markdownOptionFrozenCacheFileName"}%
         |input|markdownOptionFrozenCacheFileName|relax
       |fi
       |markdownInfo{Including markdown document number
-        "|the|markdownFrozenCacheCounter" from frozen cache}%
-      |csname markdownFrozenCache|the|markdownFrozenCacheCounter|endcsname
-      |global|advance|markdownFrozenCacheCounter by 1|relax
+        "|the|markdownOptionFrozenCacheCounter" from frozen cache}%
+      |csname markdownFrozenCache|the|markdownOptionFrozenCacheCounter|endcsname
+      |global|advance|markdownOptionFrozenCacheCounter by 1|relax
     }{%
       |markdownInfo{Including markdown document "#1"}%
       |openin|markdownInputFileStream#1
@@ -2101,7 +2103,7 @@
         assert(file:close())
         print(convert(input:gsub("\r\n?", "\n") .. "\n"))}%
       |markdownIfOption{finalizeCache}{%
-        |global|advance|markdownFrozenCacheCounter by 1|relax
+        |global|advance|markdownOptionFrozenCacheCounter by 1|relax
       }%
     }%
     |endgroup

Modified: trunk/Master/texmf-dist/tex/latex/markdown/markdown.sty
===================================================================
--- trunk/Master/texmf-dist/tex/latex/markdown/markdown.sty	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/tex/latex/markdown/markdown.sty	2022-06-29 20:10:26 UTC (rev 63757)
@@ -6,18 +6,15 @@
 %%
 %% markdown.dtx  (with options: `latex')
 %% 
-%% Copyright (C) 2016-2021 Vít Novotný
+%% Copyright (C) 2016-2022 Vít Novotný
 %% 
 %% This work may be distributed and/or modified under the
-%% conditions of the LaTeX Project Public License, either version 1.3
+%% conditions of the LaTeX Project Public License, either version 1.3c
 %% of this license or (at your option) any later version.
 %% The latest version of this license is in
 %% 
 %%    http://www.latex-project.org/lppl.txt
 %% 
-%% and version 1.3 or later is part of all distributions of LaTeX
-%% version 2005/12/01 or later.
-%% 
 %% This work has the LPPL maintenance status `maintained'.
 %% The Current Maintainer of this work is Vít Novotný.
 %% 
@@ -48,6 +45,10 @@
 \RequirePackage{keyval}
 \RequirePackage{xstring}
 \RequirePackage{expl3}
+\newcommand\markdownInfo[1]{\PackageInfo{markdown}{#1}}%
+\newcommand\markdownWarning[1]{\PackageWarning{markdown}{#1}}%
+\newcommand\markdownError[2]{\PackageError{markdown}{#1}{#2.}}%
+\input markdown/markdown
 \newenvironment{markdown}\relax\relax
 \newenvironment{markdown*}[1]\relax\relax
 \newcommand\markdownSetup[1]{%
@@ -69,15 +70,26 @@
   \@ifundefined
     {markdownLaTeXSetupSnippet\markdownLaTeXThemeName#1}%
     {#3}{#2}}%
-\newif\ifmarkdownLaTeXPlain
-  \markdownLaTeXPlainfalse
-\define at key{markdownOptions}{plain}[true]{%
-  \ifmarkdownLaTeXLoaded
-    \markdownWarning
-      {The plain option must be specified when loading the package}%
-  \else
-    \markdownLaTeXPlaintrue
-  \fi}
+\ExplSyntaxOn
+\seq_new:N \g__markdown_latex_options_seq
+\prop_new:N \g__markdown_latex_option_types_prop
+\prop_new:N \g__markdown_default_latex_options_prop
+\tl_const:Nn \c__markdown_option_layer_latex_tl { latex }
+\seq_put_right:NV \g__markdown_option_layers_seq \c__markdown_option_layer_latex_tl
+\cs_new:Nn
+  \__markdown_add_latex_option:nnn
+  {
+    \__markdown_add_option:Vnnn
+      \c__markdown_option_layer_latex_tl
+      { #1 }
+      { #2 }
+      { #3 }
+  }
+\__markdown_add_latex_option:nnn
+  { plain }
+  { boolean }
+  { false }
+\ExplSyntaxOff
 \newif\ifmarkdownLaTeXLoaded
   \markdownLaTeXLoadedfalse
 \AtEndOfPackage{\markdownLaTeXLoadedtrue}
@@ -104,96 +116,62 @@
         {The setup snippet is undefined}%
     }%
   }%
-\define at key{markdownOptions}{helperScriptFileName}{%
-  \def\markdownOptionHelperScriptFileName{#1}}%
-\define at key{markdownOptions}{inputTempFileName}{%
-  \def\markdownOptionInputTempFileName{#1}}%
-\define at key{markdownOptions}{outputTempFileName}{%
-  \def\markdownOptionOutputTempFileName{#1}}%
-\define at key{markdownOptions}{errorTempFileName}{%
-  \def\markdownOptionErrorTempFileName{#1}}%
-\define at key{markdownOptions}{cacheDir}{%
-  \def\markdownOptionCacheDir{#1}}%
-\define at key{markdownOptions}{outputDir}{%
-  \def\markdownOptionOutputDir{#1}}%
-\define at key{markdownOptions}{blankBeforeBlockquote}[true]{%
-  \def\markdownOptionBlankBeforeBlockquote{#1}}%
-\define at key{markdownOptions}{blankBeforeCodeFence}[true]{%
-  \def\markdownOptionBlankBeforeCodeFence{#1}}%
-\define at key{markdownOptions}{blankBeforeHeading}[true]{%
-  \def\markdownOptionBlankBeforeHeading{#1}}%
-\define at key{markdownOptions}{breakableBlockquotes}[true]{%
-  \def\markdownOptionBreakableBlockquotes{#1}}%
-\define at key{markdownOptions}{citations}[true]{%
-  \def\markdownOptionCitations{#1}}%
-\define at key{markdownOptions}{citationNbsps}[true]{%
-  \def\markdownOptionCitationNbsps{#1}}%
-\define at key{markdownOptions}{contentBlocks}[true]{%
-  \def\markdownOptionContentBlocks{#1}}%
-\define at key{markdownOptions}{codeSpans}[true]{%
-  \def\markdownOptionCodeSpans{#1}}%
-\define at key{markdownOptions}{contentBlocksLanguageMap}{%
-  \def\markdownOptionContentBlocksLanguageMap{#1}}%
-\define at key{markdownOptions}{definitionLists}[true]{%
-  \def\markdownOptionDefinitionLists{#1}}%
-\define at key{markdownOptions}{eagerCache}[true]{%
-  \def\markdownOptionEagerCache{#1}}%
-\define at key{markdownOptions}{expectJekyllData}[true]{%
-  \def\markdownOptionExpectJekyllData{#1}}%
-\define at key{markdownOptions}{footnotes}[true]{%
-  \def\markdownOptionFootnotes{#1}}%
-\define at key{markdownOptions}{fencedCode}[true]{%
-  \def\markdownOptionFencedCode{#1}}%
-\define at key{markdownOptions}{jekyllData}[true]{%
-  \def\markdownOptionJekyllData{#1}}%
-\define at key{markdownOptions}{hardLineBreaks}[true]{%
-  \def\markdownOptionHardLineBreaks{#1}}%
-\define at key{markdownOptions}{hashEnumerators}[true]{%
-  \def\markdownOptionHashEnumerators{#1}}%
-\define at key{markdownOptions}{headerAttributes}[true]{%
-  \def\markdownOptionHeaderAttributes{#1}}%
-\define at key{markdownOptions}{html}[true]{%
-  \def\markdownOptionHtml{#1}}%
-\define at key{markdownOptions}{hybrid}[true]{%
-  \def\markdownOptionHybrid{#1}}%
-\define at key{markdownOptions}{inlineFootnotes}[true]{%
-  \def\markdownOptionInlineFootnotes{#1}}%
-\define at key{markdownOptions}{pipeTables}[true]{%
-  \def\markdownOptionPipeTables{#1}}%
-\define at key{markdownOptions}{preserveTabs}[true]{%
-  \def\markdownOptionPreserveTabs{#1}}%
-\define at key{markdownOptions}{relativeReferences}[true]{%
-  \def\markdownOptionRelativeReferences{#1}}%
-\define at key{markdownOptions}{smartEllipses}[true]{%
-  \def\markdownOptionSmartEllipses{#1}}%
-\define at key{markdownOptions}{shiftHeadings}{%
-  \def\markdownOptionShiftHeadings{#1}}%
-\define at key{markdownOptions}{slice}{%
-  \def\markdownOptionSlice{#1}}%
-\define at key{markdownOptions}{startNumber}[true]{%
-  \def\markdownOptionStartNumber{#1}}%
-\define at key{markdownOptions}{stripIndent}[true]{%
-  \def\markdownOptionStripIndent{#1}}%
-\define at key{markdownOptions}{tableCaptions}[true]{%
-  \def\markdownOptionTableCaptions{#1}}%
-\define at key{markdownOptions}{taskLists}[true]{%
-  \def\markdownOptionTaskLists{#1}}%
-\define at key{markdownOptions}{texComments}[true]{%
-  \def\markdownOptionTexComments{#1}}%
-\define at key{markdownOptions}{tightLists}[true]{%
-  \def\markdownOptionTightLists{#1}}%
-\define at key{markdownOptions}{underscores}[true]{%
-  \def\markdownOptionUnderscores{#1}}%
-\define at key{markdownOptions}{stripPercentSigns}[true]{%
-  \def\markdownOptionStripPercentSigns{#1}}%
-\define at key{markdownOptions}{finalizeCache}[true]{%
-  \def\markdownOptionFinalizeCache{#1}}%
+\ExplSyntaxOn
+\cs_new:Nn \__markdown_latex_define_option_commands_and_keyvals:
+  {
+    \seq_map_inline:Nn
+      \g__markdown_latex_options_seq
+      {
+          \__markdown_plain_tex_define_option_command:n
+            { ##1 }
+      }
+    \seq_map_inline:Nn
+      \g__markdown_option_layers_seq
+      {
+        \seq_map_inline:cn
+          { g__markdown_ ##1 _options_seq }
+          {
+              \__markdown_latex_define_option_keyval:nn
+                { ##1 }
+                { ####1 }
+          }
+      }
+  }
+\cs_new:Nn \__markdown_latex_define_option_keyval:nn
+  {
+    \prop_get:cnN
+      { g__markdown_ #1 _option_types_prop }
+      { #2 }
+      \l_tmpa_tl
+    \str_if_eq:VVTF
+      \l_tmpa_tl
+      \c__markdown_option_type_boolean_tl
+      {
+        \define at key
+          { markdownOptions }
+          { #2 }
+          [ true ]
+          {
+            \__markdown_set_option_value:nn
+              { #2 }
+              { ##1 }
+          }
+      }
+      {
+        \define at key
+          { markdownOptions }
+          { #2 }
+          {
+            \__markdown_set_option_value:nn
+              { #2 }
+              { ##1 }
+          }
+      }
+  }
+\__markdown_latex_define_option_commands_and_keyvals:
+\ExplSyntaxOff
 \DeclareOption{finalizecache}{\markdownSetup{finalizeCache}}
-\define at key{markdownOptions}{frozenCache}[true]{%
-  \def\markdownOptionFrozenCache{#1}}%
 \DeclareOption{frozencache}{\markdownSetup{frozenCache}}
-\define at key{markdownOptions}{frozenCacheFileName}{%
-  \def\markdownOptionFrozenCacheFileName{#1}}%
 \ExplSyntaxOn
 \cs_new:Nn \__markdown_latex_define_renderers:
   {
@@ -203,16 +181,9 @@
   }
 \cs_new:Nn \__markdown_latex_define_renderer:n
   {
-    \tl_set:Nn
-      \l_tmpb_tl
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
+    \__markdown_renderer_tl_to_csname:nN
+      { #1 }
       \l_tmpa_tl
-      {
-        markdownRenderer
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
-      }
     \prop_get:NnN
       \g__markdown_renderer_arities_prop
       { #1 }
@@ -222,6 +193,19 @@
       { \l_tmpa_tl }
       \l_tmpb_tl
   }
+\cs_new:Nn \__markdown_renderer_tl_to_csname:nN
+  {
+    \tl_set:Nn
+      \l_tmpa_tl
+      { \str_upper_case:n { #1 } }
+    \tl_set:Nx
+      #2
+      {
+        markdownRenderer
+        \tl_head:f { \l_tmpa_tl }
+        \tl_tail:n { #1 }
+      }
+  }
 \cs_new:Nn \__markdown_latex_define_renderer:nNn
   {
     \define at key
@@ -248,17 +232,9 @@
   }
 \cs_new:Nn \__markdown_latex_define_renderer_prototype:n
   {
-    \tl_set:Nn
-      \l_tmpb_tl
-      { \str_upper_case:n { #1 } }
-    \tl_set:Nx
+    \__markdown_renderer_prototype_tl_to_csname:nN
+      { #1 }
       \l_tmpa_tl
-      {
-        markdownRenderer
-        \tl_head:f { \l_tmpb_tl }
-        \tl_tail:n { #1 }
-        Prototype
-      }
     \prop_get:NnN
       \g__markdown_renderer_arities_prop
       { #1 }
@@ -285,10 +261,6 @@
   \__markdown_latex_define_renderer_prototype:nNn
   { ncV }
 \ExplSyntaxOff
-\newcommand\markdownInfo[1]{\PackageInfo{markdown}{#1}}%
-\newcommand\markdownWarning[1]{\PackageWarning{markdown}{#1}}%
-\newcommand\markdownError[2]{\PackageError{markdown}{#1}{#2.}}%
-\input markdown/markdown
 \def\markdownVersionSpace{ }%
 \ProvidesPackage{markdown}[\markdownLastModified\markdownVersionSpace v%
   \markdownVersion\markdownVersionSpace markdown renderer]%
@@ -357,7 +329,7 @@
   \setkeys{markdownRendererPrototypes}{#1}%
   \def\KV at prefix{KV at markdownOptions@}}%
 \define at key{markdownOptions}{code}{#1}%
-\ifmarkdownLaTeXPlain\else
+\markdownIfOption{plain}{\iffalse}{\iftrue}
 \markdownIfOption{tightLists}{
   \@ifclassloaded{beamer}{}{\RequirePackage{paralist}}%
 }{}
@@ -811,16 +783,34 @@
   \next
 }
 \def\markdownLaTeXRendererAutolink#1#2{%
+  \tl_set:Nn
+    \l_tmpa_tl
+    { #2 }
+  \tl_trim_spaces:N
+    \l_tmpa_tl
   \tl_set:Nx
-    \l_tmpa_tl
-    { \str_range:nnn { #2 } { 1 } { 1 } }
+    \l_tmpb_tl
+    {
+      \tl_range:Nnn
+        \l_tmpa_tl
+        { 1 }
+        { 1 }
+    }
   \str_if_eq:NNTF
-    \l_tmpa_tl
+    \l_tmpb_tl
     \c_hash_str
     {
-      \exp_args:No
+      \tl_set:Nx
+        \l_tmpb_tl
+        {
+          \tl_range:Nnn
+            \l_tmpa_tl
+            { 2 }
+            { -1 }
+        }
+      \exp_args:NV
         \ref
-        { \str_range:nnn { #2 } { 2 } { -1 } }
+        \l_tmpb_tl
     }{
       \url { #2 }
     }

Modified: trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_dot.sty
===================================================================
--- trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_dot.sty	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_dot.sty	2022-06-29 20:10:26 UTC (rev 63757)
@@ -6,18 +6,15 @@
 %%
 %% markdown.dtx  (with options: `latex-themes-witiko-dot')
 %% 
-%% Copyright (C) 2016-2021 Vít Novotný
+%% Copyright (C) 2016-2022 Vít Novotný
 %% 
 %% This work may be distributed and/or modified under the
-%% conditions of the LaTeX Project Public License, either version 1.3
+%% conditions of the LaTeX Project Public License, either version 1.3c
 %% of this license or (at your option) any later version.
 %% The latest version of this license is in
 %% 
 %%    http://www.latex-project.org/lppl.txt
 %% 
-%% and version 1.3 or later is part of all distributions of LaTeX
-%% version 2005/12/01 or later.
-%% 
 %% This work has the LPPL maintenance status `maintained'.
 %% The Current Maintainer of this work is Vít Novotný.
 %% 

Modified: trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_graphicx_http.sty
===================================================================
--- trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_graphicx_http.sty	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_graphicx_http.sty	2022-06-29 20:10:26 UTC (rev 63757)
@@ -6,18 +6,15 @@
 %%
 %% markdown.dtx  (with options: `latex-themes-witiko-graphicx-http')
 %% 
-%% Copyright (C) 2016-2021 Vít Novotný
+%% Copyright (C) 2016-2022 Vít Novotný
 %% 
 %% This work may be distributed and/or modified under the
-%% conditions of the LaTeX Project Public License, either version 1.3
+%% conditions of the LaTeX Project Public License, either version 1.3c
 %% of this license or (at your option) any later version.
 %% The latest version of this license is in
 %% 
 %%    http://www.latex-project.org/lppl.txt
 %% 
-%% and version 1.3 or later is part of all distributions of LaTeX
-%% version 2005/12/01 or later.
-%% 
 %% This work has the LPPL maintenance status `maintained'.
 %% The Current Maintainer of this work is Vít Novotný.
 %% 

Modified: trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_tilde.sty
===================================================================
--- trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_tilde.sty	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/tex/latex/markdown/markdownthemewitiko_tilde.sty	2022-06-29 20:10:26 UTC (rev 63757)
@@ -6,18 +6,15 @@
 %%
 %% markdown.dtx  (with options: `latex-themes-witiko-tilde')
 %% 
-%% Copyright (C) 2016-2021 Vít Novotný
+%% Copyright (C) 2016-2022 Vít Novotný
 %% 
 %% This work may be distributed and/or modified under the
-%% conditions of the LaTeX Project Public License, either version 1.3
+%% conditions of the LaTeX Project Public License, either version 1.3c
 %% of this license or (at your option) any later version.
 %% The latest version of this license is in
 %% 
 %%    http://www.latex-project.org/lppl.txt
 %% 
-%% and version 1.3 or later is part of all distributions of LaTeX
-%% version 2005/12/01 or later.
-%% 
 %% This work has the LPPL maintenance status `maintained'.
 %% The Current Maintainer of this work is Vít Novotný.
 %% 

Modified: trunk/Master/texmf-dist/tex/luatex/markdown/markdown.lua
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/markdown/markdown.lua	2022-06-29 00:05:57 UTC (rev 63756)
+++ trunk/Master/texmf-dist/tex/luatex/markdown/markdown.lua	2022-06-29 20:10:26 UTC (rev 63757)
@@ -58,7 +58,7 @@
 -- those in the standard .ins files.
 -- 
 local metadata = {
-    version   = "2.15.2-0-gb238dbc",
+    version   = "2.15.3-0-g7c8e03d",
     comment   = "A module for the conversion from markdown to plain TeX",
     author    = "John MacFarlane, Hans Hagen, Vít Novotný",
     copyright = {"2009-2016 John MacFarlane, Hans Hagen",
@@ -2300,9 +2300,8 @@
 M.writer = {}
 function M.writer.new(options)
   local self = {}
-  options = options or {}
-  setmetatable(options, { __index = function (_, key)
-    return defaultOptions[key] end })
+  self.cacheDir = options.cacheDir
+  self.hybrid = options.hybrid
   local slice_specifiers = {}
   for specifier in options.slice:gmatch("[^%s]+") do
     table.insert(slice_specifiers, specifier)
@@ -2351,26 +2350,19 @@
     if not self.is_writing then return "" end
     return "\\markdownRendererHorizontalRule{}"
   end
-   local escaped_uri_chars = {
+   self.escaped_uri_chars = {
      ["{"] = "\\markdownRendererLeftBrace{}",
      ["}"] = "\\markdownRendererRightBrace{}",
      ["%"] = "\\markdownRendererPercentSign{}",
      ["\\"] = "\\markdownRendererBackslash{}",
    }
-   local escaped_citation_chars = {
-     ["{"] = "\\markdownRendererLeftBrace{}",
-     ["}"] = "\\markdownRendererRightBrace{}",
-     ["%"] = "\\markdownRendererPercentSign{}",
-     ["\\"] = "\\markdownRendererBackslash{}",
-     ["#"] = "\\markdownRendererHash{}",
-   }
-   local escaped_minimal_strings = {
+   self.escaped_minimal_strings = {
      ["^^"] = "\\markdownRendererCircumflex\\markdownRendererCircumflex ",
      ["☒"] = "\\markdownRendererTickedBox{}",
      ["⌛"] = "\\markdownRendererHalfTickedBox{}",
      ["☐"] = "\\markdownRendererUntickedBox{}",
    }
-  local escaped_chars = {
+  self.escaped_chars = {
      ["{"] = "\\markdownRendererLeftBrace{}",
      ["}"] = "\\markdownRendererRightBrace{}",
      ["%"] = "\\markdownRendererPercentSign{}",
@@ -2383,21 +2375,16 @@
      ["~"] = "\\markdownRendererTilde{}",
      ["|"] = "\\markdownRendererPipe{}",
    }
-  local escape = util.escaper(escaped_chars, escaped_minimal_strings)
-  local escape_citation = util.escaper(escaped_citation_chars,
-    escaped_minimal_strings)
-  local escape_uri = util.escaper(escaped_uri_chars, escaped_minimal_strings)
-  local escape_minimal = util.escaper({}, escaped_minimal_strings)
+  self.escape = util.escaper(self.escaped_chars, self.escaped_minimal_strings)
+  self.escape_uri = util.escaper(self.escaped_uri_chars, self.escaped_minimal_strings)
+  self.escape_minimal = util.escaper({}, self.escaped_minimal_strings)
   if options.hybrid then
-    self.string = escape_minimal
-    self.citation = escape_minimal
-    self.uri = escape_minimal
+    self.string = self.escape_minimal
+    self.uri = self.escape_minimal
   else
-    self.string = escape
-    self.citation = escape_citation
-    self.uri = escape_uri
+    self.string = self.escape
+    self.uri = self.escape_uri
   end
-  self.escape = escape
   function self.code(s)
     return {"\\markdownRendererCodeSpan{",self.escape(s),"}"}
   end
@@ -2407,28 +2394,6 @@
                           "{",self.uri(src),"}",
                           "{",self.string(tit or ""),"}"}
   end
-  function self.table(rows, caption)
-    if not self.is_writing then return "" end
-    local buffer = {"\\markdownRendererTable{",
-      caption or "", "}{", #rows - 1, "}{", #rows[1], "}"}
-    local temp = rows[2] -- put alignments on the first row
-    rows[2] = rows[1]
-    rows[1] = temp
-    for i, row in ipairs(rows) do
-      table.insert(buffer, "{")
-      for _, column in ipairs(row) do
-        if i > 1 then -- do not use braces for alignments
-          table.insert(buffer, "{")
-        end
-        table.insert(buffer, column)
-        if i > 1 then
-          table.insert(buffer, "}")
-        end
-      end
-      table.insert(buffer, "}")
-    end
-    return buffer
-  end
   function self.image(lab,src,tit)
     return {"\\markdownRendererImage{",lab,"}",
                            "{",self.string(src),"}",
@@ -2435,57 +2400,6 @@
                            "{",self.uri(src),"}",
                            "{",self.string(tit or ""),"}"}
   end
-  local languages_json = (function()
-    local ran_ok, kpse = pcall(require, "kpse")
-    if ran_ok then
-      kpse.set_program_name("luatex")
-    else
-      kpse = {lookup=function(filename, options) return filename end}
-    end
-    local base, prev, curr
-    for _, filename in ipairs{kpse.lookup(options.contentBlocksLanguageMap,
-                                          { all=true })} do
-      local file = io.open(filename, "r")
-      if not file then goto continue end
-      json = file:read("*all"):gsub('("[^\n]-"):','[%1]=')
-      curr = (function()
-        local _ENV={ json=json, load=load } -- run in sandbox
-        return load("return "..json)()
-      end)()
-      if type(curr) == "table" then
-        if base == nil then
-          base = curr
-        else
-          setmetatable(prev, { __index = curr })
-        end
-        prev = curr
-      end
-      ::continue::
-    end
-    return base or {}
-  end)()
-  function self.contentblock(src,suf,type,tit)
-    if not self.is_writing then return "" end
-    src = src.."."..suf
-    suf = suf:lower()
-    if type == "onlineimage" then
-      return {"\\markdownRendererContentBlockOnlineImage{",suf,"}",
-                             "{",self.string(src),"}",
-                             "{",self.uri(src),"}",
-                             "{",self.string(tit or ""),"}"}
-    elseif languages_json[suf] then
-      return {"\\markdownRendererContentBlockCode{",suf,"}",
-                             "{",self.string(languages_json[suf]),"}",
-                             "{",self.string(src),"}",
-                             "{",self.uri(src),"}",
-                             "{",self.string(tit or ""),"}"}
-    else
-      return {"\\markdownRendererContentBlock{",suf,"}",
-                             "{",self.string(src),"}",
-                             "{",self.uri(src),"}",
-                             "{",self.string(tit or ""),"}"}
-    end
-  end
   local function ulitem(s)
     return {"\\markdownRendererUlItem ",s,
             "\\markdownRendererUlItemEnd "}
@@ -2548,33 +2462,9 @@
   end
   function self.block_html_element(s)
     if not self.is_writing then return "" end
-    local name = util.cache(options.cacheDir, s, nil, nil, ".verbatim")
+    local name = util.cache(self.cacheDir, s, nil, nil, ".verbatim")
     return {"\\markdownRendererInputBlockHtmlElement{",name,"}"}
   end
-  local function dlitem(term, defs)
-    local retVal = {"\\markdownRendererDlItem{",term,"}"}
-    for _, def in ipairs(defs) do
-      retVal[#retVal+1] = {"\\markdownRendererDlDefinitionBegin ",def,
-                           "\\markdownRendererDlDefinitionEnd "}
-    end
-    retVal[#retVal+1] = "\\markdownRendererDlItemEnd "
-    return retVal
-  end
-
-  function self.definitionlist(items,tight)
-    if not self.is_writing then return "" end
-    local buffer = {}
-    for _,item in ipairs(items) do
-      buffer[#buffer + 1] = dlitem(item.term, item.definitions)
-    end
-    if tight and options.tightLists then
-      return {"\\markdownRendererDlBeginTight\n", buffer,
-        "\n\\markdownRendererDlEndTight"}
-    else
-      return {"\\markdownRendererDlBegin\n", buffer,
-        "\n\\markdownRendererDlEnd"}
-    end
-  end
   function self.emphasis(s)
     return {"\\markdownRendererEmphasis{",s,"}"}
   end
@@ -2598,15 +2488,9 @@
   function self.verbatim(s)
     if not self.is_writing then return "" end
     s = string.gsub(s, '[\r\n%s]*$', '')
-    local name = util.cache(options.cacheDir, s, nil, nil, ".verbatim")
+    local name = util.cache(self.cacheDir, s, nil, nil, ".verbatim")
     return {"\\markdownRendererInputVerbatim{",name,"}"}
   end
-  function self.fencedCode(i, s)
-    if not self.is_writing then return "" end
-    s = string.gsub(s, '[\r\n%s]*$', '')
-    local name = util.cache(options.cacheDir, s, nil, nil, ".verbatim")
-    return {"\\markdownRendererInputFencedCode{",name,"}{",i,"}"}
-  end
   function self.document(d)
     local active_attributes = self.active_attributes
     local buf = {"\\markdownRendererDocumentBegin\n", d}
@@ -2626,88 +2510,6 @@
 
     return buf
   end
-  function self.jekyllData(d, t, p)
-    if not self.is_writing then return "" end
-
-    local buf = {}
-
-    local keys = {}
-    for k, _ in pairs(d) do
-      table.insert(keys, k)
-    end
-    table.sort(keys)
-
-    if not p then
-      table.insert(buf, "\\markdownRendererJekyllDataBegin")
-    end
-
-    if #d > 0 then
-        table.insert(buf, "\\markdownRendererJekyllDataSequenceBegin{")
-        table.insert(buf, self.uri(p or "null"))
-        table.insert(buf, "}{")
-        table.insert(buf, #keys)
-        table.insert(buf, "}")
-    else
-        table.insert(buf, "\\markdownRendererJekyllDataMappingBegin{")
-        table.insert(buf, self.uri(p or "null"))
-        table.insert(buf, "}{")
-        table.insert(buf, #keys)
-        table.insert(buf, "}")
-    end
-
-    for _, k in ipairs(keys) do
-      local v = d[k]
-      local typ = type(v)
-      k = tostring(k or "null")
-      if typ == "table" and next(v) ~= nil then
-        table.insert(
-          buf,
-          self.jekyllData(v, t, k)
-        )
-      else
-        k = self.uri(k)
-        v = tostring(v)
-        if typ == "boolean" then
-          table.insert(buf, "\\markdownRendererJekyllDataBoolean{")
-          table.insert(buf, k)
-          table.insert(buf, "}{")
-          table.insert(buf, v)
-          table.insert(buf, "}")
-        elseif typ == "number" then
-          table.insert(buf, "\\markdownRendererJekyllDataNumber{")
-          table.insert(buf, k)
-          table.insert(buf, "}{")
-          table.insert(buf, v)
-          table.insert(buf, "}")
-        elseif typ == "string" then
-          table.insert(buf, "\\markdownRendererJekyllDataString{")
-          table.insert(buf, k)
-          table.insert(buf, "}{")
-          table.insert(buf, t(v))
-          table.insert(buf, "}")
-        elseif typ == "table" then
-          table.insert(buf, "\\markdownRendererJekyllDataEmpty{")
-          table.insert(buf, k)
-          table.insert(buf, "}")
-        else
-          error(format("Unexpected type %s for value of " ..
-                       "YAML key %s", typ, k))
-        end
-      end
-    end
-
-    if #d > 0 then
-      table.insert(buf, "\\markdownRendererJekyllDataSequenceEnd")
-    else
-      table.insert(buf, "\\markdownRendererJekyllDataMappingEnd")
-    end
-
-    if not p then
-      table.insert(buf, "\\markdownRendererJekyllDataEnd")
-    end
-
-    return buf
-  end
   self.active_attributes = {}
   function self.heading(s, level, attributes)
     attributes = attributes or {}
@@ -2826,18 +2628,6 @@
 
     return buf
   end
-  function self.note(s)
-    return {"\\markdownRendererFootnote{",s,"}"}
-  end
-  function self.citations(text_cites, cites)
-    local buffer = {"\\markdownRenderer", text_cites and "TextCite" or "Cite",
-      "{", #cites, "}"}
-    for _,cite in ipairs(cites) do
-      buffer[#buffer+1] = {cite.suppress_author and "-" or "+", "{",
-        cite.prenote or "", "}{", cite.postnote or "", "}{", cite.name, "}"}
-    end
-    return buffer
-  end
   function self.get_state()
     return {
       is_writing=self.is_writing,
@@ -2905,8 +2695,6 @@
 parsers.alphanumeric           = R("AZ","az","09")
 parsers.keyword                = parsers.letter
                                * parsers.alphanumeric^0
-parsers.citation_chars         = parsers.alphanumeric
-                               + S("#$%&-+<>~/_")
 parsers.internal_punctuation   = S(":;,.?")
 
 parsers.doubleasterisks        = P("**")
@@ -3162,204 +2950,6 @@
 parsers.optionaltitle
                     = parsers.spnl * parsers.title * parsers.spacechar^0
                     + Cc("")
-parsers.contentblock_tail
-                    = parsers.optionaltitle
-                    * (parsers.newline + parsers.eof)
-
--- case insensitive online image suffix:
-parsers.onlineimagesuffix
-                    = (function(...)
-                        local parser = nil
-                        for _,suffix in ipairs({...}) do
-                          local pattern=nil
-                          for i=1,#suffix do
-                            local char=suffix:sub(i,i)
-                            char = S(char:lower()..char:upper())
-                            if pattern == nil then
-                              pattern = char
-                            else
-                              pattern = pattern * char
-                            end
-                          end
-                          if parser == nil then
-                            parser = pattern
-                          else
-                            parser = parser + pattern
-                          end
-                        end
-                        return parser
-                      end)("png", "jpg", "jpeg", "gif", "tif", "tiff")
-
--- online image url for iA Writer content blocks with mandatory suffix,
--- allowing nested brackets:
-parsers.onlineimageurl
-                    = (parsers.less
-                      * Cs((parsers.anyescaped
-                           - parsers.more
-                           - #(parsers.period
-                              * parsers.onlineimagesuffix
-                              * parsers.more
-                              * parsers.contentblock_tail))^0)
-                      * parsers.period
-                      * Cs(parsers.onlineimagesuffix)
-                      * parsers.more
-                      + (Cs((parsers.inparens
-                            + (parsers.anyescaped
-                              - parsers.spacing
-                              - parsers.rparent
-                              - #(parsers.period
-                                 * parsers.onlineimagesuffix
-                                 * parsers.contentblock_tail)))^0)
-                        * parsers.period
-                        * Cs(parsers.onlineimagesuffix))
-                      ) * Cc("onlineimage")
-
--- filename for iA Writer content blocks with mandatory suffix:
-parsers.localfilepath
-                    = parsers.slash
-                    * Cs((parsers.anyescaped
-                         - parsers.tab
-                         - parsers.newline
-                         - #(parsers.period
-                            * parsers.alphanumeric^1
-                            * parsers.contentblock_tail))^1)
-                    * parsers.period
-                    * Cs(parsers.alphanumeric^1)
-                    * Cc("localfile")
-parsers.citation_name = Cs(parsers.dash^-1) * parsers.at
-                      * Cs(parsers.citation_chars
-                          * (((parsers.citation_chars + parsers.internal_punctuation
-                              - parsers.comma - parsers.semicolon)
-                             * -#((parsers.internal_punctuation - parsers.comma
-                                  - parsers.semicolon)^0
-                                 * -(parsers.citation_chars + parsers.internal_punctuation
-                                    - parsers.comma - parsers.semicolon)))^0
-                            * parsers.citation_chars)^-1)
-
-parsers.citation_body_prenote
-                    = Cs((parsers.alphanumeric^1
-                         + parsers.bracketed
-                         + parsers.inticks
-                         + (parsers.anyescaped
-                           - (parsers.rbracket + parsers.blankline^2))
-                         - (parsers.spnl * parsers.dash^-1 * parsers.at))^0)
-
-parsers.citation_body_postnote
-                    = Cs((parsers.alphanumeric^1
-                         + parsers.bracketed
-                         + parsers.inticks
-                         + (parsers.anyescaped
-                           - (parsers.rbracket + parsers.semicolon
-                             + parsers.blankline^2))
-                         - (parsers.spnl * parsers.rbracket))^0)
-
-parsers.citation_body_chunk
-                    = parsers.citation_body_prenote
-                    * parsers.spnl * parsers.citation_name
-                    * (parsers.internal_punctuation - parsers.semicolon)^-1
-                    * parsers.spnl * parsers.citation_body_postnote
-
-parsers.citation_body
-                    = parsers.citation_body_chunk
-                    * (parsers.semicolon * parsers.spnl
-                      * parsers.citation_body_chunk)^0
-
-parsers.citation_headless_body_postnote
-                    = Cs((parsers.alphanumeric^1
-                         + parsers.bracketed
-                         + parsers.inticks
-                         + (parsers.anyescaped
-                           - (parsers.rbracket + parsers.at
-                             + parsers.semicolon + parsers.blankline^2))
-                         - (parsers.spnl * parsers.rbracket))^0)
-
-parsers.citation_headless_body
-                    = parsers.citation_headless_body_postnote
-                    * (parsers.sp * parsers.semicolon * parsers.spnl
-                      * parsers.citation_body_chunk)^0
-local function strip_first_char(s)
-  return s:sub(2)
-end
-
-parsers.RawNoteRef = #(parsers.lbracket * parsers.circumflex)
-                   * parsers.tag / strip_first_char
-local function make_pipe_table_rectangular(rows)
-  local num_columns = #rows[2]
-  local rectangular_rows = {}
-  for i = 1, #rows do
-    local row = rows[i]
-    local rectangular_row = {}
-    for j = 1, num_columns do
-      rectangular_row[j] = row[j] or ""
-    end
-    table.insert(rectangular_rows, rectangular_row)
-  end
-  return rectangular_rows
-end
-
-local function pipe_table_row(allow_empty_first_column
-                             , nonempty_column
-                             , column_separator
-                             , column)
-  local row_beginning
-  if allow_empty_first_column then
-    row_beginning = -- empty first column
-                    #(parsers.spacechar^4
-                     * column_separator)
-                  * parsers.optionalspace
-                  * column
-                  * parsers.optionalspace
-                  -- non-empty first column
-                  + parsers.nonindentspace
-                  * nonempty_column^-1
-                  * parsers.optionalspace
-  else
-    row_beginning = parsers.nonindentspace
-                  * nonempty_column^-1
-                  * parsers.optionalspace
-  end
-
-  return Ct(row_beginning
-           * (-- single column with no leading pipes
-              #(column_separator
-               * parsers.optionalspace
-               * parsers.newline)
-             * column_separator
-             * parsers.optionalspace
-             -- single column with leading pipes or
-             -- more than a single column
-             + (column_separator
-               * parsers.optionalspace
-               * column
-               * parsers.optionalspace)^1
-             * (column_separator
-               * parsers.optionalspace)^-1))
-end
-
-parsers.table_hline_separator = parsers.pipe + parsers.plus
-parsers.table_hline_column = (parsers.dash
-                             - #(parsers.dash
-                                * (parsers.spacechar
-                                  + parsers.table_hline_separator
-                                  + parsers.newline)))^1
-                           * (parsers.colon * Cc("r")
-                             + parsers.dash * Cc("d"))
-                           + parsers.colon
-                           * (parsers.dash
-                             - #(parsers.dash
-                                * (parsers.spacechar
-                                  + parsers.table_hline_separator
-                                  + parsers.newline)))^1
-                           * (parsers.colon * Cc("c")
-                             + parsers.dash * Cc("l"))
-parsers.table_hline = pipe_table_row(false
-                                    , parsers.table_hline_column
-                                    , parsers.table_hline_separator
-                                    , parsers.table_hline_column)
-parsers.table_caption_beginning = parsers.skipblanklines
-                                * parsers.nonindentspace
-                                * (P("Table")^-1 * parsers.colon)
-                                * parsers.optionalspace
 -- case-insensitive match (we assume s is lowercase). must be single byte encoding
 parsers.keyword_exact = function(s)
   local parser = P(0)
@@ -3478,49 +3068,11 @@
 end
 
 parsers.urlchar      = parsers.anyescaped - parsers.newline - parsers.more
-parsers.OnlineImageURL
-                     = parsers.leader
-                     * parsers.onlineimageurl
-                     * parsers.optionaltitle
-
-parsers.LocalFilePath
-                     = parsers.leader
-                     * parsers.localfilepath
-                     * parsers.optionaltitle
-
-parsers.TildeFencedCode
-                     = parsers.fencehead(parsers.tilde)
-                     * Cs(parsers.fencedline(parsers.tilde)^0)
-                     * parsers.fencetail(parsers.tilde)
-
-parsers.BacktickFencedCode
-                     = parsers.fencehead(parsers.backtick)
-                     * Cs(parsers.fencedline(parsers.backtick)^0)
-                     * parsers.fencetail(parsers.backtick)
-
-parsers.JekyllFencedCode
-                     = parsers.fencehead(parsers.dash)
-                     * Cs(parsers.fencedline(parsers.dash)^0)
-                     * parsers.fencetail(parsers.dash)
-
 parsers.lineof = function(c)
     return (parsers.leader * (P(c) * parsers.optionalspace)^3
            * (parsers.newline * parsers.blankline^1
              + parsers.newline^-1 * parsers.eof))
 end
-parsers.defstartchar = S("~:")
-parsers.defstart     = ( parsers.defstartchar * #parsers.spacing
-                                              * (parsers.tab + parsers.space^-3)
-                     + parsers.space * parsers.defstartchar * #parsers.spacing
-                                     * (parsers.tab + parsers.space^-2)
-                     + parsers.space * parsers.space * parsers.defstartchar
-                                     * #parsers.spacing
-                                     * (parsers.tab + parsers.space^-1)
-                     + parsers.space * parsers.space * parsers.space
-                                     * parsers.defstartchar * #parsers.spacing
-                     )
-
-parsers.dlchunk = Cs(parsers.line * (parsers.indentedline - parsers.blankline)^0)
 parsers.heading_attribute = C(parsers.css_identifier)
                           + C((parsers.attribute_key_char
                              - parsers.rbrace)^1
@@ -3544,12 +3096,19 @@
   return s:gsub("[#%s]*\n$","")
 end
 M.reader = {}
-function M.reader.new(writer, options)
+function M.reader.new(writer, options, extensions)
   local self = {}
-  options = options or {}
-  setmetatable(options, { __index = function (_, key)
-    return defaultOptions[key] end })
-  local function normalize_tag(tag)
+  self.writer = writer
+  self.parsers = {}
+  (function(parsers)
+    setmetatable(self.parsers, {
+      __index = function (_, key)
+        return parsers[key]
+      end
+    })
+  end)(parsers)
+  local parsers = self.parsers
+  function self.normalize_tag(tag)
     return string.lower(
       gsub(util.rope_to_string(tag), "[ \n\r\t]+", " "))
   end
@@ -3557,21 +3116,20 @@
     rope = lpeg.match(Ct((parsers.line / f)^1), s)
     return util.rope_to_string(rope)
   end
-  local expandtabs
   if options.preserveTabs then
-    expandtabs = function(s) return s end
+    self.expandtabs = function(s) return s end
   else
-    expandtabs = function(s)
-                   if s:find("\t") then
-                     return iterlines(s, util.expand_tabs_in_line)
-                   else
-                     return s
-                   end
-                 end
+    self.expandtabs = function(s)
+                        if s:find("\t") then
+                          return iterlines(s, util.expand_tabs_in_line)
+                        else
+                          return s
+                        end
+                      end
   end
-  local larsers    = {}
-  local function create_parser(name, grammar, toplevel)
-    return function(str)
+  self.parser_functions = {}
+  self.create_parser = function(name, grammar, toplevel)
+    self.parser_functions[name] = function(str)
       if toplevel and options.stripIndent then
           local min_prefix_length, min_prefix = nil, ''
           str = iterlines(str, function(line)
@@ -3603,67 +3161,60 @@
     end
   end
 
-  local parse_blocks
-    = create_parser("parse_blocks",
-                    function()
-                      return larsers.blocks
-                    end, true)
+  self.create_parser("parse_blocks",
+                     function()
+                       return parsers.blocks
+                     end, true)
 
-  local parse_blocks_nested
-    = create_parser("parse_blocks_nested",
-                    function()
-                      return larsers.blocks_nested
-                    end, false)
+  self.create_parser("parse_blocks_nested",
+                     function()
+                       return parsers.blocks_nested
+                     end, false)
 
-  local parse_inlines
-    = create_parser("parse_inlines",
-                    function()
-                      return larsers.inlines
-                    end, false)
+  self.create_parser("parse_inlines",
+                     function()
+                       return parsers.inlines
+                     end, false)
 
-  local parse_inlines_no_link
-    = create_parser("parse_inlines_no_link",
-                    function()
-                      return larsers.inlines_no_link
-                    end, false)
+  self.create_parser("parse_inlines_no_link",
+                     function()
+                       return parsers.inlines_no_link
+                     end, false)
 
-  local parse_inlines_no_inline_note
-    = create_parser("parse_inlines_no_inline_note",
-                    function()
-                      return larsers.inlines_no_inline_note
-                    end, false)
+  self.create_parser("parse_inlines_no_inline_note",
+                     function()
+                       return parsers.inlines_no_inline_note
+                     end, false)
 
-  local parse_inlines_no_html
-    = create_parser("parse_inlines_no_html",
-                    function()
-                      return larsers.inlines_no_html
-                    end, false)
+  self.create_parser("parse_inlines_no_html",
+                     function()
+                       return parsers.inlines_no_html
+                     end, false)
 
-  local parse_inlines_nbsp
-    = create_parser("parse_inlines_nbsp",
-                    function()
-                      return larsers.inlines_nbsp
-                    end, false)
+  self.create_parser("parse_inlines_nbsp",
+                     function()
+                       return parsers.inlines_nbsp
+                     end, false)
   if options.hashEnumerators then
-    larsers.dig = parsers.digit + parsers.hash
+    parsers.dig = parsers.digit + parsers.hash
   else
-    larsers.dig = parsers.digit
+    parsers.dig = parsers.digit
   end
 
-  larsers.enumerator = C(larsers.dig^3 * parsers.period) * #parsers.spacing
-                     + C(larsers.dig^2 * parsers.period) * #parsers.spacing
+  parsers.enumerator = C(parsers.dig^3 * parsers.period) * #parsers.spacing
+                     + C(parsers.dig^2 * parsers.period) * #parsers.spacing
                                        * (parsers.tab + parsers.space^1)
-                     + C(larsers.dig * parsers.period) * #parsers.spacing
+                     + C(parsers.dig * parsers.period) * #parsers.spacing
                                      * (parsers.tab + parsers.space^-2)
-                     + parsers.space * C(larsers.dig^2 * parsers.period)
+                     + parsers.space * C(parsers.dig^2 * parsers.period)
                                      * #parsers.spacing
-                     + parsers.space * C(larsers.dig * parsers.period)
+                     + parsers.space * C(parsers.dig * parsers.period)
                                      * #parsers.spacing
                                      * (parsers.tab + parsers.space^-1)
-                     + parsers.space * parsers.space * C(larsers.dig^1
+                     + parsers.space * parsers.space * C(parsers.dig^1
                                      * parsers.period) * #parsers.spacing
   -- strip off leading > and indents, and run through blocks
-  larsers.blockquote_body = ((parsers.leader * parsers.more * parsers.space^-1)/""
+  parsers.blockquote_body = ((parsers.leader * parsers.more * parsers.space^-1)/""
                              * parsers.linechar^0 * parsers.newline)^1
                             * (-(parsers.leader * parsers.more
                                 + parsers.blankline) * parsers.linechar^1
@@ -3670,87 +3221,15 @@
                               * parsers.newline)^0
 
   if not options.breakableBlockquotes then
-    larsers.blockquote_body = larsers.blockquote_body
+    parsers.blockquote_body = parsers.blockquote_body
                             * (parsers.blankline^0 / "")
   end
-  larsers.citations = function(text_cites, raw_cites)
-      local function normalize(str)
-          if str == "" then
-              str = nil
-          else
-              str = (options.citationNbsps and parse_inlines_nbsp or
-                parse_inlines)(str)
-          end
-          return str
-      end
-
-      local cites = {}
-      for i = 1,#raw_cites,4 do
-          cites[#cites+1] = {
-              prenote = normalize(raw_cites[i]),
-              suppress_author = raw_cites[i+1] == "-",
-              name = writer.citation(raw_cites[i+2]),
-              postnote = normalize(raw_cites[i+3]),
-          }
-      end
-      return writer.citations(text_cites, cites)
-  end
-  local rawnotes = {}
-
-  -- like indirect_link
-  local function lookup_note(ref)
-    return writer.defer_call(function()
-      local found = rawnotes[normalize_tag(ref)]
-      if found then
-        return writer.note(parse_blocks_nested(found))
-      else
-        return {"[", parse_inlines("^" .. ref), "]"}
-      end
-    end)
-  end
-
-  local function register_note(ref,rawnote)
-    rawnotes[normalize_tag(ref)] = rawnote
-    return ""
-  end
-
-  larsers.NoteRef    = parsers.RawNoteRef / lookup_note
-
-  larsers.NoteBlock  = parsers.leader * parsers.RawNoteRef * parsers.colon
-                     * parsers.spnl * parsers.indented_blocks(parsers.chunk)
-                     / register_note
-
-  larsers.InlineNote = parsers.circumflex
-                     * (parsers.tag / parse_inlines_no_inline_note) -- no notes inside notes
-                     / writer.note
-larsers.table_row = pipe_table_row(true
-                                  , (C((parsers.linechar - parsers.pipe)^1)
-                                    / parse_inlines)
-                                  , parsers.pipe
-                                  , (C((parsers.linechar - parsers.pipe)^0)
-                                    / parse_inlines))
-
-if options.tableCaptions then
-  larsers.table_caption = #parsers.table_caption_beginning
-                        * parsers.table_caption_beginning
-                        * Ct(parsers.IndentedInline^1)
-                        * parsers.newline
-else
-  larsers.table_caption = parsers.fail
-end
-
-larsers.PipeTable = Ct(larsers.table_row * parsers.newline
-                    * parsers.table_hline
-                    * (parsers.newline * larsers.table_row)^0)
-                  / make_pipe_table_rectangular
-                  * larsers.table_caption^-1
-                  / writer.table
   -- List of references defined in the document
   local references
 
   -- add a reference to the list
   local function register_link(tag,url,title)
-      references[normalize_tag(tag)] = { url = url, title = title }
+      references[self.normalize_tag(tag)] = { url = url, title = title }
       return ""
   end
 
@@ -3765,16 +3244,20 @@
           tag = label
           tagpart = "[]"
       else
-          tagpart = {"[", parse_inlines(tag), "]"}
+          tagpart = {"[",
+            self.parser_functions.parse_inlines(tag),
+            "]"}
       end
       if sps then
         tagpart = {sps, tagpart}
       end
-      local r = references[normalize_tag(tag)]
+      local r = references[self.normalize_tag(tag)]
       if r then
         return r
       else
-        return nil, {"[", parse_inlines(label), "]", tagpart}
+        return nil, {"[",
+          self.parser_functions.parse_inlines(label),
+          "]", tagpart}
       end
   end
 
@@ -3784,7 +3267,9 @@
     return writer.defer_call(function()
       local r,fallback = lookup_reference(label,sps,tag)
       if r then
-        return writer.link(parse_inlines_no_link(label), r.url, r.title)
+        return writer.link(
+          self.parser_functions.parse_inlines_no_link(label),
+          r.url, r.title)
       else
         return fallback
       end
@@ -3803,56 +3288,51 @@
       end
     end)
   end
-  larsers.Str      = (parsers.normalchar * (parsers.normalchar + parsers.at)^0)
+  parsers.Str      = (parsers.normalchar * (parsers.normalchar + parsers.at)^0)
                    / writer.string
 
-  larsers.Symbol   = (parsers.specialchar - parsers.tightblocksep)
+  parsers.Symbol   = (parsers.specialchar - parsers.tightblocksep)
                    / writer.string
 
-  larsers.Ellipsis = P("...") / writer.ellipsis
+  parsers.Ellipsis = P("...") / writer.ellipsis
 
-  larsers.Smart    = larsers.Ellipsis
+  parsers.Smart    = parsers.Ellipsis
 
-  larsers.Code     = parsers.inticks / writer.code
+  parsers.Code     = parsers.inticks / writer.code
 
   if options.blankBeforeBlockquote then
-    larsers.bqstart = parsers.fail
+    parsers.bqstart = parsers.fail
   else
-    larsers.bqstart = parsers.more
+    parsers.bqstart = parsers.more
   end
 
   if options.blankBeforeHeading then
-    larsers.headerstart = parsers.fail
+    parsers.headerstart = parsers.fail
   else
-    larsers.headerstart = parsers.hash
+    parsers.headerstart = parsers.hash
                         + (parsers.line * (parsers.equal^1 + parsers.dash^1)
                         * parsers.optionalspace * parsers.newline)
   end
 
-  if not options.fencedCode or options.blankBeforeCodeFence then
-    larsers.fencestart = parsers.fail
-  else
-    larsers.fencestart = parsers.fencehead(parsers.backtick)
-                       + parsers.fencehead(parsers.tilde)
-  end
+  parsers.EndlineExceptions
+                     = parsers.blankline -- paragraph break
+                     + parsers.tightblocksep  -- nested list
+                     + parsers.eof       -- end of document
+                     + parsers.bqstart
+                     + parsers.headerstart
 
-  larsers.Endline   = parsers.newline * -( -- newline, but not before...
-                        parsers.blankline -- paragraph break
-                      + parsers.tightblocksep  -- nested list
-                      + parsers.eof       -- end of document
-                      + larsers.bqstart
-                      + larsers.headerstart
-                      + larsers.fencestart
-                    ) * parsers.spacechar^0
+  parsers.Endline   = parsers.newline
+                    * -V("EndlineExceptions")
+                    * parsers.spacechar^0
                     / (options.hardLineBreaks and writer.linebreak
                                                or writer.space)
 
-  larsers.OptionalIndent
+  parsers.OptionalIndent
                      = parsers.spacechar^1 / writer.space
 
-  larsers.Space      = parsers.spacechar^2 * larsers.Endline / writer.linebreak
-                     + parsers.spacechar^1 * larsers.Endline^-1 * parsers.eof / ""
-                     + parsers.spacechar^1 * larsers.Endline
+  parsers.Space      = parsers.spacechar^2 * parsers.Endline / writer.linebreak
+                     + parsers.spacechar^1 * parsers.Endline^-1 * parsers.eof / ""
+                     + parsers.spacechar^1 * parsers.Endline
                                            * parsers.optionalspace
                                            / (options.hardLineBreaks
                                               and writer.linebreak
@@ -3860,22 +3340,17 @@
                      + parsers.spacechar^1 * parsers.optionalspace
                                            / writer.space
 
-  larsers.NonbreakingEndline
-                    = parsers.newline * -( -- newline, but not before...
-                        parsers.blankline -- paragraph break
-                      + parsers.tightblocksep  -- nested list
-                      + parsers.eof       -- end of document
-                      + larsers.bqstart
-                      + larsers.headerstart
-                      + larsers.fencestart
-                    ) * parsers.spacechar^0
+  parsers.NonbreakingEndline
+                    = parsers.newline
+                    * -V("EndlineExceptions")
+                    * parsers.spacechar^0
                     / (options.hardLineBreaks and writer.linebreak
                                                or writer.nbsp)
 
-  larsers.NonbreakingSpace
-                  = parsers.spacechar^2 * larsers.Endline / writer.linebreak
-                  + parsers.spacechar^1 * larsers.Endline^-1 * parsers.eof / ""
-                  + parsers.spacechar^1 * larsers.Endline
+  parsers.NonbreakingSpace
+                  = parsers.spacechar^2 * parsers.Endline / writer.linebreak
+                  + parsers.spacechar^1 * parsers.Endline^-1 * parsers.eof / ""
+                  + parsers.spacechar^1 * parsers.Endline
                                         * parsers.optionalspace
                                         / (options.hardLineBreaks
                                            and writer.linebreak
@@ -3884,28 +3359,28 @@
                                         / writer.nbsp
 
   if options.underscores then
-    larsers.Strong = ( parsers.between(parsers.Inline, parsers.doubleasterisks,
+    parsers.Strong = ( parsers.between(parsers.Inline, parsers.doubleasterisks,
                                        parsers.doubleasterisks)
                      + parsers.between(parsers.Inline, parsers.doubleunderscores,
                                        parsers.doubleunderscores)
                      ) / writer.strong
 
-    larsers.Emph   = ( parsers.between(parsers.Inline, parsers.asterisk,
+    parsers.Emph   = ( parsers.between(parsers.Inline, parsers.asterisk,
                                        parsers.asterisk)
                      + parsers.between(parsers.Inline, parsers.underscore,
                                        parsers.underscore)
                      ) / writer.emphasis
   else
-    larsers.Strong = ( parsers.between(parsers.Inline, parsers.doubleasterisks,
+    parsers.Strong = ( parsers.between(parsers.Inline, parsers.doubleasterisks,
                                        parsers.doubleasterisks)
                      ) / writer.strong
 
-    larsers.Emph   = ( parsers.between(parsers.Inline, parsers.asterisk,
+    parsers.Emph   = ( parsers.between(parsers.Inline, parsers.asterisk,
                                        parsers.asterisk)
                      ) / writer.emphasis
   end
 
-  larsers.AutoLinkUrl    = parsers.less
+  parsers.AutoLinkUrl    = parsers.less
                          * C(parsers.alphanumeric^1 * P("://") * parsers.urlchar^1)
                          * parsers.more
                          / function(url)
@@ -3912,7 +3387,7 @@
                              return writer.link(writer.escape(url), url)
                            end
 
-  larsers.AutoLinkEmail = parsers.less
+  parsers.AutoLinkEmail = parsers.less
                         * C((parsers.alphanumeric + S("-._+"))^1
                         * P("@") * parsers.urlchar^1)
                         * parsers.more
@@ -3921,7 +3396,7 @@
                                                "mailto:"..email)
                           end
 
-  larsers.AutoLinkRelativeReference
+  parsers.AutoLinkRelativeReference
                          = parsers.less
                          * C(parsers.urlchar^1)
                          * parsers.more
@@ -3929,7 +3404,7 @@
                              return writer.link(writer.escape(url), url)
                            end
 
-  larsers.DirectLink    = (parsers.tag / parse_inlines_no_link)  -- no links inside links
+  parsers.DirectLink    = (parsers.tag / self.parser_functions.parse_inlines_no_link)
                         * parsers.spnl
                         * parsers.lparent
                         * (parsers.url + Cc(""))  -- link can be empty [foo]()
@@ -3937,14 +3412,14 @@
                         * parsers.rparent
                         / writer.link
 
-  larsers.IndirectLink  = parsers.tag * (C(parsers.spnl) * parsers.tag)^-1
+  parsers.IndirectLink  = parsers.tag * (C(parsers.spnl) * parsers.tag)^-1
                         / indirect_link
 
   -- parse a link or image (direct or indirect)
-  larsers.Link          = larsers.DirectLink + larsers.IndirectLink
+  parsers.Link          = parsers.DirectLink + parsers.IndirectLink
 
-  larsers.DirectImage   = parsers.exclamation
-                        * (parsers.tag / parse_inlines)
+  parsers.DirectImage   = parsers.exclamation
+                        * (parsers.tag / self.parser_functions.parse_inlines)
                         * parsers.spnl
                         * parsers.lparent
                         * (parsers.url + Cc(""))  -- link can be empty [foo]()
@@ -3952,55 +3427,28 @@
                         * parsers.rparent
                         / writer.image
 
-  larsers.IndirectImage = parsers.exclamation * parsers.tag
+  parsers.IndirectImage = parsers.exclamation * parsers.tag
                         * (C(parsers.spnl) * parsers.tag)^-1 / indirect_image
 
-  larsers.Image         = larsers.DirectImage + larsers.IndirectImage
+  parsers.Image         = parsers.DirectImage + parsers.IndirectImage
 
-  larsers.TextCitations = Ct((parsers.spnl
-                        * Cc("")
-                        * parsers.citation_name
-                        * ((parsers.spnl
-                            * parsers.lbracket
-                            * parsers.citation_headless_body
-                            * parsers.rbracket) + Cc("")))^1)
-                        / function(raw_cites)
-                            return larsers.citations(true, raw_cites)
-                          end
-
-  larsers.ParenthesizedCitations
-                        = Ct((parsers.spnl
-                        * parsers.lbracket
-                        * parsers.citation_body
-                        * parsers.rbracket)^1)
-                        / function(raw_cites)
-                            return larsers.citations(false, raw_cites)
-                          end
-
-  larsers.Citations     = larsers.TextCitations + larsers.ParenthesizedCitations
-
   -- avoid parsing long strings of * or _ as emph/strong
-  larsers.UlOrStarLine  = parsers.asterisk^4 + parsers.underscore^4
+  parsers.UlOrStarLine  = parsers.asterisk^4 + parsers.underscore^4
                         / writer.string
 
-  larsers.EscapedChar   = parsers.backslash * C(parsers.escapable) / writer.string
+  parsers.EscapedChar   = parsers.backslash * C(parsers.escapable) / writer.string
 
-  larsers.InlineHtml    = parsers.emptyelt_any / writer.inline_html_tag
-                        + (parsers.htmlcomment / parse_inlines_no_html)
+  parsers.InlineHtml    = parsers.emptyelt_any / writer.inline_html_tag
+                        + (parsers.htmlcomment / self.parser_functions.parse_inlines_no_html)
                         / writer.inline_html_comment
                         + parsers.htmlinstruction
                         + parsers.openelt_any / writer.inline_html_tag
                         + parsers.closeelt_any / writer.inline_html_tag
 
-  larsers.HtmlEntity    = parsers.hexentity / entities.hex_entity  / writer.string
+  parsers.HtmlEntity    = parsers.hexentity / entities.hex_entity  / writer.string
                         + parsers.decentity / entities.dec_entity  / writer.string
                         + parsers.tagentity / entities.char_entity / writer.string
-  larsers.ContentBlock = parsers.leader
-                       * (parsers.localfilepath + parsers.onlineimageurl)
-                       * parsers.contentblock_tail
-                       / writer.contentblock
-
-  larsers.DisplayHtml  = (parsers.htmlcomment / parse_blocks_nested)
+  parsers.DisplayHtml  = (parsers.htmlcomment / self.parser_functions.parse_blocks_nested)
                        / writer.block_html_comment
                        + parsers.emptyelt_block / writer.block_html_element
                        + parsers.openelt_exact("hr") / writer.block_html_element
@@ -4007,60 +3455,22 @@
                        + parsers.in_matched_block_tags / writer.block_html_element
                        + parsers.htmlinstruction
 
-  larsers.Verbatim     = Cs( (parsers.blanklines
+  parsers.Verbatim     = Cs( (parsers.blanklines
                            * ((parsers.indentedline - parsers.blankline))^1)^1
-                           ) / expandtabs / writer.verbatim
+                           ) / self.expandtabs / writer.verbatim
 
-  larsers.FencedCode   = (parsers.TildeFencedCode
-                         + parsers.BacktickFencedCode)
-                       / function(infostring, code)
-                           return writer.fencedCode(writer.string(infostring),
-                                                    expandtabs(code))
-                         end
+  parsers.Blockquote   = Cs(parsers.blockquote_body^1)
+                       / self.parser_functions.parse_blocks_nested
+                       / writer.blockquote
 
-  larsers.JekyllData   = Cmt( C((parsers.line - P("---") - P("..."))^0)
-                            , function(s, i, text)
-                                local data
-                                local ran_ok, error = pcall(function()
-                                  local tinyyaml = require("markdown-tinyyaml")
-                                  data = tinyyaml.parse(text, {timestamps=false})
-                                end)
-                                if ran_ok and data ~= nil then
-                                  return true, writer.jekyllData(data, function(s)
-                                    return parse_blocks_nested(s)
-                                  end, nil)
-                                else
-                                  return false
-                                end
-                              end
-                            )
-
-  larsers.UnexpectedJekyllData
-                       = P("---")
-                       * parsers.blankline / 0
-                       * #(-parsers.blankline)  -- if followed by blank, it's an hrule
-                       * larsers.JekyllData
-                       * (P("---") + P("..."))
-
-  larsers.ExpectedJekyllData
-                       = ( P("---")
-                         * parsers.blankline / 0
-                         * #(-parsers.blankline)  -- if followed by blank, it's an hrule
-                         )^-1
-                       * larsers.JekyllData
-                       * (P("---") + P("..."))^-1
-
-  larsers.Blockquote   = Cs(larsers.blockquote_body^1)
-                       / parse_blocks_nested / writer.blockquote
-
-  larsers.HorizontalRule = ( parsers.lineof(parsers.asterisk)
+  parsers.HorizontalRule = ( parsers.lineof(parsers.asterisk)
                            + parsers.lineof(parsers.dash)
                            + parsers.lineof(parsers.underscore)
                            ) / writer.hrule
 
-  larsers.Reference    = parsers.define_reference_parser / register_link
+  parsers.Reference    = parsers.define_reference_parser / register_link
 
-  larsers.Paragraph    = parsers.nonindentspace * Ct(parsers.Inline^1)
+  parsers.Paragraph    = parsers.nonindentspace * Ct(parsers.Inline^1)
                        * ( parsers.newline
                        * ( parsers.blankline^1
                          + #parsers.hash
@@ -4070,52 +3480,52 @@
                        + parsers.eof )
                        / writer.paragraph
 
-  larsers.Plain        = parsers.nonindentspace * Ct(parsers.Inline^1)
+  parsers.Plain        = parsers.nonindentspace * Ct(parsers.Inline^1)
                        / writer.plain
-  larsers.starter = parsers.bullet + larsers.enumerator
+  parsers.starter = parsers.bullet + parsers.enumerator
 
   if options.taskLists then
-    larsers.tickbox = ( parsers.ticked_box
+    parsers.tickbox = ( parsers.ticked_box
                       + parsers.halfticked_box
                       + parsers.unticked_box
                       ) / writer.tickbox
   else
-     larsers.tickbox = parsers.fail
+     parsers.tickbox = parsers.fail
   end
 
   -- we use \001 as a separator between a tight list item and a
   -- nested list under it.
-  larsers.NestedList            = Cs((parsers.optionallyindentedline
-                                     - larsers.starter)^1)
+  parsers.NestedList            = Cs((parsers.optionallyindentedline
+                                     - parsers.starter)^1)
                                 / function(a) return "\001"..a end
 
-  larsers.ListBlockLine         = parsers.optionallyindentedline
+  parsers.ListBlockLine         = parsers.optionallyindentedline
                                 - parsers.blankline - (parsers.indent^-1
-                                                      * larsers.starter)
+                                                      * parsers.starter)
 
-  larsers.ListBlock             = parsers.line * larsers.ListBlockLine^0
+  parsers.ListBlock             = parsers.line * parsers.ListBlockLine^0
 
-  larsers.ListContinuationBlock = parsers.blanklines * (parsers.indent / "")
-                                * larsers.ListBlock
+  parsers.ListContinuationBlock = parsers.blanklines * (parsers.indent / "")
+                                * parsers.ListBlock
 
-  larsers.TightListItem = function(starter)
-      return -larsers.HorizontalRule
-             * (Cs(starter / "" * larsers.tickbox^-1 * larsers.ListBlock * larsers.NestedList^-1)
-               / parse_blocks_nested)
+  parsers.TightListItem = function(starter)
+      return -parsers.HorizontalRule
+             * (Cs(starter / "" * parsers.tickbox^-1 * parsers.ListBlock * parsers.NestedList^-1)
+               / self.parser_functions.parse_blocks_nested)
              * -(parsers.blanklines * parsers.indent)
   end
 
-  larsers.LooseListItem = function(starter)
-      return -larsers.HorizontalRule
-             * Cs( starter / "" * larsers.tickbox^-1 * larsers.ListBlock * Cc("\n")
-               * (larsers.NestedList + larsers.ListContinuationBlock^0)
+  parsers.LooseListItem = function(starter)
+      return -parsers.HorizontalRule
+             * Cs( starter / "" * parsers.tickbox^-1 * parsers.ListBlock * Cc("\n")
+               * (parsers.NestedList + parsers.ListContinuationBlock^0)
                * (parsers.blanklines / "\n\n")
-               ) / parse_blocks_nested
+               ) / self.parser_functions.parse_blocks_nested
   end
 
-  larsers.BulletList = ( Ct(larsers.TightListItem(parsers.bullet)^1) * Cc(true)
+  parsers.BulletList = ( Ct(parsers.TightListItem(parsers.bullet)^1) * Cc(true)
                        * parsers.skipblanklines * -parsers.bullet
-                       + Ct(larsers.LooseListItem(parsers.bullet)^1) * Cc(false)
+                       + Ct(parsers.LooseListItem(parsers.bullet)^1) * Cc(false)
                        * parsers.skipblanklines )
                      / writer.bulletlist
 
@@ -4131,100 +3541,37 @@
     return writer.orderedlist(items,tight,startNumber)
   end
 
-  larsers.OrderedList = Cg(larsers.enumerator, "listtype") *
-                      ( Ct(larsers.TightListItem(Cb("listtype"))
-                          * larsers.TightListItem(larsers.enumerator)^0)
-                      * Cc(true) * parsers.skipblanklines * -larsers.enumerator
-                      + Ct(larsers.LooseListItem(Cb("listtype"))
-                          * larsers.LooseListItem(larsers.enumerator)^0)
+  parsers.OrderedList = Cg(parsers.enumerator, "listtype") *
+                      ( Ct(parsers.TightListItem(Cb("listtype"))
+                          * parsers.TightListItem(parsers.enumerator)^0)
+                      * Cc(true) * parsers.skipblanklines * -parsers.enumerator
+                      + Ct(parsers.LooseListItem(Cb("listtype"))
+                          * parsers.LooseListItem(parsers.enumerator)^0)
                       * Cc(false) * parsers.skipblanklines
                       ) * Cb("listtype") / ordered_list
-
-  local function definition_list_item(term, defs, tight)
-    return { term = parse_inlines(term), definitions = defs }
-  end
-
-  larsers.DefinitionListItemLoose = C(parsers.line) * parsers.skipblanklines
-                                  * Ct((parsers.defstart
-                                       * parsers.indented_blocks(parsers.dlchunk)
-                                       / parse_blocks_nested)^1)
-                                  * Cc(false) / definition_list_item
-
-  larsers.DefinitionListItemTight = C(parsers.line)
-                                  * Ct((parsers.defstart * parsers.dlchunk
-                                       / parse_blocks_nested)^1)
-                                  * Cc(true) / definition_list_item
-
-  larsers.DefinitionList = ( Ct(larsers.DefinitionListItemLoose^1) * Cc(false)
-                           + Ct(larsers.DefinitionListItemTight^1)
-                           * (parsers.skipblanklines
-                             * -larsers.DefinitionListItemLoose * Cc(true))
-                           ) / writer.definitionlist
-  larsers.Blank        = parsers.blankline / ""
-                       + larsers.NoteBlock
-                       + larsers.Reference
+  parsers.Blank        = parsers.blankline / ""
+                       + parsers.Reference
                        + (parsers.tightblocksep / "\n")
   -- parse atx header
-  if options.headerAttributes then
-    larsers.AtxHeading = Cg(parsers.HeadingStart,"level")
-                       * parsers.optionalspace
-                       * (C(((parsers.linechar
-                             - ((parsers.hash^1
-                                * parsers.optionalspace
-                                * parsers.HeadingAttributes^-1
-                                + parsers.HeadingAttributes)
-                               * parsers.optionalspace
-                               * parsers.newline))
-                            * (parsers.linechar
-                              - parsers.hash
-                              - parsers.lbrace)^0)^1)
-                           / parse_inlines)
-                       * Cg(Ct(parsers.newline
-                              + (parsers.hash^1
-                                * parsers.optionalspace
-                                * parsers.HeadingAttributes^-1
-                                + parsers.HeadingAttributes)
-                              * parsers.optionalspace
-                              * parsers.newline), "attributes")
-                       * Cb("level")
-                       * Cb("attributes")
-                       / writer.heading
+  parsers.AtxHeading = Cg(parsers.HeadingStart,"level")
+                     * parsers.optionalspace
+                     * (C(parsers.line)
+                       / strip_atx_end
+                       / self.parser_functions.parse_inlines)
+                     * Cb("level")
+                     / writer.heading
 
-    larsers.SetextHeading = #(parsers.line * S("=-"))
-                          * (C(((parsers.linechar
-                                - (parsers.HeadingAttributes
-                                  * parsers.optionalspace
-                                  * parsers.newline))
-                               * (parsers.linechar
-                                 - parsers.lbrace)^0)^1)
-                              / parse_inlines)
-                          * Cg(Ct(parsers.newline
-                                 + (parsers.HeadingAttributes
-                                   * parsers.optionalspace
-                                   * parsers.newline)), "attributes")
-                          * parsers.HeadingLevel
-                          * Cb("attributes")
-                          * parsers.optionalspace
-                          * parsers.newline
-                          / writer.heading
-  else
-    larsers.AtxHeading = Cg(parsers.HeadingStart,"level")
-                       * parsers.optionalspace
-                       * (C(parsers.line) / strip_atx_end / parse_inlines)
-                       * Cb("level")
-                       / writer.heading
+  parsers.SetextHeading = #(parsers.line * S("=-"))
+                        * Ct(parsers.linechar^1
+                            / self.parser_functions.parse_inlines)
+                        * parsers.newline
+                        * parsers.HeadingLevel
+                        * parsers.optionalspace
+                        * parsers.newline
+                        / writer.heading
 
-    larsers.SetextHeading = #(parsers.line * S("=-"))
-                          * Ct(parsers.linechar^1 / parse_inlines)
-                          * parsers.newline
-                          * parsers.HeadingLevel
-                          * parsers.optionalspace
-                          * parsers.newline
-                          / writer.heading
-  end
-
-  larsers.Heading = larsers.AtxHeading + larsers.SetextHeading
-  local syntax =
+  parsers.Heading = parsers.AtxHeading + parsers.SetextHeading
+  self.syntax =
     { "Blocks",
 
       Blocks                = ( V("ExpectedJekyllData")
@@ -4236,10 +3583,10 @@
                               * V("Block"))^0
                             * V("Blank")^0 * parsers.eof,
 
-      Blank                 = larsers.Blank,
+      Blank                 = parsers.Blank,
 
-      UnexpectedJekyllData  = larsers.UnexpectedJekyllData,
-      ExpectedJekyllData    = larsers.ExpectedJekyllData,
+      UnexpectedJekyllData  = parsers.fail,
+      ExpectedJekyllData    = parsers.fail,
 
       Block                 = V("ContentBlock")
                             + V("UnexpectedJekyllData")
@@ -4256,19 +3603,20 @@
                             + V("Paragraph")
                             + V("Plain"),
 
-      ContentBlock          = larsers.ContentBlock,
-      Blockquote            = larsers.Blockquote,
-      Verbatim              = larsers.Verbatim,
-      FencedCode            = larsers.FencedCode,
-      HorizontalRule        = larsers.HorizontalRule,
-      BulletList            = larsers.BulletList,
-      OrderedList           = larsers.OrderedList,
-      Heading               = larsers.Heading,
-      DefinitionList        = larsers.DefinitionList,
-      DisplayHtml           = larsers.DisplayHtml,
-      Paragraph             = larsers.Paragraph,
-      PipeTable             = larsers.PipeTable,
-      Plain                 = larsers.Plain,
+      ContentBlock          = parsers.fail,
+      Blockquote            = parsers.Blockquote,
+      Verbatim              = parsers.Verbatim,
+      FencedCode            = parsers.fail,
+      HorizontalRule        = parsers.HorizontalRule,
+      BulletList            = parsers.BulletList,
+      OrderedList           = parsers.OrderedList,
+      Heading               = parsers.Heading,
+      DefinitionList        = parsers.fail,
+      DisplayHtml           = parsers.DisplayHtml,
+      Paragraph             = parsers.Paragraph,
+      PipeTable             = parsers.fail,
+      Plain                 = parsers.Plain,
+      EndlineExceptions     = parsers.EndlineExceptions,
 
       Inline                = V("Str")
                             + V("Space")
@@ -4312,117 +3660,86 @@
                             + V("Smart")
                             + V("Symbol"),
 
-      Str                   = larsers.Str,
-      Space                 = larsers.Space,
-      OptionalIndent        = larsers.OptionalIndent,
-      Endline               = larsers.Endline,
-      UlOrStarLine          = larsers.UlOrStarLine,
-      Strong                = larsers.Strong,
-      Emph                  = larsers.Emph,
-      InlineNote            = larsers.InlineNote,
-      NoteRef               = larsers.NoteRef,
-      Citations             = larsers.Citations,
-      Link                  = larsers.Link,
-      Image                 = larsers.Image,
-      Code                  = larsers.Code,
-      AutoLinkUrl           = larsers.AutoLinkUrl,
-      AutoLinkEmail         = larsers.AutoLinkEmail,
+      Str                   = parsers.Str,
+      Space                 = parsers.Space,
+      OptionalIndent        = parsers.OptionalIndent,
+      Endline               = parsers.Endline,
+      UlOrStarLine          = parsers.UlOrStarLine,
+      Strong                = parsers.Strong,
+      Emph                  = parsers.Emph,
+      InlineNote            = parsers.fail,
+      NoteRef               = parsers.fail,
+      Citations             = parsers.fail,
+      Link                  = parsers.Link,
+      Image                 = parsers.Image,
+      Code                  = parsers.Code,
+      AutoLinkUrl           = parsers.AutoLinkUrl,
+      AutoLinkEmail         = parsers.AutoLinkEmail,
       AutoLinkRelativeReference
-                            = larsers.AutoLinkRelativeReference,
-      InlineHtml            = larsers.InlineHtml,
-      HtmlEntity            = larsers.HtmlEntity,
-      EscapedChar           = larsers.EscapedChar,
-      Smart                 = larsers.Smart,
-      Symbol                = larsers.Symbol,
+                            = parsers.AutoLinkRelativeReference,
+      InlineHtml            = parsers.InlineHtml,
+      HtmlEntity            = parsers.HtmlEntity,
+      EscapedChar           = parsers.EscapedChar,
+      Smart                 = parsers.Smart,
+      Symbol                = parsers.Symbol,
     }
 
-  if not options.citations then
-    syntax.Citations = parsers.fail
+  for _, extension in ipairs(extensions) do
+    extension.extend_writer(writer)
+    extension.extend_reader(self)
   end
 
-  if not options.contentBlocks then
-    syntax.ContentBlock = parsers.fail
-  end
-
   if not options.codeSpans then
-    syntax.Code = parsers.fail
+    self.syntax.Code = parsers.fail
   end
 
-  if not options.definitionLists then
-    syntax.DefinitionList = parsers.fail
-  end
-
-  if not options.fencedCode then
-    syntax.FencedCode = parsers.fail
-  end
-
-  if not options.footnotes then
-    syntax.NoteRef = parsers.fail
-  end
-
   if not options.html then
-    syntax.DisplayHtml = parsers.fail
-    syntax.InlineHtml = parsers.fail
-    syntax.HtmlEntity  = parsers.fail
+    self.syntax.DisplayHtml = parsers.fail
+    self.syntax.InlineHtml = parsers.fail
+    self.syntax.HtmlEntity  = parsers.fail
   end
 
-  if not options.inlineFootnotes then
-    syntax.InlineNote = parsers.fail
-  end
-
-  if not options.jekyllData then
-    syntax.UnexpectedJekyllData = parsers.fail
-  end
-
-  if not options.jekyllData or not options.expectJekyllData then
-    syntax.ExpectedJekyllData = parsers.fail
-  end
-
   if options.preserveTabs then
     options.stripIndent = false
   end
 
-  if not options.pipeTables then
-    syntax.PipeTable = parsers.fail
-  end
-
   if not options.smartEllipses then
-    syntax.Smart = parsers.fail
+    self.syntax.Smart = parsers.fail
   end
 
   if not options.relativeReferences then
-    syntax.AutoLinkRelativeReference = parsers.fail
+    self.syntax.AutoLinkRelativeReference = parsers.fail
   end
 
-  local blocks_nested_t = util.table_copy(syntax)
+  local blocks_nested_t = util.table_copy(self.syntax)
   blocks_nested_t.ExpectedJekyllData = parsers.fail
-  larsers.blocks_nested = Ct(blocks_nested_t)
+  parsers.blocks_nested = Ct(blocks_nested_t)
 
-  larsers.blocks = Ct(syntax)
+  parsers.blocks = Ct(self.syntax)
 
-  local inlines_t = util.table_copy(syntax)
+  local inlines_t = util.table_copy(self.syntax)
   inlines_t[1] = "Inlines"
   inlines_t.Inlines = parsers.Inline^0 * (parsers.spacing^0 * parsers.eof / "")
-  larsers.inlines = Ct(inlines_t)
+  parsers.inlines = Ct(inlines_t)
 
   local inlines_no_link_t = util.table_copy(inlines_t)
   inlines_no_link_t.Link = parsers.fail
-  larsers.inlines_no_link = Ct(inlines_no_link_t)
+  parsers.inlines_no_link = Ct(inlines_no_link_t)
 
   local inlines_no_inline_note_t = util.table_copy(inlines_t)
   inlines_no_inline_note_t.InlineNote = parsers.fail
-  larsers.inlines_no_inline_note = Ct(inlines_no_inline_note_t)
+  parsers.inlines_no_inline_note = Ct(inlines_no_inline_note_t)
 
   local inlines_no_html_t = util.table_copy(inlines_t)
   inlines_no_html_t.DisplayHtml = parsers.fail
   inlines_no_html_t.InlineHtml = parsers.fail
   inlines_no_html_t.HtmlEntity = parsers.fail
-  larsers.inlines_no_html = Ct(inlines_no_html_t)
+  parsers.inlines_no_html = Ct(inlines_no_html_t)
 
   local inlines_nbsp_t = util.table_copy(inlines_t)
-  inlines_nbsp_t.Endline = larsers.NonbreakingEndline
-  inlines_nbsp_t.Space = larsers.NonbreakingSpace
-  larsers.inlines_nbsp = Ct(inlines_nbsp_t)
+  inlines_nbsp_t.Endline = parsers.NonbreakingEndline
+  inlines_nbsp_t.Space = parsers.NonbreakingSpace
+  parsers.inlines_nbsp = Ct(inlines_nbsp_t)
   function self.convert(input)
     references = {}
     local opt_string = {}
@@ -4436,7 +3753,7 @@
     local salt = table.concat(opt_string, ",") .. "," .. metadata.version
     local output
     local function convert(input)
-      local document = parse_blocks(input)
+      local document = self.parser_functions.parse_blocks(input)
       return util.rope_to_string(writer.document(document))
     end
     if options.eagerCache or options.finalizeCache then
@@ -4464,9 +3781,890 @@
   end
   return self
 end
+M.extensions = {}
+M.extensions.citations = function(citation_nbsps)
+  local escaped_citation_chars = {
+    ["{"] = "\\markdownRendererLeftBrace{}",
+    ["}"] = "\\markdownRendererRightBrace{}",
+    ["%"] = "\\markdownRendererPercentSign{}",
+    ["\\"] = "\\markdownRendererBackslash{}",
+    ["#"] = "\\markdownRendererHash{}",
+  }
+  return {
+    extend_writer = function(self)
+      local escape_citation = util.escaper(
+        escaped_citation_chars,
+        self.escaped_minimal_strings)
+      if self.hybrid then
+        self.citation = self.escape_minimal
+      else
+        self.citation = escape_citation
+      end
+      function self.citations(text_cites, cites)
+        local buffer = {"\\markdownRenderer", text_cites and "TextCite" or "Cite",
+          "{", #cites, "}"}
+        for _,cite in ipairs(cites) do
+          buffer[#buffer+1] = {cite.suppress_author and "-" or "+", "{",
+            cite.prenote or "", "}{", cite.postnote or "", "}{", cite.name, "}"}
+        end
+        return buffer
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local citation_chars
+                    = parsers.alphanumeric
+                    + S("#$%&-+<>~/_")
+
+      local citation_name
+                    = Cs(parsers.dash^-1) * parsers.at
+                    * Cs(citation_chars
+                        * (((citation_chars + parsers.internal_punctuation
+                            - parsers.comma - parsers.semicolon)
+                           * -#((parsers.internal_punctuation - parsers.comma
+                                - parsers.semicolon)^0
+                               * -(citation_chars + parsers.internal_punctuation
+                                  - parsers.comma - parsers.semicolon)))^0
+                          * citation_chars)^-1)
+
+      local citation_body_prenote
+                    = Cs((parsers.alphanumeric^1
+                         + parsers.bracketed
+                         + parsers.inticks
+                         + (parsers.anyescaped
+                           - (parsers.rbracket + parsers.blankline^2))
+                         - (parsers.spnl * parsers.dash^-1 * parsers.at))^0)
+
+      local citation_body_postnote
+                    = Cs((parsers.alphanumeric^1
+                         + parsers.bracketed
+                         + parsers.inticks
+                         + (parsers.anyescaped
+                           - (parsers.rbracket + parsers.semicolon
+                             + parsers.blankline^2))
+                         - (parsers.spnl * parsers.rbracket))^0)
+
+      local citation_body_chunk
+                    = citation_body_prenote
+                    * parsers.spnl * citation_name
+                    * (parsers.internal_punctuation - parsers.semicolon)^-1
+                    * parsers.spnl * citation_body_postnote
+
+      local citation_body
+                    = citation_body_chunk
+                    * (parsers.semicolon * parsers.spnl
+                      * citation_body_chunk)^0
+
+      local citation_headless_body_postnote
+                    = Cs((parsers.alphanumeric^1
+                         + parsers.bracketed
+                         + parsers.inticks
+                         + (parsers.anyescaped
+                           - (parsers.rbracket + parsers.at
+                             + parsers.semicolon + parsers.blankline^2))
+                         - (parsers.spnl * parsers.rbracket))^0)
+
+      local citation_headless_body
+                    = citation_headless_body_postnote
+                    * (parsers.sp * parsers.semicolon * parsers.spnl
+                      * citation_body_chunk)^0
+
+      local citations
+                    = function(text_cites, raw_cites)
+          local function normalize(str)
+              if str == "" then
+                  str = nil
+              else
+                  str = (citation_nbsps and
+                    self.parser_functions.parse_inlines_nbsp or
+                    self.parser_functions.parse_inlines)(str)
+              end
+              return str
+          end
+
+          local cites = {}
+          for i = 1,#raw_cites,4 do
+              cites[#cites+1] = {
+                  prenote = normalize(raw_cites[i]),
+                  suppress_author = raw_cites[i+1] == "-",
+                  name = writer.citation(raw_cites[i+2]),
+                  postnote = normalize(raw_cites[i+3]),
+              }
+          end
+          return writer.citations(text_cites, cites)
+      end
+
+      local TextCitations
+                    = Ct((parsers.spnl
+                    * Cc("")
+                    * citation_name
+                    * ((parsers.spnl
+                        * parsers.lbracket
+                        * citation_headless_body
+                        * parsers.rbracket) + Cc("")))^1)
+                    / function(raw_cites)
+                        return citations(true, raw_cites)
+                      end
+
+      local ParenthesizedCitations
+                    = Ct((parsers.spnl
+                    * parsers.lbracket
+                    * citation_body
+                    * parsers.rbracket)^1)
+                    / function(raw_cites)
+                        return citations(false, raw_cites)
+                      end
+
+      local Citations = TextCitations + ParenthesizedCitations
+
+      syntax.Citations = Citations
+    end
+  }
+end
+M.extensions.content_blocks = function(language_map)
+  local languages_json = (function()
+    local ran_ok, kpse = pcall(require, "kpse")
+    if ran_ok then
+      kpse.set_program_name("luatex")
+    else
+      kpse = {lookup=function(filename, options) return filename end}
+    end
+    local base, prev, curr
+    for _, filename in ipairs{kpse.lookup(language_map, { all=true })} do
+      local file = io.open(filename, "r")
+      if not file then goto continue end
+      json = file:read("*all"):gsub('("[^\n]-"):','[%1]=')
+      curr = (function()
+        local _ENV={ json=json, load=load } -- run in sandbox
+        return load("return "..json)()
+      end)()
+      if type(curr) == "table" then
+        if base == nil then
+          base = curr
+        else
+          setmetatable(prev, { __index = curr })
+        end
+        prev = curr
+      end
+      ::continue::
+    end
+    return base or {}
+  end)()
+
+  return {
+    extend_writer = function(self)
+      function self.contentblock(src,suf,type,tit)
+        if not self.is_writing then return "" end
+        src = src.."."..suf
+        suf = suf:lower()
+        if type == "onlineimage" then
+          return {"\\markdownRendererContentBlockOnlineImage{",suf,"}",
+                                 "{",self.string(src),"}",
+                                 "{",self.uri(src),"}",
+                                 "{",self.string(tit or ""),"}"}
+        elseif languages_json[suf] then
+          return {"\\markdownRendererContentBlockCode{",suf,"}",
+                                 "{",self.string(languages_json[suf]),"}",
+                                 "{",self.string(src),"}",
+                                 "{",self.uri(src),"}",
+                                 "{",self.string(tit or ""),"}"}
+        else
+          return {"\\markdownRendererContentBlock{",suf,"}",
+                                 "{",self.string(src),"}",
+                                 "{",self.uri(src),"}",
+                                 "{",self.string(tit or ""),"}"}
+        end
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local contentblock_tail
+                    = parsers.optionaltitle
+                    * (parsers.newline + parsers.eof)
+
+      -- case insensitive online image suffix:
+      local onlineimagesuffix
+                    = (function(...)
+                        local parser = nil
+                        for _, suffix in ipairs({...}) do
+                          local pattern=nil
+                          for i=1,#suffix do
+                            local char=suffix:sub(i,i)
+                            char = S(char:lower()..char:upper())
+                            if pattern == nil then
+                              pattern = char
+                            else
+                              pattern = pattern * char
+                            end
+                          end
+                          if parser == nil then
+                            parser = pattern
+                          else
+                            parser = parser + pattern
+                          end
+                        end
+                        return parser
+                      end)("png", "jpg", "jpeg", "gif", "tif", "tiff")
+
+      -- online image url for iA Writer content blocks with mandatory suffix,
+      -- allowing nested brackets:
+      local onlineimageurl
+                    = (parsers.less
+                      * Cs((parsers.anyescaped
+                           - parsers.more
+                           - #(parsers.period
+                              * onlineimagesuffix
+                              * parsers.more
+                              * contentblock_tail))^0)
+                      * parsers.period
+                      * Cs(onlineimagesuffix)
+                      * parsers.more
+                      + (Cs((parsers.inparens
+                            + (parsers.anyescaped
+                              - parsers.spacing
+                              - parsers.rparent
+                              - #(parsers.period
+                                 * onlineimagesuffix
+                                 * contentblock_tail)))^0)
+                        * parsers.period
+                        * Cs(onlineimagesuffix))
+                      ) * Cc("onlineimage")
+
+      -- filename for iA Writer content blocks with mandatory suffix:
+      local localfilepath
+                    = parsers.slash
+                    * Cs((parsers.anyescaped
+                         - parsers.tab
+                         - parsers.newline
+                         - #(parsers.period
+                            * parsers.alphanumeric^1
+                            * contentblock_tail))^1)
+                    * parsers.period
+                    * Cs(parsers.alphanumeric^1)
+                    * Cc("localfile")
+
+      local ContentBlock
+                    = parsers.leader
+                    * (localfilepath + onlineimageurl)
+                    * contentblock_tail
+                    / writer.contentblock
+
+      syntax.ContentBlock = ContentBlock
+    end
+  }
+end
+M.extensions.definition_lists = function(tight_lists)
+  return {
+    extend_writer = function(self)
+      local function dlitem(term, defs)
+        local retVal = {"\\markdownRendererDlItem{",term,"}"}
+        for _, def in ipairs(defs) do
+          retVal[#retVal+1] = {"\\markdownRendererDlDefinitionBegin ",def,
+                               "\\markdownRendererDlDefinitionEnd "}
+        end
+        retVal[#retVal+1] = "\\markdownRendererDlItemEnd "
+        return retVal
+      end
+
+      function self.definitionlist(items,tight)
+        if not self.is_writing then return "" end
+        local buffer = {}
+        for _,item in ipairs(items) do
+          buffer[#buffer + 1] = dlitem(item.term, item.definitions)
+        end
+        if tight and tight_lists then
+          return {"\\markdownRendererDlBeginTight\n", buffer,
+            "\n\\markdownRendererDlEndTight"}
+        else
+          return {"\\markdownRendererDlBegin\n", buffer,
+            "\n\\markdownRendererDlEnd"}
+        end
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local defstartchar = S("~:")
+
+      local defstart = ( defstartchar * #parsers.spacing
+                                      * (parsers.tab + parsers.space^-3)
+                       + parsers.space * defstartchar * #parsers.spacing
+                                       * (parsers.tab + parsers.space^-2)
+                       + parsers.space * parsers.space * defstartchar
+                                       * #parsers.spacing
+                                       * (parsers.tab + parsers.space^-1)
+                       + parsers.space * parsers.space * parsers.space
+                                       * defstartchar * #parsers.spacing
+                       )
+
+      local dlchunk = Cs(parsers.line * (parsers.indentedline - parsers.blankline)^0)
+
+      local function definition_list_item(term, defs, tight)
+        return { term = self.parser_functions.parse_inlines(term),
+                 definitions = defs }
+      end
+
+      local DefinitionListItemLoose
+                    = C(parsers.line) * parsers.skipblanklines
+                    * Ct((defstart
+                         * parsers.indented_blocks(dlchunk)
+                         / self.parser_functions.parse_blocks_nested)^1)
+                    * Cc(false) / definition_list_item
+
+      local DefinitionListItemTight
+                    = C(parsers.line)
+                    * Ct((defstart * dlchunk
+                         / self.parser_functions.parse_blocks_nested)^1)
+                    * Cc(true) / definition_list_item
+
+      local DefinitionList
+                    = ( Ct(DefinitionListItemLoose^1) * Cc(false)
+                      + Ct(DefinitionListItemTight^1)
+                      * (parsers.skipblanklines
+                        * -DefinitionListItemLoose * Cc(true))
+                      ) / writer.definitionlist
+
+      syntax.DefinitionList = DefinitionList
+    end
+  }
+end
+M.extensions.fenced_code = function(blank_before_code_fence)
+  return {
+    extend_writer = function(self)
+      function self.fencedCode(i, s)
+        if not self.is_writing then return "" end
+        s = string.gsub(s, '[\r\n%s]*$', '')
+        local name = util.cache(self.cacheDir, s, nil, nil, ".verbatim")
+        return {"\\markdownRendererInputFencedCode{",name,"}{",i,"}"}
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local function captures_geq_length(s,i,a,b)
+        return #a >= #b and i
+      end
+
+      local infostring   = (parsers.linechar - (parsers.backtick
+                         + parsers.space^1 * (parsers.newline + parsers.eof)))^0
+
+      local fenceindent
+      local fencehead    = function(char)
+        return             C(parsers.nonindentspace) / function(s) fenceindent = #s end
+                         * Cg(char^3, "fencelength")
+                         * parsers.optionalspace * C(infostring)
+                         * parsers.optionalspace * (parsers.newline + parsers.eof)
+      end
+
+      local fencetail    = function(char)
+        return             parsers.nonindentspace
+                         * Cmt(C(char^3) * Cb("fencelength"), captures_geq_length)
+                         * parsers.optionalspace * (parsers.newline + parsers.eof)
+                         + parsers.eof
+      end
+
+      local fencedline   = function(char)
+        return             C(parsers.line - fencetail(char))
+                         / function(s)
+                             i = 1
+                             remaining = fenceindent
+                             while true do
+                               c = s:sub(i, i)
+                               if c == " " and remaining > 0 then
+                                 remaining = remaining - 1
+                                 i = i + 1
+                               elseif c == "\t" and remaining > 3 then
+                                 remaining = remaining - 4
+                                 i = i + 1
+                               else
+                                 break
+                               end
+                             end
+                             return s:sub(i)
+                           end
+      end
+
+      local TildeFencedCode
+             = fencehead(parsers.tilde)
+             * Cs(fencedline(parsers.tilde)^0)
+             * fencetail(parsers.tilde)
+
+      local BacktickFencedCode
+             = fencehead(parsers.backtick)
+             * Cs(fencedline(parsers.backtick)^0)
+             * fencetail(parsers.backtick)
+
+      local FencedCode = (TildeFencedCode
+                           + BacktickFencedCode)
+                         / function(infostring, code)
+                             return writer.fencedCode(writer.string(infostring),
+                                                      self.expandtabs(code))
+                           end
+
+      syntax.FencedCode = FencedCode
+
+      if blank_before_code_fence then
+        fencestart = parsers.fail
+      else
+        fencestart = fencehead(parsers.backtick)
+                   + fencehead(parsers.tilde)
+      end
+
+      parsers.EndlineExceptions = parsers.EndlineExceptions + fencestart
+      syntax.EndlineExceptions = parsers.EndlineExceptions
+    end
+  }
+end
+M.extensions.footnotes = function(footnotes, inline_footnotes)
+  assert(footnotes or inline_footnotes)
+  return {
+    extend_writer = function(self)
+      function self.note(s)
+        return {"\\markdownRendererFootnote{",s,"}"}
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      if footnotes then
+        local function strip_first_char(s)
+          return s:sub(2)
+        end
+
+        local RawNoteRef
+                      = #(parsers.lbracket * parsers.circumflex)
+                      * parsers.tag / strip_first_char
+
+        local rawnotes = {}
+
+        -- like indirect_link
+        local function lookup_note(ref)
+          return writer.defer_call(function()
+            local found = rawnotes[self.normalize_tag(ref)]
+            if found then
+              return writer.note(
+                self.parser_functions.parse_blocks_nested(found))
+            else
+              return {"[",
+                self.parser_functions.parse_inlines("^" .. ref), "]"}
+            end
+          end)
+        end
+
+        local function register_note(ref,rawnote)
+          rawnotes[self.normalize_tag(ref)] = rawnote
+          return ""
+        end
+
+        local NoteRef = RawNoteRef / lookup_note
+
+        local NoteBlock
+                    = parsers.leader * RawNoteRef * parsers.colon
+                    * parsers.spnl * parsers.indented_blocks(parsers.chunk)
+                    / register_note
+
+        parsers.Blank = NoteBlock + parsers.Blank
+        syntax.Blank = parsers.Blank
+
+        syntax.NoteRef = NoteRef
+      end
+      if inline_footnotes then
+        local InlineNote
+                    = parsers.circumflex
+                    * (parsers.tag / self.parser_functions.parse_inlines_no_inline_note)
+                    / writer.note
+        syntax.InlineNote = InlineNote
+      end
+    end
+  }
+end
+M.extensions.header_attributes = function()
+  return {
+    extend_writer = function(self)
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      parsers.AtxHeading = Cg(parsers.HeadingStart,"level")
+                         * parsers.optionalspace
+                         * (C(((parsers.linechar
+                               - ((parsers.hash^1
+                                  * parsers.optionalspace
+                                  * parsers.HeadingAttributes^-1
+                                  + parsers.HeadingAttributes)
+                                 * parsers.optionalspace
+                                 * parsers.newline))
+                              * (parsers.linechar
+                                - parsers.hash
+                                - parsers.lbrace)^0)^1)
+                             / self.parser_functions.parse_inlines)
+                         * Cg(Ct(parsers.newline
+                                + (parsers.hash^1
+                                  * parsers.optionalspace
+                                  * parsers.HeadingAttributes^-1
+                                  + parsers.HeadingAttributes)
+                                * parsers.optionalspace
+                                * parsers.newline), "attributes")
+                         * Cb("level")
+                         * Cb("attributes")
+                         / writer.heading
+
+      parsers.SetextHeading = #(parsers.line * S("=-"))
+                            * (C(((parsers.linechar
+                                  - (parsers.HeadingAttributes
+                                    * parsers.optionalspace
+                                    * parsers.newline))
+                                 * (parsers.linechar
+                                   - parsers.lbrace)^0)^1)
+                                / self.parser_functions.parse_inlines)
+                            * Cg(Ct(parsers.newline
+                                   + (parsers.HeadingAttributes
+                                     * parsers.optionalspace
+                                     * parsers.newline)), "attributes")
+                            * parsers.HeadingLevel
+                            * Cb("attributes")
+                            * parsers.optionalspace
+                            * parsers.newline
+                            / writer.heading
+
+      parsers.Heading = parsers.AtxHeading + parsers.SetextHeading
+      syntax.Heading = parsers.Heading
+    end
+  }
+end
+M.extensions.jekyll_data = function(expect_jekyll_data)
+  return {
+    extend_writer = function(self)
+      function self.jekyllData(d, t, p)
+        if not self.is_writing then return "" end
+
+        local buf = {}
+
+        local keys = {}
+        for k, _ in pairs(d) do
+          table.insert(keys, k)
+        end
+        table.sort(keys)
+
+        if not p then
+          table.insert(buf, "\\markdownRendererJekyllDataBegin")
+        end
+
+        if #d > 0 then
+            table.insert(buf, "\\markdownRendererJekyllDataSequenceBegin{")
+            table.insert(buf, self.uri(p or "null"))
+            table.insert(buf, "}{")
+            table.insert(buf, #keys)
+            table.insert(buf, "}")
+        else
+            table.insert(buf, "\\markdownRendererJekyllDataMappingBegin{")
+            table.insert(buf, self.uri(p or "null"))
+            table.insert(buf, "}{")
+            table.insert(buf, #keys)
+            table.insert(buf, "}")
+        end
+
+        for _, k in ipairs(keys) do
+          local v = d[k]
+          local typ = type(v)
+          k = tostring(k or "null")
+          if typ == "table" and next(v) ~= nil then
+            table.insert(
+              buf,
+              self.jekyllData(v, t, k)
+            )
+          else
+            k = self.uri(k)
+            v = tostring(v)
+            if typ == "boolean" then
+              table.insert(buf, "\\markdownRendererJekyllDataBoolean{")
+              table.insert(buf, k)
+              table.insert(buf, "}{")
+              table.insert(buf, v)
+              table.insert(buf, "}")
+            elseif typ == "number" then
+              table.insert(buf, "\\markdownRendererJekyllDataNumber{")
+              table.insert(buf, k)
+              table.insert(buf, "}{")
+              table.insert(buf, v)
+              table.insert(buf, "}")
+            elseif typ == "string" then
+              table.insert(buf, "\\markdownRendererJekyllDataString{")
+              table.insert(buf, k)
+              table.insert(buf, "}{")
+              table.insert(buf, t(v))
+              table.insert(buf, "}")
+            elseif typ == "table" then
+              table.insert(buf, "\\markdownRendererJekyllDataEmpty{")
+              table.insert(buf, k)
+              table.insert(buf, "}")
+            else
+              error(format("Unexpected type %s for value of " ..
+                           "YAML key %s", typ, k))
+            end
+          end
+        end
+
+        if #d > 0 then
+          table.insert(buf, "\\markdownRendererJekyllDataSequenceEnd")
+        else
+          table.insert(buf, "\\markdownRendererJekyllDataMappingEnd")
+        end
+
+        if not p then
+          table.insert(buf, "\\markdownRendererJekyllDataEnd")
+        end
+
+        return buf
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local JekyllData
+                    = Cmt( C((parsers.line - P("---") - P("..."))^0)
+                         , function(s, i, text)
+                             local data
+                             local ran_ok, error = pcall(function()
+                               local tinyyaml = require("markdown-tinyyaml")
+                               data = tinyyaml.parse(text, {timestamps=false})
+                             end)
+                             if ran_ok and data ~= nil then
+                               return true, writer.jekyllData(data, function(s)
+                                 return self.parser_functions.parse_blocks_nested(s)
+                               end, nil)
+                             else
+                               return false
+                             end
+                           end
+                         )
+
+      local UnexpectedJekyllData
+                    = P("---")
+                    * parsers.blankline / 0
+                    * #(-parsers.blankline)  -- if followed by blank, it's an hrule
+                    * JekyllData
+                    * (P("---") + P("..."))
+
+      local ExpectedJekyllData
+                    = ( P("---")
+                      * parsers.blankline / 0
+                      * #(-parsers.blankline)  -- if followed by blank, it's an hrule
+                      )^-1
+                    * JekyllData
+                    * (P("---") + P("..."))^-1
+
+      syntax.UnexpectedJekyllData = UnexpectedJekyllData
+      if expect_jekyll_data then
+        syntax.ExpectedJekyllData = ExpectedJekyllData
+      end
+    end
+  }
+end
+M.extensions.pipe_tables = function(table_captions)
+
+  local function make_pipe_table_rectangular(rows)
+    local num_columns = #rows[2]
+    local rectangular_rows = {}
+    for i = 1, #rows do
+      local row = rows[i]
+      local rectangular_row = {}
+      for j = 1, num_columns do
+        rectangular_row[j] = row[j] or ""
+      end
+      table.insert(rectangular_rows, rectangular_row)
+    end
+    return rectangular_rows
+  end
+
+  local function pipe_table_row(allow_empty_first_column
+                               , nonempty_column
+                               , column_separator
+                               , column)
+    local row_beginning
+    if allow_empty_first_column then
+      row_beginning = -- empty first column
+                      #(parsers.spacechar^4
+                       * column_separator)
+                    * parsers.optionalspace
+                    * column
+                    * parsers.optionalspace
+                    -- non-empty first column
+                    + parsers.nonindentspace
+                    * nonempty_column^-1
+                    * parsers.optionalspace
+    else
+      row_beginning = parsers.nonindentspace
+                    * nonempty_column^-1
+                    * parsers.optionalspace
+    end
+
+    return Ct(row_beginning
+             * (-- single column with no leading pipes
+                #(column_separator
+                 * parsers.optionalspace
+                 * parsers.newline)
+               * column_separator
+               * parsers.optionalspace
+               -- single column with leading pipes or
+               -- more than a single column
+               + (column_separator
+                 * parsers.optionalspace
+                 * column
+                 * parsers.optionalspace)^1
+               * (column_separator
+                 * parsers.optionalspace)^-1))
+  end
+
+  return {
+    extend_writer = function(self)
+      function self.table(rows, caption)
+        if not self.is_writing then return "" end
+        local buffer = {"\\markdownRendererTable{",
+          caption or "", "}{", #rows - 1, "}{", #rows[1], "}"}
+        local temp = rows[2] -- put alignments on the first row
+        rows[2] = rows[1]
+        rows[1] = temp
+        for i, row in ipairs(rows) do
+          table.insert(buffer, "{")
+          for _, column in ipairs(row) do
+            if i > 1 then -- do not use braces for alignments
+              table.insert(buffer, "{")
+            end
+            table.insert(buffer, column)
+            if i > 1 then
+              table.insert(buffer, "}")
+            end
+          end
+          table.insert(buffer, "}")
+        end
+        return buffer
+      end
+    end, extend_reader = function(self)
+      local parsers = self.parsers
+      local syntax = self.syntax
+      local writer = self.writer
+
+      local table_hline_separator = parsers.pipe + parsers.plus
+
+      local table_hline_column = (parsers.dash
+                                 - #(parsers.dash
+                                    * (parsers.spacechar
+                                      + table_hline_separator
+                                      + parsers.newline)))^1
+                               * (parsers.colon * Cc("r")
+                                 + parsers.dash * Cc("d"))
+                               + parsers.colon
+                               * (parsers.dash
+                                 - #(parsers.dash
+                                    * (parsers.spacechar
+                                      + table_hline_separator
+                                      + parsers.newline)))^1
+                               * (parsers.colon * Cc("c")
+                                 + parsers.dash * Cc("l"))
+
+      local table_hline = pipe_table_row(false
+                                        , table_hline_column
+                                        , table_hline_separator
+                                        , table_hline_column)
+
+      local table_caption_beginning = parsers.skipblanklines
+                                    * parsers.nonindentspace
+                                    * (P("Table")^-1 * parsers.colon)
+                                    * parsers.optionalspace
+
+      local table_row = pipe_table_row(true
+                                      , (C((parsers.linechar - parsers.pipe)^1)
+                                        / self.parser_functions.parse_inlines)
+                                      , parsers.pipe
+                                      , (C((parsers.linechar - parsers.pipe)^0)
+                                        / self.parser_functions.parse_inlines))
+
+      local table_caption
+      if table_captions then
+        table_caption = #table_caption_beginning
+                      * table_caption_beginning
+                      * Ct(parsers.IndentedInline^1)
+                      * parsers.newline
+      else
+        table_caption = parsers.fail
+      end
+
+      local PipeTable = Ct(table_row * parsers.newline
+                        * table_hline
+                        * (parsers.newline * table_row)^0)
+                      / make_pipe_table_rectangular
+                      * table_caption^-1
+                      / writer.table
+
+      syntax.PipeTable = PipeTable
+    end
+  }
+end
 function M.new(options)
+  options = options or {}
+  setmetatable(options, { __index = function (_, key)
+    return defaultOptions[key] end })
+  extensions = {}
+
+  if options.citations then
+    citations_extension = M.extensions.citations(options.citationNbsps)
+    table.insert(extensions, citations_extension)
+  end
+
+  if options.contentBlocks then
+    content_blocks_extension = M.extensions.content_blocks(
+      options.contentBlocksLanguageMap)
+    table.insert(extensions, content_blocks_extension)
+  end
+
+  if options.definitionLists then
+    definition_lists_extension = M.extensions.definition_lists(
+      options.tightLists)
+    table.insert(extensions, definition_lists_extension)
+  end
+
+  if options.fencedCode then
+    fenced_code_extension = M.extensions.fenced_code(
+      options.blankBeforeCodeFence)
+    table.insert(extensions, fenced_code_extension)
+  end
+
+  if options.footnotes or options.inlineFootnotes then
+    footnotes_extension = M.extensions.footnotes(
+      options.footnotes, options.inlineFootnotes)
+    table.insert(extensions, footnotes_extension)
+  end
+
+  if options.headerAttributes then
+    header_attributes_extension = M.extensions.header_attributes()
+    table.insert(extensions, header_attributes_extension)
+  end
+
+  if options.jekyllData then
+    jekyll_data_extension = M.extensions.jekyll_data(
+      options.expectJekyllData)
+    table.insert(extensions, jekyll_data_extension)
+  end
+
+  if options.pipeTables then
+    pipe_tables_extension = M.extensions.pipe_tables(
+      options.tableCaptions)
+    table.insert(extensions, pipe_tables_extension)
+  end
+
   local writer = M.writer.new(options)
-  local reader = M.reader.new(writer, options)
+  local reader = M.reader.new(writer, options, extensions)
+
   return reader.convert
 end
 



More information about the tex-live-commits mailing list.