--
-- Copyright (C) 2009-2016 John MacFarlane, Hans Hagen
--
-- Permission is hereby granted, free of charge, to any person obtaining
-- a copy of this software and associated documentation files (the
-- "Software"), to deal in the Software without restriction, including
-- without limitation the rights to use, copy, modify, merge, publish,
-- distribute, sublicense, and/or sell copies of the Software, and to
-- permit persons to whom the Software is furnished to do so, subject to
-- the following conditions:
--
-- The above copyright notice and this permission notice shall be included
-- in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--
-- Copyright (C) 2016-2024 Vít Starý Novotný
--
-- This work may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License, either version 1.3
-- 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 Starý Novotný.
--
-- Send bug reports, requests for additions and questions
-- either to the GitHub issue tracker at
--
--     https://github.com/witiko/markdown/issues
--
-- or to the e-mail address <witiko@mail.muni.cz>.
--
-- MODIFICATION ADVICE:
--
-- If you want to customize this file, it is best to make a copy of
-- the source file(s) from which it was produced. Use a different
-- name for your copy(ies) and modify the copy(ies); this will ensure
-- that your modifications do not get overwritten when you install a
-- new release of the standard system. You should also ensure that
-- your modified source file does not generate any modified file with
-- the same name as a standard file.
--
-- You will also need to produce your own, suitably named, .ins file to
-- control the generation of files from your source file; this file
-- should contain your own preambles for the files it generates, not
-- those in the standard .ins files.
--
local metadata = {
    version   = "3.6.1-0-g5a45a922",
    comment   = "A module for the conversion from markdown to plain TeX",
    author    = "John MacFarlane, Hans Hagen, Vít Starý Novotný",
    copyright = {"2009-2016 John MacFarlane, Hans Hagen",
                 "2016-2023 Vít Starý Novotný"},
    license   = "LPPL 1.3c"
}

if not modules then modules = { } end
modules['markdown'] = metadata
local lpeg = require("lpeg")
local unicode = require("unicode")
local md5 = require("md5");
(function()
  local should_initialize = package.loaded.kpse == nil
                       or tex.initialize ~= nil
  kpse = require("kpse")
  if should_initialize then
    kpse.set_program_name("luatex")
  end
end)()
local uni_algos = require("lua-uni-algos")
local M = {metadata = metadata}
local walkable_syntax = {
  Block = {
    "Blockquote",
    "Verbatim",
    "ThematicBreak",
    "BulletList",
    "OrderedList",
    "DisplayHtml",
    "Heading",
  },
  BlockOrParagraph = {
    "Block",
    "Paragraph",
    "Plain",
  },
  Inline = {
    "Str",
    "Space",
    "Endline",
    "EndlineBreak",
    "LinkAndEmph",
    "Code",
    "AutoLinkUrl",
    "AutoLinkEmail",
    "AutoLinkRelativeReference",
    "InlineHtml",
    "HtmlEntity",
    "EscapedChar",
    "Smart",
    "Symbol",
  },
}
local defaultOptions = {}
defaultOptions.eagerCache = false
defaultOptions.singletonCache = true
local singletonCache = {
  convert = nil,
  options = nil,
}
defaultOptions.unicodeNormalization = true
defaultOptions.unicodeNormalizationForm = "nfc"
defaultOptions.cacheDir = "."
defaultOptions.contentBlocksLanguageMap = "markdown-languages.json"
defaultOptions.debugExtensionsFileName = "debug-extensions.json"
defaultOptions.frozenCacheFileName = "frozenCache.tex"
defaultOptions.autoIdentifiers = false
defaultOptions.blankBeforeBlockquote = false
defaultOptions.blankBeforeCodeFence = false
defaultOptions.blankBeforeDivFence = false
defaultOptions.blankBeforeHeading = false
defaultOptions.blankBeforeList = false
defaultOptions.bracketedSpans = false
defaultOptions.breakableBlockquotes = true
defaultOptions.citationNbsps = true
defaultOptions.citations = false
defaultOptions.codeSpans = true
defaultOptions.contentBlocks = false
defaultOptions.contentLevel = "block"
defaultOptions.debugExtensions = false
defaultOptions.definitionLists = false
defaultOptions.expectJekyllData = false
metadata.user_extension_api_version = 2
metadata.grammar_version = 4
defaultOptions.extensions = {}
defaultOptions.fancyLists = false
defaultOptions.fencedCode = true
defaultOptions.fencedCodeAttributes = false
defaultOptions.fencedDivs = false
defaultOptions.finalizeCache = false
defaultOptions.frozenCacheCounter = 0
defaultOptions.gfmAutoIdentifiers = false
defaultOptions.hashEnumerators = false
defaultOptions.headerAttributes = false
defaultOptions.html = true
defaultOptions.hybrid = false
defaultOptions.inlineCodeAttributes = false
defaultOptions.inlineNotes = false
defaultOptions.jekyllData = false
defaultOptions.linkAttributes = false
defaultOptions.lineBlocks = false
defaultOptions.mark = false
defaultOptions.notes = false
defaultOptions.pipeTables = false
defaultOptions.preserveTabs = true
defaultOptions.rawAttribute = false
defaultOptions.relativeReferences = false
defaultOptions.shiftHeadings = 0
defaultOptions.slice = "^ $"
defaultOptions.smartEllipses = false
defaultOptions.startNumber = true
defaultOptions.strikeThrough = false
defaultOptions.stripIndent = false
defaultOptions.subscripts = false
defaultOptions.superscripts = false
defaultOptions.tableAttributes = false
defaultOptions.tableCaptions = false
defaultOptions.taskLists = false
defaultOptions.texComments = false
defaultOptions.texMathDollars = false
defaultOptions.texMathDoubleBackslash = false
defaultOptions.texMathSingleBackslash = false
defaultOptions.tightLists = true
defaultOptions.underscores = true
local upper, format, length =
  string.upper, string.format, string.len
local P, R, S, V, C, Cg, Cb, Cmt, Cc, Ct, B, Cs, Cp, any =
  lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.C, lpeg.Cg, lpeg.Cb,
  lpeg.Cmt, lpeg.Cc, lpeg.Ct, lpeg.B, lpeg.Cs, lpeg.Cp, lpeg.P(1)
local util = {}
function util.err(msg, exit_code)
  io.stderr:write("markdown.lua: " .. msg .. "\n")
  os.exit(exit_code or 1)
end
function util.cache(dir, string, salt, transform, suffix)
  local digest = md5.sumhexa(string .. (salt or ""))
  local name = util.pathname(dir, digest .. suffix)
  local file = io.open(name, "r")
  if file == nil then -- If no cache entry exists, then create a new one.
    file = assert(io.open(name, "w"),
      [[Could not open file "]] .. name .. [[" for writing]])
    local result = string
    if transform ~= nil then
      result = transform(result)
    end
    assert(file:write(result))
    assert(file:close())
  end
  return name
end
function util.cache_verbatim(dir, string)
  local name = util.cache(dir, string, nil, nil, ".verbatim")
  return name
end
function util.table_copy(t)
  local u = { }
  for k, v in pairs(t) do u[k] = v end
  return setmetatable(u, getmetatable(t))
end
function util.encode_json_string(s)
  s = s:gsub([[\]], [[\\]])
  s = s:gsub([["]], [[\"]])
  return [["]] .. s .. [["]]
end
function util.expand_tabs_in_line(s, tabstop)
  local tab = tabstop or 4
  local corr = 0
  return (s:gsub("()\t", function(p)
            local sp = tab - (p - 1 + corr) % tab
            corr = corr - 1 + sp
            return string.rep(" ", sp)
          end))
end
function util.walk(t, f)
  local typ = type(t)
  if typ == "string" then
    f(t)
  elseif typ == "table" then
    local i = 1
    local n
    n = t[i]
    while n do
      util.walk(n, f)
      i = i + 1
      n = t[i]
    end
  elseif typ == "function" then
    local ok, val = pcall(t)
    if ok then
      util.walk(val,f)
    end
  else
    f(tostring(t))
  end
end
function util.flatten(ary)
  local new = {}
  for _,v in ipairs(ary) do
    if type(v) == "table" then
      for _,w in ipairs(util.flatten(v)) do
        new[#new + 1] = w
      end
    else
      new[#new + 1] = v
    end
  end
  return new
end
function util.rope_to_string(rope)
  local buffer = {}
  util.walk(rope, function(x) buffer[#buffer + 1] = x end)
  return table.concat(buffer)
end
function util.rope_last(rope)
  if #rope == 0 then
    return nil
  else
    local l = rope[#rope]
    if type(l) == "table" then
      return util.rope_last(l)
    else
      return l
    end
  end
end
function util.intersperse(ary, x)
  local new = {}
  local l = #ary
  for i,v in ipairs(ary) do
    local n = #new
    new[n + 1] = v
    if i ~= l then
      new[n + 2] = x
    end
  end
  return new
end
function util.map(ary, f)
  local new = {}
  for i,v in ipairs(ary) do
    new[i] = f(v)
  end
  return new
end
function util.escaper(char_escapes, string_escapes)
  local char_escapes_list = ""
  for i,_ in pairs(char_escapes) do
    char_escapes_list = char_escapes_list .. i
  end
  local escapable = S(char_escapes_list) / char_escapes
  if string_escapes then
    for k,v in pairs(string_escapes) do
      escapable = P(k) / v + escapable
    end
  end
  local escape_string = Cs((escapable + any)^0)
  return function(s)
    return lpeg.match(escape_string, s)
  end
end
function util.pathname(dir, file)
  if #dir == 0 then
    return file
  else
    return dir .. "/" .. file
  end
end
local entities = {}

local character_entities = {
  ["Tab"] = 9,
  ["NewLine"] = 10,
  ["excl"] = 33,
  ["QUOT"] = 34,
  ["quot"] = 34,
  ["num"] = 35,
  ["dollar"] = 36,
  ["percnt"] = 37,
  ["AMP"] = 38,
  ["amp"] = 38,
  ["apos"] = 39,
  ["lpar"] = 40,
  ["rpar"] = 41,
  ["ast"] = 42,
  ["midast"] = 42,
  ["plus"] = 43,
  ["comma"] = 44,
  ["period"] = 46,
  ["sol"] = 47,
  ["colon"] = 58,
  ["semi"] = 59,
  ["LT"] = 60,
  ["lt"] = 60,
  ["nvlt"] = {60, 8402},
  ["bne"] = {61, 8421},
  ["equals"] = 61,
  ["GT"] = 62,
  ["gt"] = 62,
  ["nvgt"] = {62, 8402},
  ["quest"] = 63,
  ["commat"] = 64,
  ["lbrack"] = 91,
  ["lsqb"] = 91,
  ["bsol"] = 92,
  ["rbrack"] = 93,
  ["rsqb"] = 93,
  ["Hat"] = 94,
  ["UnderBar"] = 95,
  ["lowbar"] = 95,
  ["DiacriticalGrave"] = 96,
  ["grave"] = 96,
  ["fjlig"] = {102, 106},
  ["lbrace"] = 123,
  ["lcub"] = 123,
  ["VerticalLine"] = 124,
  ["verbar"] = 124,
  ["vert"] = 124,
  ["rbrace"] = 125,
  ["rcub"] = 125,
  ["NonBreakingSpace"] = 160,
  ["nbsp"] = 160,
  ["iexcl"] = 161,
  ["cent"] = 162,
  ["pound"] = 163,
  ["curren"] = 164,
  ["yen"] = 165,
  ["brvbar"] = 166,
  ["sect"] = 167,
  ["Dot"] = 168,
  ["DoubleDot"] = 168,
  ["die"] = 168,
  ["uml"] = 168,
  ["COPY"] = 169,
  ["copy"] = 169,
  ["ordf"] = 170,
  ["laquo"] = 171,
  ["not"] = 172,
  ["shy"] = 173,
  ["REG"] = 174,
  ["circledR"] = 174,
  ["reg"] = 174,
  ["macr"] = 175,
  ["strns"] = 175,
  ["deg"] = 176,
  ["PlusMinus"] = 177,
  ["plusmn"] = 177,
  ["pm"] = 177,
  ["sup2"] = 178,
  ["sup3"] = 179,
  ["DiacriticalAcute"] = 180,
  ["acute"] = 180,
  ["micro"] = 181,
  ["para"] = 182,
  ["CenterDot"] = 183,
  ["centerdot"] = 183,
  ["middot"] = 183,
  ["Cedilla"] = 184,
  ["cedil"] = 184,
  ["sup1"] = 185,
  ["ordm"] = 186,
  ["raquo"] = 187,
  ["frac14"] = 188,
  ["frac12"] = 189,
  ["half"] = 189,
  ["frac34"] = 190,
  ["iquest"] = 191,
  ["Agrave"] = 192,
  ["Aacute"] = 193,
  ["Acirc"] = 194,
  ["Atilde"] = 195,
  ["Auml"] = 196,
  ["Aring"] = 197,
  ["angst"] = 197,
  ["AElig"] = 198,
  ["Ccedil"] = 199,
  ["Egrave"] = 200,
  ["Eacute"] = 201,
  ["Ecirc"] = 202,
  ["Euml"] = 203,
  ["Igrave"] = 204,
  ["Iacute"] = 205,
  ["Icirc"] = 206,
  ["Iuml"] = 207,
  ["ETH"] = 208,
  ["Ntilde"] = 209,
  ["Ograve"] = 210,
  ["Oacute"] = 211,
  ["Ocirc"] = 212,
  ["Otilde"] = 213,
  ["Ouml"] = 214,
  ["times"] = 215,
  ["Oslash"] = 216,
  ["Ugrave"] = 217,
  ["Uacute"] = 218,
  ["Ucirc"] = 219,
  ["Uuml"] = 220,
  ["Yacute"] = 221,
  ["THORN"] = 222,
  ["szlig"] = 223,
  ["agrave"] = 224,
  ["aacute"] = 225,
  ["acirc"] = 226,
  ["atilde"] = 227,
  ["auml"] = 228,
  ["aring"] = 229,
  ["aelig"] = 230,
  ["ccedil"] = 231,
  ["egrave"] = 232,
  ["eacute"] = 233,
  ["ecirc"] = 234,
  ["euml"] = 235,
  ["igrave"] = 236,
  ["iacute"] = 237,
  ["icirc"] = 238,
  ["iuml"] = 239,
  ["eth"] = 240,
  ["ntilde"] = 241,
  ["ograve"] = 242,
  ["oacute"] = 243,
  ["ocirc"] = 244,
  ["otilde"] = 245,
  ["ouml"] = 246,
  ["div"] = 247,
  ["divide"] = 247,
  ["oslash"] = 248,
  ["ugrave"] = 249,
  ["uacute"] = 250,
  ["ucirc"] = 251,
  ["uuml"] = 252,
  ["yacute"] = 253,
  ["thorn"] = 254,
  ["yuml"] = 255,
  ["Amacr"] = 256,
  ["amacr"] = 257,
  ["Abreve"] = 258,
  ["abreve"] = 259,
  ["Aogon"] = 260,
  ["aogon"] = 261,
  ["Cacute"] = 262,
  ["cacute"] = 263,
  ["Ccirc"] = 264,
  ["ccirc"] = 265,
  ["Cdot"] = 266,
  ["cdot"] = 267,
  ["Ccaron"] = 268,
  ["ccaron"] = 269,
  ["Dcaron"] = 270,
  ["dcaron"] = 271,
  ["Dstrok"] = 272,
  ["dstrok"] = 273,
  ["Emacr"] = 274,
  ["emacr"] = 275,
  ["Edot"] = 278,
  ["edot"] = 279,
  ["Eogon"] = 280,
  ["eogon"] = 281,
  ["Ecaron"] = 282,
  ["ecaron"] = 283,
  ["Gcirc"] = 284,
  ["gcirc"] = 285,
  ["Gbreve"] = 286,
  ["gbreve"] = 287,
  ["Gdot"] = 288,
  ["gdot"] = 289,
  ["Gcedil"] = 290,
  ["Hcirc"] = 292,
  ["hcirc"] = 293,
  ["Hstrok"] = 294,
  ["hstrok"] = 295,
  ["Itilde"] = 296,
  ["itilde"] = 297,
  ["Imacr"] = 298,
  ["imacr"] = 299,
  ["Iogon"] = 302,
  ["iogon"] = 303,
  ["Idot"] = 304,
  ["imath"] = 305,
  ["inodot"] = 305,
  ["IJlig"] = 306,
  ["ijlig"] = 307,
  ["Jcirc"] = 308,
  ["jcirc"] = 309,
  ["Kcedil"] = 310,
  ["kcedil"] = 311,
  ["kgreen"] = 312,
  ["Lacute"] = 313,
  ["lacute"] = 314,
  ["Lcedil"] = 315,
  ["lcedil"] = 316,
  ["Lcaron"] = 317,
  ["lcaron"] = 318,
  ["Lmidot"] = 319,
  ["lmidot"] = 320,
  ["Lstrok"] = 321,
  ["lstrok"] = 322,
  ["Nacute"] = 323,
  ["nacute"] = 324,
  ["Ncedil"] = 325,
  ["ncedil"] = 326,
  ["Ncaron"] = 327,
  ["ncaron"] = 328,
  ["napos"] = 329,
  ["ENG"] = 330,
  ["eng"] = 331,
  ["Omacr"] = 332,
  ["omacr"] = 333,
  ["Odblac"] = 336,
  ["odblac"] = 337,
  ["OElig"] = 338,
  ["oelig"] = 339,
  ["Racute"] = 340,
  ["racute"] = 341,
  ["Rcedil"] = 342,
  ["rcedil"] = 343,
  ["Rcaron"] = 344,
  ["rcaron"] = 345,
  ["Sacute"] = 346,
  ["sacute"] = 347,
  ["Scirc"] = 348,
  ["scirc"] = 349,
  ["Scedil"] = 350,
  ["scedil"] = 351,
  ["Scaron"] = 352,
  ["scaron"] = 353,
  ["Tcedil"] = 354,
  ["tcedil"] = 355,
  ["Tcaron"] = 356,
  ["tcaron"] = 357,
  ["Tstrok"] = 358,
  ["tstrok"] = 359,
  ["Utilde"] = 360,
  ["utilde"] = 361,
  ["Umacr"] = 362,
  ["umacr"] = 363,
  ["Ubreve"] = 364,
  ["ubreve"] = 365,
  ["Uring"] = 366,
  ["uring"] = 367,
  ["Udblac"] = 368,
  ["udblac"] = 369,
  ["Uogon"] = 370,
  ["uogon"] = 371,
  ["Wcirc"] = 372,
  ["wcirc"] = 373,
  ["Ycirc"] = 374,
  ["ycirc"] = 375,
  ["Yuml"] = 376,
  ["Zacute"] = 377,
  ["zacute"] = 378,
  ["Zdot"] = 379,
  ["zdot"] = 380,
  ["Zcaron"] = 381,
  ["zcaron"] = 382,
  ["fnof"] = 402,
  ["imped"] = 437,
  ["gacute"] = 501,
  ["jmath"] = 567,
  ["circ"] = 710,
  ["Hacek"] = 711,
  ["caron"] = 711,
  ["Breve"] = 728,
  ["breve"] = 728,
  ["DiacriticalDot"] = 729,
  ["dot"] = 729,
  ["ring"] = 730,
  ["ogon"] = 731,
  ["DiacriticalTilde"] = 732,
  ["tilde"] = 732,
  ["DiacriticalDoubleAcute"] = 733,
  ["dblac"] = 733,
  ["DownBreve"] = 785,
  ["Alpha"] = 913,
  ["Beta"] = 914,
  ["Gamma"] = 915,
  ["Delta"] = 916,
  ["Epsilon"] = 917,
  ["Zeta"] = 918,
  ["Eta"] = 919,
  ["Theta"] = 920,
  ["Iota"] = 921,
  ["Kappa"] = 922,
  ["Lambda"] = 923,
  ["Mu"] = 924,
  ["Nu"] = 925,
  ["Xi"] = 926,
  ["Omicron"] = 927,
  ["Pi"] = 928,
  ["Rho"] = 929,
  ["Sigma"] = 931,
  ["Tau"] = 932,
  ["Upsilon"] = 933,
  ["Phi"] = 934,
  ["Chi"] = 935,
  ["Psi"] = 936,
  ["Omega"] = 937,
  ["ohm"] = 937,
  ["alpha"] = 945,
  ["beta"] = 946,
  ["gamma"] = 947,
  ["delta"] = 948,
  ["epsi"] = 949,
  ["epsilon"] = 949,
  ["zeta"] = 950,
  ["eta"] = 951,
  ["theta"] = 952,
  ["iota"] = 953,
  ["kappa"] = 954,
  ["lambda"] = 955,
  ["mu"] = 956,
  ["nu"] = 957,
  ["xi"] = 958,
  ["omicron"] = 959,
  ["pi"] = 960,
  ["rho"] = 961,
  ["sigmaf"] = 962,
  ["sigmav"] = 962,
  ["varsigma"] = 962,
  ["sigma"] = 963,
  ["tau"] = 964,
  ["upsi"] = 965,
  ["upsilon"] = 965,
  ["phi"] = 966,
  ["chi"] = 967,
  ["psi"] = 968,
  ["omega"] = 969,
  ["thetasym"] = 977,
  ["thetav"] = 977,
  ["vartheta"] = 977,
  ["Upsi"] = 978,
  ["upsih"] = 978,
  ["phiv"] = 981,
  ["straightphi"] = 981,
  ["varphi"] = 981,
  ["piv"] = 982,
  ["varpi"] = 982,
  ["Gammad"] = 988,
  ["digamma"] = 989,
  ["gammad"] = 989,
  ["kappav"] = 1008,
  ["varkappa"] = 1008,
  ["rhov"] = 1009,
  ["varrho"] = 1009,
  ["epsiv"] = 1013,
  ["straightepsilon"] = 1013,
  ["varepsilon"] = 1013,
  ["backepsilon"] = 1014,
  ["bepsi"] = 1014,
  ["IOcy"] = 1025,
  ["DJcy"] = 1026,
  ["GJcy"] = 1027,
  ["Jukcy"] = 1028,
  ["DScy"] = 1029,
  ["Iukcy"] = 1030,
  ["YIcy"] = 1031,
  ["Jsercy"] = 1032,
  ["LJcy"] = 1033,
  ["NJcy"] = 1034,
  ["TSHcy"] = 1035,
  ["KJcy"] = 1036,
  ["Ubrcy"] = 1038,
  ["DZcy"] = 1039,
  ["Acy"] = 1040,
  ["Bcy"] = 1041,
  ["Vcy"] = 1042,
  ["Gcy"] = 1043,
  ["Dcy"] = 1044,
  ["IEcy"] = 1045,
  ["ZHcy"] = 1046,
  ["Zcy"] = 1047,
  ["Icy"] = 1048,
  ["Jcy"] = 1049,
  ["Kcy"] = 1050,
  ["Lcy"] = 1051,
  ["Mcy"] = 1052,
  ["Ncy"] = 1053,
  ["Ocy"] = 1054,
  ["Pcy"] = 1055,
  ["Rcy"] = 1056,
  ["Scy"] = 1057,
  ["Tcy"] = 1058,
  ["Ucy"] = 1059,
  ["Fcy"] = 1060,
  ["KHcy"] = 1061,
  ["TScy"] = 1062,
  ["CHcy"] = 1063,
  ["SHcy"] = 1064,
  ["SHCHcy"] = 1065,
  ["HARDcy"] = 1066,
  ["Ycy"] = 1067,
  ["SOFTcy"] = 1068,
  ["Ecy"] = 1069,
  ["YUcy"] = 1070,
  ["YAcy"] = 1071,
  ["acy"] = 1072,
  ["bcy"] = 1073,
  ["vcy"] = 1074,
  ["gcy"] = 1075,
  ["dcy"] = 1076,
  ["iecy"] = 1077,
  ["zhcy"] = 1078,
  ["zcy"] = 1079,
  ["icy"] = 1080,
  ["jcy"] = 1081,
  ["kcy"] = 1082,
  ["lcy"] = 1083,
  ["mcy"] = 1084,
  ["ncy"] = 1085,
  ["ocy"] = 1086,
  ["pcy"] = 1087,
  ["rcy"] = 1088,
  ["scy"] = 1089,
  ["tcy"] = 1090,
  ["ucy"] = 1091,
  ["fcy"] = 1092,
  ["khcy"] = 1093,
  ["tscy"] = 1094,
  ["chcy"] = 1095,
  ["shcy"] = 1096,
  ["shchcy"] = 1097,
  ["hardcy"] = 1098,
  ["ycy"] = 1099,
  ["softcy"] = 1100,
  ["ecy"] = 1101,
  ["yucy"] = 1102,
  ["yacy"] = 1103,
  ["iocy"] = 1105,
  ["djcy"] = 1106,
  ["gjcy"] = 1107,
  ["jukcy"] = 1108,
  ["dscy"] = 1109,
  ["iukcy"] = 1110,
  ["yicy"] = 1111,
  ["jsercy"] = 1112,
  ["ljcy"] = 1113,
  ["njcy"] = 1114,
  ["tshcy"] = 1115,
  ["kjcy"] = 1116,
  ["ubrcy"] = 1118,
  ["dzcy"] = 1119,
  ["ensp"] = 8194,
  ["emsp"] = 8195,
  ["emsp13"] = 8196,
  ["emsp14"] = 8197,
  ["numsp"] = 8199,
  ["puncsp"] = 8200,
  ["ThinSpace"] = 8201,
  ["thinsp"] = 8201,
  ["VeryThinSpace"] = 8202,
  ["hairsp"] = 8202,
  ["NegativeMediumSpace"] = 8203,
  ["NegativeThickSpace"] = 8203,
  ["NegativeThinSpace"] = 8203,
  ["NegativeVeryThinSpace"] = 8203,
  ["ZeroWidthSpace"] = 8203,
  ["zwnj"] = 8204,
  ["zwj"] = 8205,
  ["lrm"] = 8206,
  ["rlm"] = 8207,
  ["dash"] = 8208,
  ["hyphen"] = 8208,
  ["ndash"] = 8211,
  ["mdash"] = 8212,
  ["horbar"] = 8213,
  ["Verbar"] = 8214,
  ["Vert"] = 8214,
  ["OpenCurlyQuote"] = 8216,
  ["lsquo"] = 8216,
  ["CloseCurlyQuote"] = 8217,
  ["rsquo"] = 8217,
  ["rsquor"] = 8217,
  ["lsquor"] = 8218,
  ["sbquo"] = 8218,
  ["OpenCurlyDoubleQuote"] = 8220,
  ["ldquo"] = 8220,
  ["CloseCurlyDoubleQuote"] = 8221,
  ["rdquo"] = 8221,
  ["rdquor"] = 8221,
  ["bdquo"] = 8222,
  ["ldquor"] = 8222,
  ["dagger"] = 8224,
  ["Dagger"] = 8225,
  ["ddagger"] = 8225,
  ["bull"] = 8226,
  ["bullet"] = 8226,
  ["nldr"] = 8229,
  ["hellip"] = 8230,
  ["mldr"] = 8230,
  ["permil"] = 8240,
  ["pertenk"] = 8241,
  ["prime"] = 8242,
  ["Prime"] = 8243,
  ["tprime"] = 8244,
  ["backprime"] = 8245,
  ["bprime"] = 8245,
  ["lsaquo"] = 8249,
  ["rsaquo"] = 8250,
  ["OverBar"] = 8254,
  ["oline"] = 8254,
  ["caret"] = 8257,
  ["hybull"] = 8259,
  ["frasl"] = 8260,
  ["bsemi"] = 8271,
  ["qprime"] = 8279,
  ["MediumSpace"] = 8287,
  ["ThickSpace"] = {8287, 8202},
  ["NoBreak"] = 8288,
  ["ApplyFunction"] = 8289,
  ["af"] = 8289,
  ["InvisibleTimes"] = 8290,
  ["it"] = 8290,
  ["InvisibleComma"] = 8291,
  ["ic"] = 8291,
  ["euro"] = 8364,
  ["TripleDot"] = 8411,
  ["tdot"] = 8411,
  ["DotDot"] = 8412,
  ["Copf"] = 8450,
  ["complexes"] = 8450,
  ["incare"] = 8453,
  ["gscr"] = 8458,
  ["HilbertSpace"] = 8459,
  ["Hscr"] = 8459,
  ["hamilt"] = 8459,
  ["Hfr"] = 8460,
  ["Poincareplane"] = 8460,
  ["Hopf"] = 8461,
  ["quaternions"] = 8461,
  ["planckh"] = 8462,
  ["hbar"] = 8463,
  ["hslash"] = 8463,
  ["planck"] = 8463,
  ["plankv"] = 8463,
  ["Iscr"] = 8464,
  ["imagline"] = 8464,
  ["Ifr"] = 8465,
  ["Im"] = 8465,
  ["image"] = 8465,
  ["imagpart"] = 8465,
  ["Laplacetrf"] = 8466,
  ["Lscr"] = 8466,
  ["lagran"] = 8466,
  ["ell"] = 8467,
  ["Nopf"] = 8469,
  ["naturals"] = 8469,
  ["numero"] = 8470,
  ["copysr"] = 8471,
  ["weierp"] = 8472,
  ["wp"] = 8472,
  ["Popf"] = 8473,
  ["primes"] = 8473,
  ["Qopf"] = 8474,
  ["rationals"] = 8474,
  ["Rscr"] = 8475,
  ["realine"] = 8475,
  ["Re"] = 8476,
  ["Rfr"] = 8476,
  ["real"] = 8476,
  ["realpart"] = 8476,
  ["Ropf"] = 8477,
  ["reals"] = 8477,
  ["rx"] = 8478,
  ["TRADE"] = 8482,
  ["trade"] = 8482,
  ["Zopf"] = 8484,
  ["integers"] = 8484,
  ["mho"] = 8487,
  ["Zfr"] = 8488,
  ["zeetrf"] = 8488,
  ["iiota"] = 8489,
  ["Bernoullis"] = 8492,
  ["Bscr"] = 8492,
  ["bernou"] = 8492,
  ["Cayleys"] = 8493,
  ["Cfr"] = 8493,
  ["escr"] = 8495,
  ["Escr"] = 8496,
  ["expectation"] = 8496,
  ["Fouriertrf"] = 8497,
  ["Fscr"] = 8497,
  ["Mellintrf"] = 8499,
  ["Mscr"] = 8499,
  ["phmmat"] = 8499,
  ["order"] = 8500,
  ["orderof"] = 8500,
  ["oscr"] = 8500,
  ["alefsym"] = 8501,
  ["aleph"] = 8501,
  ["beth"] = 8502,
  ["gimel"] = 8503,
  ["daleth"] = 8504,
  ["CapitalDifferentialD"] = 8517,
  ["DD"] = 8517,
  ["DifferentialD"] = 8518,
  ["dd"] = 8518,
  ["ExponentialE"] = 8519,
  ["ee"] = 8519,
  ["exponentiale"] = 8519,
  ["ImaginaryI"] = 8520,
  ["ii"] = 8520,
  ["frac13"] = 8531,
  ["frac23"] = 8532,
  ["frac15"] = 8533,
  ["frac25"] = 8534,
  ["frac35"] = 8535,
  ["frac45"] = 8536,
  ["frac16"] = 8537,
  ["frac56"] = 8538,
  ["frac18"] = 8539,
  ["frac38"] = 8540,
  ["frac58"] = 8541,
  ["frac78"] = 8542,
  ["LeftArrow"] = 8592,
  ["ShortLeftArrow"] = 8592,
  ["larr"] = 8592,
  ["leftarrow"] = 8592,
  ["slarr"] = 8592,
  ["ShortUpArrow"] = 8593,
  ["UpArrow"] = 8593,
  ["uarr"] = 8593,
  ["uparrow"] = 8593,
  ["RightArrow"] = 8594,
  ["ShortRightArrow"] = 8594,
  ["rarr"] = 8594,
  ["rightarrow"] = 8594,
  ["srarr"] = 8594,
  ["DownArrow"] = 8595,
  ["ShortDownArrow"] = 8595,
  ["darr"] = 8595,
  ["downarrow"] = 8595,
  ["LeftRightArrow"] = 8596,
  ["harr"] = 8596,
  ["leftrightarrow"] = 8596,
  ["UpDownArrow"] = 8597,
  ["updownarrow"] = 8597,
  ["varr"] = 8597,
  ["UpperLeftArrow"] = 8598,
  ["nwarr"] = 8598,
  ["nwarrow"] = 8598,
  ["UpperRightArrow"] = 8599,
  ["nearr"] = 8599,
  ["nearrow"] = 8599,
  ["LowerRightArrow"] = 8600,
  ["searr"] = 8600,
  ["searrow"] = 8600,
  ["LowerLeftArrow"] = 8601,
  ["swarr"] = 8601,
  ["swarrow"] = 8601,
  ["nlarr"] = 8602,
  ["nleftarrow"] = 8602,
  ["nrarr"] = 8603,
  ["nrightarrow"] = 8603,
  ["nrarrw"] = {8605, 824},
  ["rarrw"] = 8605,
  ["rightsquigarrow"] = 8605,
  ["Larr"] = 8606,
  ["twoheadleftarrow"] = 8606,
  ["Uarr"] = 8607,
  ["Rarr"] = 8608,
  ["twoheadrightarrow"] = 8608,
  ["Darr"] = 8609,
  ["larrtl"] = 8610,
  ["leftarrowtail"] = 8610,
  ["rarrtl"] = 8611,
  ["rightarrowtail"] = 8611,
  ["LeftTeeArrow"] = 8612,
  ["mapstoleft"] = 8612,
  ["UpTeeArrow"] = 8613,
  ["mapstoup"] = 8613,
  ["RightTeeArrow"] = 8614,
  ["map"] = 8614,
  ["mapsto"] = 8614,
  ["DownTeeArrow"] = 8615,
  ["mapstodown"] = 8615,
  ["hookleftarrow"] = 8617,
  ["larrhk"] = 8617,
  ["hookrightarrow"] = 8618,
  ["rarrhk"] = 8618,
  ["larrlp"] = 8619,
  ["looparrowleft"] = 8619,
  ["looparrowright"] = 8620,
  ["rarrlp"] = 8620,
  ["harrw"] = 8621,
  ["leftrightsquigarrow"] = 8621,
  ["nharr"] = 8622,
  ["nleftrightarrow"] = 8622,
  ["Lsh"] = 8624,
  ["lsh"] = 8624,
  ["Rsh"] = 8625,
  ["rsh"] = 8625,
  ["ldsh"] = 8626,
  ["rdsh"] = 8627,
  ["crarr"] = 8629,
  ["cularr"] = 8630,
  ["curvearrowleft"] = 8630,
  ["curarr"] = 8631,
  ["curvearrowright"] = 8631,
  ["circlearrowleft"] = 8634,
  ["olarr"] = 8634,
  ["circlearrowright"] = 8635,
  ["orarr"] = 8635,
  ["LeftVector"] = 8636,
  ["leftharpoonup"] = 8636,
  ["lharu"] = 8636,
  ["DownLeftVector"] = 8637,
  ["leftharpoondown"] = 8637,
  ["lhard"] = 8637,
  ["RightUpVector"] = 8638,
  ["uharr"] = 8638,
  ["upharpoonright"] = 8638,
  ["LeftUpVector"] = 8639,
  ["uharl"] = 8639,
  ["upharpoonleft"] = 8639,
  ["RightVector"] = 8640,
  ["rharu"] = 8640,
  ["rightharpoonup"] = 8640,
  ["DownRightVector"] = 8641,
  ["rhard"] = 8641,
  ["rightharpoondown"] = 8641,
  ["RightDownVector"] = 8642,
  ["dharr"] = 8642,
  ["downharpoonright"] = 8642,
  ["LeftDownVector"] = 8643,
  ["dharl"] = 8643,
  ["downharpoonleft"] = 8643,
  ["RightArrowLeftArrow"] = 8644,
  ["rightleftarrows"] = 8644,
  ["rlarr"] = 8644,
  ["UpArrowDownArrow"] = 8645,
  ["udarr"] = 8645,
  ["LeftArrowRightArrow"] = 8646,
  ["leftrightarrows"] = 8646,
  ["lrarr"] = 8646,
  ["leftleftarrows"] = 8647,
  ["llarr"] = 8647,
  ["upuparrows"] = 8648,
  ["uuarr"] = 8648,
  ["rightrightarrows"] = 8649,
  ["rrarr"] = 8649,
  ["ddarr"] = 8650,
  ["downdownarrows"] = 8650,
  ["ReverseEquilibrium"] = 8651,
  ["leftrightharpoons"] = 8651,
  ["lrhar"] = 8651,
  ["Equilibrium"] = 8652,
  ["rightleftharpoons"] = 8652,
  ["rlhar"] = 8652,
  ["nLeftarrow"] = 8653,
  ["nlArr"] = 8653,
  ["nLeftrightarrow"] = 8654,
  ["nhArr"] = 8654,
  ["nRightarrow"] = 8655,
  ["nrArr"] = 8655,
  ["DoubleLeftArrow"] = 8656,
  ["Leftarrow"] = 8656,
  ["lArr"] = 8656,
  ["DoubleUpArrow"] = 8657,
  ["Uparrow"] = 8657,
  ["uArr"] = 8657,
  ["DoubleRightArrow"] = 8658,
  ["Implies"] = 8658,
  ["Rightarrow"] = 8658,
  ["rArr"] = 8658,
  ["DoubleDownArrow"] = 8659,
  ["Downarrow"] = 8659,
  ["dArr"] = 8659,
  ["DoubleLeftRightArrow"] = 8660,
  ["Leftrightarrow"] = 8660,
  ["hArr"] = 8660,
  ["iff"] = 8660,
  ["DoubleUpDownArrow"] = 8661,
  ["Updownarrow"] = 8661,
  ["vArr"] = 8661,
  ["nwArr"] = 8662,
  ["neArr"] = 8663,
  ["seArr"] = 8664,
  ["swArr"] = 8665,
  ["Lleftarrow"] = 8666,
  ["lAarr"] = 8666,
  ["Rrightarrow"] = 8667,
  ["rAarr"] = 8667,
  ["zigrarr"] = 8669,
  ["LeftArrowBar"] = 8676,
  ["larrb"] = 8676,
  ["RightArrowBar"] = 8677,
  ["rarrb"] = 8677,
  ["DownArrowUpArrow"] = 8693,
  ["duarr"] = 8693,
  ["loarr"] = 8701,
  ["roarr"] = 8702,
  ["hoarr"] = 8703,
  ["ForAll"] = 8704,
  ["forall"] = 8704,
  ["comp"] = 8705,
  ["complement"] = 8705,
  ["PartialD"] = 8706,
  ["npart"] = {8706, 824},
  ["part"] = 8706,
  ["Exists"] = 8707,
  ["exist"] = 8707,
  ["NotExists"] = 8708,
  ["nexist"] = 8708,
  ["nexists"] = 8708,
  ["empty"] = 8709,
  ["emptyset"] = 8709,
  ["emptyv"] = 8709,
  ["varnothing"] = 8709,
  ["Del"] = 8711,
  ["nabla"] = 8711,
  ["Element"] = 8712,
  ["in"] = 8712,
  ["isin"] = 8712,
  ["isinv"] = 8712,
  ["NotElement"] = 8713,
  ["notin"] = 8713,
  ["notinva"] = 8713,
  ["ReverseElement"] = 8715,
  ["SuchThat"] = 8715,
  ["ni"] = 8715,
  ["niv"] = 8715,
  ["NotReverseElement"] = 8716,
  ["notni"] = 8716,
  ["notniva"] = 8716,
  ["Product"] = 8719,
  ["prod"] = 8719,
  ["Coproduct"] = 8720,
  ["coprod"] = 8720,
  ["Sum"] = 8721,
  ["sum"] = 8721,
  ["minus"] = 8722,
  ["MinusPlus"] = 8723,
  ["mnplus"] = 8723,
  ["mp"] = 8723,
  ["dotplus"] = 8724,
  ["plusdo"] = 8724,
  ["Backslash"] = 8726,
  ["setminus"] = 8726,
  ["setmn"] = 8726,
  ["smallsetminus"] = 8726,
  ["ssetmn"] = 8726,
  ["lowast"] = 8727,
  ["SmallCircle"] = 8728,
  ["compfn"] = 8728,
  ["Sqrt"] = 8730,
  ["radic"] = 8730,
  ["Proportional"] = 8733,
  ["prop"] = 8733,
  ["propto"] = 8733,
  ["varpropto"] = 8733,
  ["vprop"] = 8733,
  ["infin"] = 8734,
  ["angrt"] = 8735,
  ["ang"] = 8736,
  ["angle"] = 8736,
  ["nang"] = {8736, 8402},
  ["angmsd"] = 8737,
  ["measuredangle"] = 8737,
  ["angsph"] = 8738,
  ["VerticalBar"] = 8739,
  ["mid"] = 8739,
  ["shortmid"] = 8739,
  ["smid"] = 8739,
  ["NotVerticalBar"] = 8740,
  ["nmid"] = 8740,
  ["nshortmid"] = 8740,
  ["nsmid"] = 8740,
  ["DoubleVerticalBar"] = 8741,
  ["par"] = 8741,
  ["parallel"] = 8741,
  ["shortparallel"] = 8741,
  ["spar"] = 8741,
  ["NotDoubleVerticalBar"] = 8742,
  ["npar"] = 8742,
  ["nparallel"] = 8742,
  ["nshortparallel"] = 8742,
  ["nspar"] = 8742,
  ["and"] = 8743,
  ["wedge"] = 8743,
  ["or"] = 8744,
  ["vee"] = 8744,
  ["cap"] = 8745,
  ["caps"] = {8745, 65024},
  ["cup"] = 8746,
  ["cups"] = {8746, 65024},
  ["Integral"] = 8747,
  ["int"] = 8747,
  ["Int"] = 8748,
  ["iiint"] = 8749,
  ["tint"] = 8749,
  ["ContourIntegral"] = 8750,
  ["conint"] = 8750,
  ["oint"] = 8750,
  ["Conint"] = 8751,
  ["DoubleContourIntegral"] = 8751,
  ["Cconint"] = 8752,
  ["cwint"] = 8753,
  ["ClockwiseContourIntegral"] = 8754,
  ["cwconint"] = 8754,
  ["CounterClockwiseContourIntegral"] = 8755,
  ["awconint"] = 8755,
  ["Therefore"] = 8756,
  ["there4"] = 8756,
  ["therefore"] = 8756,
  ["Because"] = 8757,
  ["becaus"] = 8757,
  ["because"] = 8757,
  ["ratio"] = 8758,
  ["Colon"] = 8759,
  ["Proportion"] = 8759,
  ["dotminus"] = 8760,
  ["minusd"] = 8760,
  ["mDDot"] = 8762,
  ["homtht"] = 8763,
  ["Tilde"] = 8764,
  ["nvsim"] = {8764, 8402},
  ["sim"] = 8764,
  ["thicksim"] = 8764,
  ["thksim"] = 8764,
  ["backsim"] = 8765,
  ["bsim"] = 8765,
  ["race"] = {8765, 817},
  ["ac"] = 8766,
  ["acE"] = {8766, 819},
  ["mstpos"] = 8766,
  ["acd"] = 8767,
  ["VerticalTilde"] = 8768,
  ["wr"] = 8768,
  ["wreath"] = 8768,
  ["NotTilde"] = 8769,
  ["nsim"] = 8769,
  ["EqualTilde"] = 8770,
  ["NotEqualTilde"] = {8770, 824},
  ["eqsim"] = 8770,
  ["esim"] = 8770,
  ["nesim"] = {8770, 824},
  ["TildeEqual"] = 8771,
  ["sime"] = 8771,
  ["simeq"] = 8771,
  ["NotTildeEqual"] = 8772,
  ["nsime"] = 8772,
  ["nsimeq"] = 8772,
  ["TildeFullEqual"] = 8773,
  ["cong"] = 8773,
  ["simne"] = 8774,
  ["NotTildeFullEqual"] = 8775,
  ["ncong"] = 8775,
  ["TildeTilde"] = 8776,
  ["ap"] = 8776,
  ["approx"] = 8776,
  ["asymp"] = 8776,
  ["thickapprox"] = 8776,
  ["thkap"] = 8776,
  ["NotTildeTilde"] = 8777,
  ["nap"] = 8777,
  ["napprox"] = 8777,
  ["ape"] = 8778,
  ["approxeq"] = 8778,
  ["apid"] = 8779,
  ["napid"] = {8779, 824},
  ["backcong"] = 8780,
  ["bcong"] = 8780,
  ["CupCap"] = 8781,
  ["asympeq"] = 8781,
  ["nvap"] = {8781, 8402},
  ["Bumpeq"] = 8782,
  ["HumpDownHump"] = 8782,
  ["NotHumpDownHump"] = {8782, 824},
  ["bump"] = 8782,
  ["nbump"] = {8782, 824},
  ["HumpEqual"] = 8783,
  ["NotHumpEqual"] = {8783, 824},
  ["bumpe"] = 8783,
  ["bumpeq"] = 8783,
  ["nbumpe"] = {8783, 824},
  ["DotEqual"] = 8784,
  ["doteq"] = 8784,
  ["esdot"] = 8784,
  ["nedot"] = {8784, 824},
  ["doteqdot"] = 8785,
  ["eDot"] = 8785,
  ["efDot"] = 8786,
  ["fallingdotseq"] = 8786,
  ["erDot"] = 8787,
  ["risingdotseq"] = 8787,
  ["Assign"] = 8788,
  ["colone"] = 8788,
  ["coloneq"] = 8788,
  ["ecolon"] = 8789,
  ["eqcolon"] = 8789,
  ["ecir"] = 8790,
  ["eqcirc"] = 8790,
  ["circeq"] = 8791,
  ["cire"] = 8791,
  ["wedgeq"] = 8793,
  ["veeeq"] = 8794,
  ["triangleq"] = 8796,
  ["trie"] = 8796,
  ["equest"] = 8799,
  ["questeq"] = 8799,
  ["NotEqual"] = 8800,
  ["ne"] = 8800,
  ["Congruent"] = 8801,
  ["bnequiv"] = {8801, 8421},
  ["equiv"] = 8801,
  ["NotCongruent"] = 8802,
  ["nequiv"] = 8802,
  ["le"] = 8804,
  ["leq"] = 8804,
  ["nvle"] = {8804, 8402},
  ["GreaterEqual"] = 8805,
  ["ge"] = 8805,
  ["geq"] = 8805,
  ["nvge"] = {8805, 8402},
  ["LessFullEqual"] = 8806,
  ["lE"] = 8806,
  ["leqq"] = 8806,
  ["nlE"] = {8806, 824},
  ["nleqq"] = {8806, 824},
  ["GreaterFullEqual"] = 8807,
  ["NotGreaterFullEqual"] = {8807, 824},
  ["gE"] = 8807,
  ["geqq"] = 8807,
  ["ngE"] = {8807, 824},
  ["ngeqq"] = {8807, 824},
  ["lnE"] = 8808,
  ["lneqq"] = 8808,
  ["lvertneqq"] = {8808, 65024},
  ["lvnE"] = {8808, 65024},
  ["gnE"] = 8809,
  ["gneqq"] = 8809,
  ["gvertneqq"] = {8809, 65024},
  ["gvnE"] = {8809, 65024},
  ["Lt"] = 8810,
  ["NestedLessLess"] = 8810,
  ["NotLessLess"] = {8810, 824},
  ["ll"] = 8810,
  ["nLt"] = {8810, 8402},
  ["nLtv"] = {8810, 824},
  ["Gt"] = 8811,
  ["NestedGreaterGreater"] = 8811,
  ["NotGreaterGreater"] = {8811, 824},
  ["gg"] = 8811,
  ["nGt"] = {8811, 8402},
  ["nGtv"] = {8811, 824},
  ["between"] = 8812,
  ["twixt"] = 8812,
  ["NotCupCap"] = 8813,
  ["NotLess"] = 8814,
  ["nless"] = 8814,
  ["nlt"] = 8814,
  ["NotGreater"] = 8815,
  ["ngt"] = 8815,
  ["ngtr"] = 8815,
  ["NotLessEqual"] = 8816,
  ["nle"] = 8816,
  ["nleq"] = 8816,
  ["NotGreaterEqual"] = 8817,
  ["nge"] = 8817,
  ["ngeq"] = 8817,
  ["LessTilde"] = 8818,
  ["lesssim"] = 8818,
  ["lsim"] = 8818,
  ["GreaterTilde"] = 8819,
  ["gsim"] = 8819,
  ["gtrsim"] = 8819,
  ["NotLessTilde"] = 8820,
  ["nlsim"] = 8820,
  ["NotGreaterTilde"] = 8821,
  ["ngsim"] = 8821,
  ["LessGreater"] = 8822,
  ["lessgtr"] = 8822,
  ["lg"] = 8822,
  ["GreaterLess"] = 8823,
  ["gl"] = 8823,
  ["gtrless"] = 8823,
  ["NotLessGreater"] = 8824,
  ["ntlg"] = 8824,
  ["NotGreaterLess"] = 8825,
  ["ntgl"] = 8825,
  ["Precedes"] = 8826,
  ["pr"] = 8826,
  ["prec"] = 8826,
  ["Succeeds"] = 8827,
  ["sc"] = 8827,
  ["succ"] = 8827,
  ["PrecedesSlantEqual"] = 8828,
  ["prcue"] = 8828,
  ["preccurlyeq"] = 8828,
  ["SucceedsSlantEqual"] = 8829,
  ["sccue"] = 8829,
  ["succcurlyeq"] = 8829,
  ["PrecedesTilde"] = 8830,
  ["precsim"] = 8830,
  ["prsim"] = 8830,
  ["NotSucceedsTilde"] = {8831, 824},
  ["SucceedsTilde"] = 8831,
  ["scsim"] = 8831,
  ["succsim"] = 8831,
  ["NotPrecedes"] = 8832,
  ["npr"] = 8832,
  ["nprec"] = 8832,
  ["NotSucceeds"] = 8833,
  ["nsc"] = 8833,
  ["nsucc"] = 8833,
  ["NotSubset"] = {8834, 8402},
  ["nsubset"] = {8834, 8402},
  ["sub"] = 8834,
  ["subset"] = 8834,
  ["vnsub"] = {8834, 8402},
  ["NotSuperset"] = {8835, 8402},
  ["Superset"] = 8835,
  ["nsupset"] = {8835, 8402},
  ["sup"] = 8835,
  ["supset"] = 8835,
  ["vnsup"] = {8835, 8402},
  ["nsub"] = 8836,
  ["nsup"] = 8837,
  ["SubsetEqual"] = 8838,
  ["sube"] = 8838,
  ["subseteq"] = 8838,
  ["SupersetEqual"] = 8839,
  ["supe"] = 8839,
  ["supseteq"] = 8839,
  ["NotSubsetEqual"] = 8840,
  ["nsube"] = 8840,
  ["nsubseteq"] = 8840,
  ["NotSupersetEqual"] = 8841,
  ["nsupe"] = 8841,
  ["nsupseteq"] = 8841,
  ["subne"] = 8842,
  ["subsetneq"] = 8842,
  ["varsubsetneq"] = {8842, 65024},
  ["vsubne"] = {8842, 65024},
  ["supne"] = 8843,
  ["supsetneq"] = 8843,
  ["varsupsetneq"] = {8843, 65024},
  ["vsupne"] = {8843, 65024},
  ["cupdot"] = 8845,
  ["UnionPlus"] = 8846,
  ["uplus"] = 8846,
  ["NotSquareSubset"] = {8847, 824},
  ["SquareSubset"] = 8847,
  ["sqsub"] = 8847,
  ["sqsubset"] = 8847,
  ["NotSquareSuperset"] = {8848, 824},
  ["SquareSuperset"] = 8848,
  ["sqsup"] = 8848,
  ["sqsupset"] = 8848,
  ["SquareSubsetEqual"] = 8849,
  ["sqsube"] = 8849,
  ["sqsubseteq"] = 8849,
  ["SquareSupersetEqual"] = 8850,
  ["sqsupe"] = 8850,
  ["sqsupseteq"] = 8850,
  ["SquareIntersection"] = 8851,
  ["sqcap"] = 8851,
  ["sqcaps"] = {8851, 65024},
  ["SquareUnion"] = 8852,
  ["sqcup"] = 8852,
  ["sqcups"] = {8852, 65024},
  ["CirclePlus"] = 8853,
  ["oplus"] = 8853,
  ["CircleMinus"] = 8854,
  ["ominus"] = 8854,
  ["CircleTimes"] = 8855,
  ["otimes"] = 8855,
  ["osol"] = 8856,
  ["CircleDot"] = 8857,
  ["odot"] = 8857,
  ["circledcirc"] = 8858,
  ["ocir"] = 8858,
  ["circledast"] = 8859,
  ["oast"] = 8859,
  ["circleddash"] = 8861,
  ["odash"] = 8861,
  ["boxplus"] = 8862,
  ["plusb"] = 8862,
  ["boxminus"] = 8863,
  ["minusb"] = 8863,
  ["boxtimes"] = 8864,
  ["timesb"] = 8864,
  ["dotsquare"] = 8865,
  ["sdotb"] = 8865,
  ["RightTee"] = 8866,
  ["vdash"] = 8866,
  ["LeftTee"] = 8867,
  ["dashv"] = 8867,
  ["DownTee"] = 8868,
  ["top"] = 8868,
  ["UpTee"] = 8869,
  ["bot"] = 8869,
  ["bottom"] = 8869,
  ["perp"] = 8869,
  ["models"] = 8871,
  ["DoubleRightTee"] = 8872,
  ["vDash"] = 8872,
  ["Vdash"] = 8873,
  ["Vvdash"] = 8874,
  ["VDash"] = 8875,
  ["nvdash"] = 8876,
  ["nvDash"] = 8877,
  ["nVdash"] = 8878,
  ["nVDash"] = 8879,
  ["prurel"] = 8880,
  ["LeftTriangle"] = 8882,
  ["vartriangleleft"] = 8882,
  ["vltri"] = 8882,
  ["RightTriangle"] = 8883,
  ["vartriangleright"] = 8883,
  ["vrtri"] = 8883,
  ["LeftTriangleEqual"] = 8884,
  ["ltrie"] = 8884,
  ["nvltrie"] = {8884, 8402},
  ["trianglelefteq"] = 8884,
  ["RightTriangleEqual"] = 8885,
  ["nvrtrie"] = {8885, 8402},
  ["rtrie"] = 8885,
  ["trianglerighteq"] = 8885,
  ["origof"] = 8886,
  ["imof"] = 8887,
  ["multimap"] = 8888,
  ["mumap"] = 8888,
  ["hercon"] = 8889,
  ["intcal"] = 8890,
  ["intercal"] = 8890,
  ["veebar"] = 8891,
  ["barvee"] = 8893,
  ["angrtvb"] = 8894,
  ["lrtri"] = 8895,
  ["Wedge"] = 8896,
  ["bigwedge"] = 8896,
  ["xwedge"] = 8896,
  ["Vee"] = 8897,
  ["bigvee"] = 8897,
  ["xvee"] = 8897,
  ["Intersection"] = 8898,
  ["bigcap"] = 8898,
  ["xcap"] = 8898,
  ["Union"] = 8899,
  ["bigcup"] = 8899,
  ["xcup"] = 8899,
  ["Diamond"] = 8900,
  ["diam"] = 8900,
  ["diamond"] = 8900,
  ["sdot"] = 8901,
  ["Star"] = 8902,
  ["sstarf"] = 8902,
  ["divideontimes"] = 8903,
  ["divonx"] = 8903,
  ["bowtie"] = 8904,
  ["ltimes"] = 8905,
  ["rtimes"] = 8906,
  ["leftthreetimes"] = 8907,
  ["lthree"] = 8907,
  ["rightthreetimes"] = 8908,
  ["rthree"] = 8908,
  ["backsimeq"] = 8909,
  ["bsime"] = 8909,
  ["curlyvee"] = 8910,
  ["cuvee"] = 8910,
  ["curlywedge"] = 8911,
  ["cuwed"] = 8911,
  ["Sub"] = 8912,
  ["Subset"] = 8912,
  ["Sup"] = 8913,
  ["Supset"] = 8913,
  ["Cap"] = 8914,
  ["Cup"] = 8915,
  ["fork"] = 8916,
  ["pitchfork"] = 8916,
  ["epar"] = 8917,
  ["lessdot"] = 8918,
  ["ltdot"] = 8918,
  ["gtdot"] = 8919,
  ["gtrdot"] = 8919,
  ["Ll"] = 8920,
  ["nLl"] = {8920, 824},
  ["Gg"] = 8921,
  ["ggg"] = 8921,
  ["nGg"] = {8921, 824},
  ["LessEqualGreater"] = 8922,
  ["leg"] = 8922,
  ["lesg"] = {8922, 65024},
  ["lesseqgtr"] = 8922,
  ["GreaterEqualLess"] = 8923,
  ["gel"] = 8923,
  ["gesl"] = {8923, 65024},
  ["gtreqless"] = 8923,
  ["cuepr"] = 8926,
  ["curlyeqprec"] = 8926,
  ["cuesc"] = 8927,
  ["curlyeqsucc"] = 8927,
  ["NotPrecedesSlantEqual"] = 8928,
  ["nprcue"] = 8928,
  ["NotSucceedsSlantEqual"] = 8929,
  ["nsccue"] = 8929,
  ["NotSquareSubsetEqual"] = 8930,
  ["nsqsube"] = 8930,
  ["NotSquareSupersetEqual"] = 8931,
  ["nsqsupe"] = 8931,
  ["lnsim"] = 8934,
  ["gnsim"] = 8935,
  ["precnsim"] = 8936,
  ["prnsim"] = 8936,
  ["scnsim"] = 8937,
  ["succnsim"] = 8937,
  ["NotLeftTriangle"] = 8938,
  ["nltri"] = 8938,
  ["ntriangleleft"] = 8938,
  ["NotRightTriangle"] = 8939,
  ["nrtri"] = 8939,
  ["ntriangleright"] = 8939,
  ["NotLeftTriangleEqual"] = 8940,
  ["nltrie"] = 8940,
  ["ntrianglelefteq"] = 8940,
  ["NotRightTriangleEqual"] = 8941,
  ["nrtrie"] = 8941,
  ["ntrianglerighteq"] = 8941,
  ["vellip"] = 8942,
  ["ctdot"] = 8943,
  ["utdot"] = 8944,
  ["dtdot"] = 8945,
  ["disin"] = 8946,
  ["isinsv"] = 8947,
  ["isins"] = 8948,
  ["isindot"] = 8949,
  ["notindot"] = {8949, 824},
  ["notinvc"] = 8950,
  ["notinvb"] = 8951,
  ["isinE"] = 8953,
  ["notinE"] = {8953, 824},
  ["nisd"] = 8954,
  ["xnis"] = 8955,
  ["nis"] = 8956,
  ["notnivc"] = 8957,
  ["notnivb"] = 8958,
  ["barwed"] = 8965,
  ["barwedge"] = 8965,
  ["Barwed"] = 8966,
  ["doublebarwedge"] = 8966,
  ["LeftCeiling"] = 8968,
  ["lceil"] = 8968,
  ["RightCeiling"] = 8969,
  ["rceil"] = 8969,
  ["LeftFloor"] = 8970,
  ["lfloor"] = 8970,
  ["RightFloor"] = 8971,
  ["rfloor"] = 8971,
  ["drcrop"] = 8972,
  ["dlcrop"] = 8973,
  ["urcrop"] = 8974,
  ["ulcrop"] = 8975,
  ["bnot"] = 8976,
  ["profline"] = 8978,
  ["profsurf"] = 8979,
  ["telrec"] = 8981,
  ["target"] = 8982,
  ["ulcorn"] = 8988,
  ["ulcorner"] = 8988,
  ["urcorn"] = 8989,
  ["urcorner"] = 8989,
  ["dlcorn"] = 8990,
  ["llcorner"] = 8990,
  ["drcorn"] = 8991,
  ["lrcorner"] = 8991,
  ["frown"] = 8994,
  ["sfrown"] = 8994,
  ["smile"] = 8995,
  ["ssmile"] = 8995,
  ["cylcty"] = 9005,
  ["profalar"] = 9006,
  ["topbot"] = 9014,
  ["ovbar"] = 9021,
  ["solbar"] = 9023,
  ["angzarr"] = 9084,
  ["lmoust"] = 9136,
  ["lmoustache"] = 9136,
  ["rmoust"] = 9137,
  ["rmoustache"] = 9137,
  ["OverBracket"] = 9140,
  ["tbrk"] = 9140,
  ["UnderBracket"] = 9141,
  ["bbrk"] = 9141,
  ["bbrktbrk"] = 9142,
  ["OverParenthesis"] = 9180,
  ["UnderParenthesis"] = 9181,
  ["OverBrace"] = 9182,
  ["UnderBrace"] = 9183,
  ["trpezium"] = 9186,
  ["elinters"] = 9191,
  ["blank"] = 9251,
  ["circledS"] = 9416,
  ["oS"] = 9416,
  ["HorizontalLine"] = 9472,
  ["boxh"] = 9472,
  ["boxv"] = 9474,
  ["boxdr"] = 9484,
  ["boxdl"] = 9488,
  ["boxur"] = 9492,
  ["boxul"] = 9496,
  ["boxvr"] = 9500,
  ["boxvl"] = 9508,
  ["boxhd"] = 9516,
  ["boxhu"] = 9524,
  ["boxvh"] = 9532,
  ["boxH"] = 9552,
  ["boxV"] = 9553,
  ["boxdR"] = 9554,
  ["boxDr"] = 9555,
  ["boxDR"] = 9556,
  ["boxdL"] = 9557,
  ["boxDl"] = 9558,
  ["boxDL"] = 9559,
  ["boxuR"] = 9560,
  ["boxUr"] = 9561,
  ["boxUR"] = 9562,
  ["boxuL"] = 9563,
  ["boxUl"] = 9564,
  ["boxUL"] = 9565,
  ["boxvR"] = 9566,
  ["boxVr"] = 9567,
  ["boxVR"] = 9568,
  ["boxvL"] = 9569,
  ["boxVl"] = 9570,
  ["boxVL"] = 9571,
  ["boxHd"] = 9572,
  ["boxhD"] = 9573,
  ["boxHD"] = 9574,
  ["boxHu"] = 9575,
  ["boxhU"] = 9576,
  ["boxHU"] = 9577,
  ["boxvH"] = 9578,
  ["boxVh"] = 9579,
  ["boxVH"] = 9580,
  ["uhblk"] = 9600,
  ["lhblk"] = 9604,
  ["block"] = 9608,
  ["blk14"] = 9617,
  ["blk12"] = 9618,
  ["blk34"] = 9619,
  ["Square"] = 9633,
  ["squ"] = 9633,
  ["square"] = 9633,
  ["FilledVerySmallSquare"] = 9642,
  ["blacksquare"] = 9642,
  ["squarf"] = 9642,
  ["squf"] = 9642,
  ["EmptyVerySmallSquare"] = 9643,
  ["rect"] = 9645,
  ["marker"] = 9646,
  ["fltns"] = 9649,
  ["bigtriangleup"] = 9651,
  ["xutri"] = 9651,
  ["blacktriangle"] = 9652,
  ["utrif"] = 9652,
  ["triangle"] = 9653,
  ["utri"] = 9653,
  ["blacktriangleright"] = 9656,
  ["rtrif"] = 9656,
  ["rtri"] = 9657,
  ["triangleright"] = 9657,
  ["bigtriangledown"] = 9661,
  ["xdtri"] = 9661,
  ["blacktriangledown"] = 9662,
  ["dtrif"] = 9662,
  ["dtri"] = 9663,
  ["triangledown"] = 9663,
  ["blacktriangleleft"] = 9666,
  ["ltrif"] = 9666,
  ["ltri"] = 9667,
  ["triangleleft"] = 9667,
  ["loz"] = 9674,
  ["lozenge"] = 9674,
  ["cir"] = 9675,
  ["tridot"] = 9708,
  ["bigcirc"] = 9711,
  ["xcirc"] = 9711,
  ["ultri"] = 9720,
  ["urtri"] = 9721,
  ["lltri"] = 9722,
  ["EmptySmallSquare"] = 9723,
  ["FilledSmallSquare"] = 9724,
  ["bigstar"] = 9733,
  ["starf"] = 9733,
  ["star"] = 9734,
  ["phone"] = 9742,
  ["female"] = 9792,
  ["male"] = 9794,
  ["spades"] = 9824,
  ["spadesuit"] = 9824,
  ["clubs"] = 9827,
  ["clubsuit"] = 9827,
  ["hearts"] = 9829,
  ["heartsuit"] = 9829,
  ["diamondsuit"] = 9830,
  ["diams"] = 9830,
  ["sung"] = 9834,
  ["flat"] = 9837,
  ["natur"] = 9838,
  ["natural"] = 9838,
  ["sharp"] = 9839,
  ["check"] = 10003,
  ["checkmark"] = 10003,
  ["cross"] = 10007,
  ["malt"] = 10016,
  ["maltese"] = 10016,
  ["sext"] = 10038,
  ["VerticalSeparator"] = 10072,
  ["lbbrk"] = 10098,
  ["rbbrk"] = 10099,
  ["bsolhsub"] = 10184,
  ["suphsol"] = 10185,
  ["LeftDoubleBracket"] = 10214,
  ["lobrk"] = 10214,
  ["RightDoubleBracket"] = 10215,
  ["robrk"] = 10215,
  ["LeftAngleBracket"] = 10216,
  ["lang"] = 10216,
  ["langle"] = 10216,
  ["RightAngleBracket"] = 10217,
  ["rang"] = 10217,
  ["rangle"] = 10217,
  ["Lang"] = 10218,
  ["Rang"] = 10219,
  ["loang"] = 10220,
  ["roang"] = 10221,
  ["LongLeftArrow"] = 10229,
  ["longleftarrow"] = 10229,
  ["xlarr"] = 10229,
  ["LongRightArrow"] = 10230,
  ["longrightarrow"] = 10230,
  ["xrarr"] = 10230,
  ["LongLeftRightArrow"] = 10231,
  ["longleftrightarrow"] = 10231,
  ["xharr"] = 10231,
  ["DoubleLongLeftArrow"] = 10232,
  ["Longleftarrow"] = 10232,
  ["xlArr"] = 10232,
  ["DoubleLongRightArrow"] = 10233,
  ["Longrightarrow"] = 10233,
  ["xrArr"] = 10233,
  ["DoubleLongLeftRightArrow"] = 10234,
  ["Longleftrightarrow"] = 10234,
  ["xhArr"] = 10234,
  ["longmapsto"] = 10236,
  ["xmap"] = 10236,
  ["dzigrarr"] = 10239,
  ["nvlArr"] = 10498,
  ["nvrArr"] = 10499,
  ["nvHarr"] = 10500,
  ["Map"] = 10501,
  ["lbarr"] = 10508,
  ["bkarow"] = 10509,
  ["rbarr"] = 10509,
  ["lBarr"] = 10510,
  ["dbkarow"] = 10511,
  ["rBarr"] = 10511,
  ["RBarr"] = 10512,
  ["drbkarow"] = 10512,
  ["DDotrahd"] = 10513,
  ["UpArrowBar"] = 10514,
  ["DownArrowBar"] = 10515,
  ["Rarrtl"] = 10518,
  ["latail"] = 10521,
  ["ratail"] = 10522,
  ["lAtail"] = 10523,
  ["rAtail"] = 10524,
  ["larrfs"] = 10525,
  ["rarrfs"] = 10526,
  ["larrbfs"] = 10527,
  ["rarrbfs"] = 10528,
  ["nwarhk"] = 10531,
  ["nearhk"] = 10532,
  ["hksearow"] = 10533,
  ["searhk"] = 10533,
  ["hkswarow"] = 10534,
  ["swarhk"] = 10534,
  ["nwnear"] = 10535,
  ["nesear"] = 10536,
  ["toea"] = 10536,
  ["seswar"] = 10537,
  ["tosa"] = 10537,
  ["swnwar"] = 10538,
  ["nrarrc"] = {10547, 824},
  ["rarrc"] = 10547,
  ["cudarrr"] = 10549,
  ["ldca"] = 10550,
  ["rdca"] = 10551,
  ["cudarrl"] = 10552,
  ["larrpl"] = 10553,
  ["curarrm"] = 10556,
  ["cularrp"] = 10557,
  ["rarrpl"] = 10565,
  ["harrcir"] = 10568,
  ["Uarrocir"] = 10569,
  ["lurdshar"] = 10570,
  ["ldrushar"] = 10571,
  ["LeftRightVector"] = 10574,
  ["RightUpDownVector"] = 10575,
  ["DownLeftRightVector"] = 10576,
  ["LeftUpDownVector"] = 10577,
  ["LeftVectorBar"] = 10578,
  ["RightVectorBar"] = 10579,
  ["RightUpVectorBar"] = 10580,
  ["RightDownVectorBar"] = 10581,
  ["DownLeftVectorBar"] = 10582,
  ["DownRightVectorBar"] = 10583,
  ["LeftUpVectorBar"] = 10584,
  ["LeftDownVectorBar"] = 10585,
  ["LeftTeeVector"] = 10586,
  ["RightTeeVector"] = 10587,
  ["RightUpTeeVector"] = 10588,
  ["RightDownTeeVector"] = 10589,
  ["DownLeftTeeVector"] = 10590,
  ["DownRightTeeVector"] = 10591,
  ["LeftUpTeeVector"] = 10592,
  ["LeftDownTeeVector"] = 10593,
  ["lHar"] = 10594,
  ["uHar"] = 10595,
  ["rHar"] = 10596,
  ["dHar"] = 10597,
  ["luruhar"] = 10598,
  ["ldrdhar"] = 10599,
  ["ruluhar"] = 10600,
  ["rdldhar"] = 10601,
  ["lharul"] = 10602,
  ["llhard"] = 10603,
  ["rharul"] = 10604,
  ["lrhard"] = 10605,
  ["UpEquilibrium"] = 10606,
  ["udhar"] = 10606,
  ["ReverseUpEquilibrium"] = 10607,
  ["duhar"] = 10607,
  ["RoundImplies"] = 10608,
  ["erarr"] = 10609,
  ["simrarr"] = 10610,
  ["larrsim"] = 10611,
  ["rarrsim"] = 10612,
  ["rarrap"] = 10613,
  ["ltlarr"] = 10614,
  ["gtrarr"] = 10616,
  ["subrarr"] = 10617,
  ["suplarr"] = 10619,
  ["lfisht"] = 10620,
  ["rfisht"] = 10621,
  ["ufisht"] = 10622,
  ["dfisht"] = 10623,
  ["lopar"] = 10629,
  ["ropar"] = 10630,
  ["lbrke"] = 10635,
  ["rbrke"] = 10636,
  ["lbrkslu"] = 10637,
  ["rbrksld"] = 10638,
  ["lbrksld"] = 10639,
  ["rbrkslu"] = 10640,
  ["langd"] = 10641,
  ["rangd"] = 10642,
  ["lparlt"] = 10643,
  ["rpargt"] = 10644,
  ["gtlPar"] = 10645,
  ["ltrPar"] = 10646,
  ["vzigzag"] = 10650,
  ["vangrt"] = 10652,
  ["angrtvbd"] = 10653,
  ["ange"] = 10660,
  ["range"] = 10661,
  ["dwangle"] = 10662,
  ["uwangle"] = 10663,
  ["angmsdaa"] = 10664,
  ["angmsdab"] = 10665,
  ["angmsdac"] = 10666,
  ["angmsdad"] = 10667,
  ["angmsdae"] = 10668,
  ["angmsdaf"] = 10669,
  ["angmsdag"] = 10670,
  ["angmsdah"] = 10671,
  ["bemptyv"] = 10672,
  ["demptyv"] = 10673,
  ["cemptyv"] = 10674,
  ["raemptyv"] = 10675,
  ["laemptyv"] = 10676,
  ["ohbar"] = 10677,
  ["omid"] = 10678,
  ["opar"] = 10679,
  ["operp"] = 10681,
  ["olcross"] = 10683,
  ["odsold"] = 10684,
  ["olcir"] = 10686,
  ["ofcir"] = 10687,
  ["olt"] = 10688,
  ["ogt"] = 10689,
  ["cirscir"] = 10690,
  ["cirE"] = 10691,
  ["solb"] = 10692,
  ["bsolb"] = 10693,
  ["boxbox"] = 10697,
  ["trisb"] = 10701,
  ["rtriltri"] = 10702,
  ["LeftTriangleBar"] = 10703,
  ["NotLeftTriangleBar"] = {10703, 824},
  ["NotRightTriangleBar"] = {10704, 824},
  ["RightTriangleBar"] = 10704,
  ["iinfin"] = 10716,
  ["infintie"] = 10717,
  ["nvinfin"] = 10718,
  ["eparsl"] = 10723,
  ["smeparsl"] = 10724,
  ["eqvparsl"] = 10725,
  ["blacklozenge"] = 10731,
  ["lozf"] = 10731,
  ["RuleDelayed"] = 10740,
  ["dsol"] = 10742,
  ["bigodot"] = 10752,
  ["xodot"] = 10752,
  ["bigoplus"] = 10753,
  ["xoplus"] = 10753,
  ["bigotimes"] = 10754,
  ["xotime"] = 10754,
  ["biguplus"] = 10756,
  ["xuplus"] = 10756,
  ["bigsqcup"] = 10758,
  ["xsqcup"] = 10758,
  ["iiiint"] = 10764,
  ["qint"] = 10764,
  ["fpartint"] = 10765,
  ["cirfnint"] = 10768,
  ["awint"] = 10769,
  ["rppolint"] = 10770,
  ["scpolint"] = 10771,
  ["npolint"] = 10772,
  ["pointint"] = 10773,
  ["quatint"] = 10774,
  ["intlarhk"] = 10775,
  ["pluscir"] = 10786,
  ["plusacir"] = 10787,
  ["simplus"] = 10788,
  ["plusdu"] = 10789,
  ["plussim"] = 10790,
  ["plustwo"] = 10791,
  ["mcomma"] = 10793,
  ["minusdu"] = 10794,
  ["loplus"] = 10797,
  ["roplus"] = 10798,
  ["Cross"] = 10799,
  ["timesd"] = 10800,
  ["timesbar"] = 10801,
  ["smashp"] = 10803,
  ["lotimes"] = 10804,
  ["rotimes"] = 10805,
  ["otimesas"] = 10806,
  ["Otimes"] = 10807,
  ["odiv"] = 10808,
  ["triplus"] = 10809,
  ["triminus"] = 10810,
  ["tritime"] = 10811,
  ["intprod"] = 10812,
  ["iprod"] = 10812,
  ["amalg"] = 10815,
  ["capdot"] = 10816,
  ["ncup"] = 10818,
  ["ncap"] = 10819,
  ["capand"] = 10820,
  ["cupor"] = 10821,
  ["cupcap"] = 10822,
  ["capcup"] = 10823,
  ["cupbrcap"] = 10824,
  ["capbrcup"] = 10825,
  ["cupcup"] = 10826,
  ["capcap"] = 10827,
  ["ccups"] = 10828,
  ["ccaps"] = 10829,
  ["ccupssm"] = 10832,
  ["And"] = 10835,
  ["Or"] = 10836,
  ["andand"] = 10837,
  ["oror"] = 10838,
  ["orslope"] = 10839,
  ["andslope"] = 10840,
  ["andv"] = 10842,
  ["orv"] = 10843,
  ["andd"] = 10844,
  ["ord"] = 10845,
  ["wedbar"] = 10847,
  ["sdote"] = 10854,
  ["simdot"] = 10858,
  ["congdot"] = 10861,
  ["ncongdot"] = {10861, 824},
  ["easter"] = 10862,
  ["apacir"] = 10863,
  ["apE"] = 10864,
  ["napE"] = {10864, 824},
  ["eplus"] = 10865,
  ["pluse"] = 10866,
  ["Esim"] = 10867,
  ["Colone"] = 10868,
  ["Equal"] = 10869,
  ["ddotseq"] = 10871,
  ["eDDot"] = 10871,
  ["equivDD"] = 10872,
  ["ltcir"] = 10873,
  ["gtcir"] = 10874,
  ["ltquest"] = 10875,
  ["gtquest"] = 10876,
  ["LessSlantEqual"] = 10877,
  ["NotLessSlantEqual"] = {10877, 824},
  ["leqslant"] = 10877,
  ["les"] = 10877,
  ["nleqslant"] = {10877, 824},
  ["nles"] = {10877, 824},
  ["GreaterSlantEqual"] = 10878,
  ["NotGreaterSlantEqual"] = {10878, 824},
  ["geqslant"] = 10878,
  ["ges"] = 10878,
  ["ngeqslant"] = {10878, 824},
  ["nges"] = {10878, 824},
  ["lesdot"] = 10879,
  ["gesdot"] = 10880,
  ["lesdoto"] = 10881,
  ["gesdoto"] = 10882,
  ["lesdotor"] = 10883,
  ["gesdotol"] = 10884,
  ["lap"] = 10885,
  ["lessapprox"] = 10885,
  ["gap"] = 10886,
  ["gtrapprox"] = 10886,
  ["lne"] = 10887,
  ["lneq"] = 10887,
  ["gne"] = 10888,
  ["gneq"] = 10888,
  ["lnap"] = 10889,
  ["lnapprox"] = 10889,
  ["gnap"] = 10890,
  ["gnapprox"] = 10890,
  ["lEg"] = 10891,
  ["lesseqqgtr"] = 10891,
  ["gEl"] = 10892,
  ["gtreqqless"] = 10892,
  ["lsime"] = 10893,
  ["gsime"] = 10894,
  ["lsimg"] = 10895,
  ["gsiml"] = 10896,
  ["lgE"] = 10897,
  ["glE"] = 10898,
  ["lesges"] = 10899,
  ["gesles"] = 10900,
  ["els"] = 10901,
  ["eqslantless"] = 10901,
  ["egs"] = 10902,
  ["eqslantgtr"] = 10902,
  ["elsdot"] = 10903,
  ["egsdot"] = 10904,
  ["el"] = 10905,
  ["eg"] = 10906,
  ["siml"] = 10909,
  ["simg"] = 10910,
  ["simlE"] = 10911,
  ["simgE"] = 10912,
  ["LessLess"] = 10913,
  ["NotNestedLessLess"] = {10913, 824},
  ["GreaterGreater"] = 10914,
  ["NotNestedGreaterGreater"] = {10914, 824},
  ["glj"] = 10916,
  ["gla"] = 10917,
  ["ltcc"] = 10918,
  ["gtcc"] = 10919,
  ["lescc"] = 10920,
  ["gescc"] = 10921,
  ["smt"] = 10922,
  ["lat"] = 10923,
  ["smte"] = 10924,
  ["smtes"] = {10924, 65024},
  ["late"] = 10925,
  ["lates"] = {10925, 65024},
  ["bumpE"] = 10926,
  ["NotPrecedesEqual"] = {10927, 824},
  ["PrecedesEqual"] = 10927,
  ["npre"] = {10927, 824},
  ["npreceq"] = {10927, 824},
  ["pre"] = 10927,
  ["preceq"] = 10927,
  ["NotSucceedsEqual"] = {10928, 824},
  ["SucceedsEqual"] = 10928,
  ["nsce"] = {10928, 824},
  ["nsucceq"] = {10928, 824},
  ["sce"] = 10928,
  ["succeq"] = 10928,
  ["prE"] = 10931,
  ["scE"] = 10932,
  ["precneqq"] = 10933,
  ["prnE"] = 10933,
  ["scnE"] = 10934,
  ["succneqq"] = 10934,
  ["prap"] = 10935,
  ["precapprox"] = 10935,
  ["scap"] = 10936,
  ["succapprox"] = 10936,
  ["precnapprox"] = 10937,
  ["prnap"] = 10937,
  ["scnap"] = 10938,
  ["succnapprox"] = 10938,
  ["Pr"] = 10939,
  ["Sc"] = 10940,
  ["subdot"] = 10941,
  ["supdot"] = 10942,
  ["subplus"] = 10943,
  ["supplus"] = 10944,
  ["submult"] = 10945,
  ["supmult"] = 10946,
  ["subedot"] = 10947,
  ["supedot"] = 10948,
  ["nsubE"] = {10949, 824},
  ["nsubseteqq"] = {10949, 824},
  ["subE"] = 10949,
  ["subseteqq"] = 10949,
  ["nsupE"] = {10950, 824},
  ["nsupseteqq"] = {10950, 824},
  ["supE"] = 10950,
  ["supseteqq"] = 10950,
  ["subsim"] = 10951,
  ["supsim"] = 10952,
  ["subnE"] = 10955,
  ["subsetneqq"] = 10955,
  ["varsubsetneqq"] = {10955, 65024},
  ["vsubnE"] = {10955, 65024},
  ["supnE"] = 10956,
  ["supsetneqq"] = 10956,
  ["varsupsetneqq"] = {10956, 65024},
  ["vsupnE"] = {10956, 65024},
  ["csub"] = 10959,
  ["csup"] = 10960,
  ["csube"] = 10961,
  ["csupe"] = 10962,
  ["subsup"] = 10963,
  ["supsub"] = 10964,
  ["subsub"] = 10965,
  ["supsup"] = 10966,
  ["suphsub"] = 10967,
  ["supdsub"] = 10968,
  ["forkv"] = 10969,
  ["topfork"] = 10970,
  ["mlcp"] = 10971,
  ["Dashv"] = 10980,
  ["DoubleLeftTee"] = 10980,
  ["Vdashl"] = 10982,
  ["Barv"] = 10983,
  ["vBar"] = 10984,
  ["vBarv"] = 10985,
  ["Vbar"] = 10987,
  ["Not"] = 10988,
  ["bNot"] = 10989,
  ["rnmid"] = 10990,
  ["cirmid"] = 10991,
  ["midcir"] = 10992,
  ["topcir"] = 10993,
  ["nhpar"] = 10994,
  ["parsim"] = 10995,
  ["nparsl"] = {11005, 8421},
  ["parsl"] = 11005,
  ["fflig"] = 64256,
  ["filig"] = 64257,
  ["fllig"] = 64258,
  ["ffilig"] = 64259,
  ["ffllig"] = 64260,
  ["Ascr"] = 119964,
  ["Cscr"] = 119966,
  ["Dscr"] = 119967,
  ["Gscr"] = 119970,
  ["Jscr"] = 119973,
  ["Kscr"] = 119974,
  ["Nscr"] = 119977,
  ["Oscr"] = 119978,
  ["Pscr"] = 119979,
  ["Qscr"] = 119980,
  ["Sscr"] = 119982,
  ["Tscr"] = 119983,
  ["Uscr"] = 119984,
  ["Vscr"] = 119985,
  ["Wscr"] = 119986,
  ["Xscr"] = 119987,
  ["Yscr"] = 119988,
  ["Zscr"] = 119989,
  ["ascr"] = 119990,
  ["bscr"] = 119991,
  ["cscr"] = 119992,
  ["dscr"] = 119993,
  ["fscr"] = 119995,
  ["hscr"] = 119997,
  ["iscr"] = 119998,
  ["jscr"] = 119999,
  ["kscr"] = 120000,
  ["lscr"] = 120001,
  ["mscr"] = 120002,
  ["nscr"] = 120003,
  ["pscr"] = 120005,
  ["qscr"] = 120006,
  ["rscr"] = 120007,
  ["sscr"] = 120008,
  ["tscr"] = 120009,
  ["uscr"] = 120010,
  ["vscr"] = 120011,
  ["wscr"] = 120012,
  ["xscr"] = 120013,
  ["yscr"] = 120014,
  ["zscr"] = 120015,
  ["Afr"] = 120068,
  ["Bfr"] = 120069,
  ["Dfr"] = 120071,
  ["Efr"] = 120072,
  ["Ffr"] = 120073,
  ["Gfr"] = 120074,
  ["Jfr"] = 120077,
  ["Kfr"] = 120078,
  ["Lfr"] = 120079,
  ["Mfr"] = 120080,
  ["Nfr"] = 120081,
  ["Ofr"] = 120082,
  ["Pfr"] = 120083,
  ["Qfr"] = 120084,
  ["Sfr"] = 120086,
  ["Tfr"] = 120087,
  ["Ufr"] = 120088,
  ["Vfr"] = 120089,
  ["Wfr"] = 120090,
  ["Xfr"] = 120091,
  ["Yfr"] = 120092,
  ["afr"] = 120094,
  ["bfr"] = 120095,
  ["cfr"] = 120096,
  ["dfr"] = 120097,
  ["efr"] = 120098,
  ["ffr"] = 120099,
  ["gfr"] = 120100,
  ["hfr"] = 120101,
  ["ifr"] = 120102,
  ["jfr"] = 120103,
  ["kfr"] = 120104,
  ["lfr"] = 120105,
  ["mfr"] = 120106,
  ["nfr"] = 120107,
  ["ofr"] = 120108,
  ["pfr"] = 120109,
  ["qfr"] = 120110,
  ["rfr"] = 120111,
  ["sfr"] = 120112,
  ["tfr"] = 120113,
  ["ufr"] = 120114,
  ["vfr"] = 120115,
  ["wfr"] = 120116,
  ["xfr"] = 120117,
  ["yfr"] = 120118,
  ["zfr"] = 120119,
  ["Aopf"] = 120120,
  ["Bopf"] = 120121,
  ["Dopf"] = 120123,
  ["Eopf"] = 120124,
  ["Fopf"] = 120125,
  ["Gopf"] = 120126,
  ["Iopf"] = 120128,
  ["Jopf"] = 120129,
  ["Kopf"] = 120130,
  ["Lopf"] = 120131,
  ["Mopf"] = 120132,
  ["Oopf"] = 120134,
  ["Sopf"] = 120138,
  ["Topf"] = 120139,
  ["Uopf"] = 120140,
  ["Vopf"] = 120141,
  ["Wopf"] = 120142,
  ["Xopf"] = 120143,
  ["Yopf"] = 120144,
  ["aopf"] = 120146,
  ["bopf"] = 120147,
  ["copf"] = 120148,
  ["dopf"] = 120149,
  ["eopf"] = 120150,
  ["fopf"] = 120151,
  ["gopf"] = 120152,
  ["hopf"] = 120153,
  ["iopf"] = 120154,
  ["jopf"] = 120155,
  ["kopf"] = 120156,
  ["lopf"] = 120157,
  ["mopf"] = 120158,
  ["nopf"] = 120159,
  ["oopf"] = 120160,
  ["popf"] = 120161,
  ["qopf"] = 120162,
  ["ropf"] = 120163,
  ["sopf"] = 120164,
  ["topf"] = 120165,
  ["uopf"] = 120166,
  ["vopf"] = 120167,
  ["wopf"] = 120168,
  ["xopf"] = 120169,
  ["yopf"] = 120170,
  ["zopf"] = 120171,
}
function entities.dec_entity(s)
  local n = tonumber(s)
  if n == nil then
    return "&#" .. s .. ";"  -- fallback for unknown entities
  end
  return unicode.utf8.char(n)
end
function entities.hex_entity(s)
  local n = tonumber("0x"..s)
  if n == nil then
    return "&#x" .. s .. ";"  -- fallback for unknown entities
  end
  return unicode.utf8.char(n)
end
function entities.hex_entity_with_x_char(x, s)
  local n = tonumber("0x"..s)
  if n == nil then
    return "&#" .. x .. s .. ";"  -- fallback for unknown entities
  end
  return unicode.utf8.char(n)
end
function entities.char_entity(s)
  local code_points = character_entities[s]
  if code_points == nil then
    return "&" .. s .. ";"
  end
  if type(code_points) ~= 'table' then
    code_points = {code_points}
  end
  local char_table = {}
    for _, code_point in ipairs(code_points) do
      table.insert(char_table, unicode.utf8.char(code_point))
    end
  return table.concat(char_table)
end
M.writer = {}
function M.writer.new(options)
  local self = {}
  self.options = options
  self.flatten_inlines = false
  local slice_specifiers = {}
  for specifier in options.slice:gmatch("[^%s]+") do
    table.insert(slice_specifiers, specifier)
  end

  if #slice_specifiers == 2 then
    self.slice_begin, self.slice_end = table.unpack(slice_specifiers)
    local slice_begin_type = self.slice_begin:sub(1, 1)
    if slice_begin_type ~= "^" and slice_begin_type ~= "$" then
      self.slice_begin = "^" .. self.slice_begin
    end
    local slice_end_type = self.slice_end:sub(1, 1)
    if slice_end_type ~= "^" and slice_end_type ~= "$" then
      self.slice_end = "$" .. self.slice_end
    end
  elseif #slice_specifiers == 1 then
    self.slice_begin = "^" .. slice_specifiers[1]
    self.slice_end = "$" .. slice_specifiers[1]
  end

  self.slice_begin_type = self.slice_begin:sub(1, 1)
  self.slice_begin_identifier = self.slice_begin:sub(2) or ""
  self.slice_end_type = self.slice_end:sub(1, 1)
  self.slice_end_identifier = self.slice_end:sub(2) or ""

  if self.slice_begin == "^" and self.slice_end ~= "^" then
    self.is_writing = true
  else
    self.is_writing = false
  end
  self.suffix = ".tex"
  self.space = " "
  self.nbsp = "\\markdownRendererNbsp{}"
  function self.plain(s)
    return s
  end
  function self.paragraph(s)
    if not self.is_writing then return "" end
    return s
  end
  function self.pack(name)
    return [[\input{]] .. name .. [[}\relax]]
  end
  self.interblocksep_text = "\\markdownRendererInterblockSeparator\n{}"
  function self.interblocksep()
    if not self.is_writing then return "" end
    return self.interblocksep_text
  end
  self.paragraphsep_text = "\\markdownRendererParagraphSeparator\n{}"
  function self.paragraphsep()
    if not self.is_writing then return "" end
    return self.paragraphsep_text
  end
  self.undosep_text = "\\markdownRendererUndoSeparator\n{}"
  function self.undosep()
    if not self.is_writing then return "" end
    return self.undosep_text
  end
  self.soft_line_break = function()
    if self.flatten_inlines then return "\n" end
    return "\\markdownRendererSoftLineBreak\n{}"
  end
  self.hard_line_break = function()
    if self.flatten_inlines then return "\n" end
    return "\\markdownRendererHardLineBreak\n{}"
  end
  self.ellipsis = "\\markdownRendererEllipsis{}"
  function self.thematic_break()
    if not self.is_writing then return "" end
    return "\\markdownRendererThematicBreak{}"
  end
  self.escaped_uri_chars = {
    ["{"] = "\\markdownRendererLeftBrace{}",
    ["}"] = "\\markdownRendererRightBrace{}",
    ["\\"] = "\\markdownRendererBackslash{}",
  }
  self.escaped_minimal_strings = {
    ["^^"] = "\\markdownRendererCircumflex\\markdownRendererCircumflex ",
    ["☒"] = "\\markdownRendererTickedBox{}",
    ["⌛"] = "\\markdownRendererHalfTickedBox{}",
    ["☐"] = "\\markdownRendererUntickedBox{}",
    [entities.hex_entity('FFFD')] = "\\markdownRendererReplacementCharacter{}",
  }
  self.escaped_strings = util.table_copy(self.escaped_minimal_strings)
  self.escaped_strings[entities.hex_entity('00A0')] = self.nbsp
  self.escaped_chars = {
    ["{"] = "\\markdownRendererLeftBrace{}",
    ["}"] = "\\markdownRendererRightBrace{}",
    ["%"] = "\\markdownRendererPercentSign{}",
    ["\\"] = "\\markdownRendererBackslash{}",
    ["#"] = "\\markdownRendererHash{}",
    ["$"] = "\\markdownRendererDollarSign{}",
    ["&"] = "\\markdownRendererAmpersand{}",
    ["_"] = "\\markdownRendererUnderscore{}",
    ["^"] = "\\markdownRendererCircumflex{}",
    ["~"] = "\\markdownRendererTilde{}",
    ["|"] = "\\markdownRendererPipe{}",
    [entities.hex_entity('0000')] = "\\markdownRendererReplacementCharacter{}",
  }
  local function create_escaper(char_escapes, string_escapes)
    local escape = util.escaper(char_escapes, string_escapes)
    return function(s)
      if self.flatten_inlines then return s end
      return escape(s)
    end
  end
  local escape_typographic_text = create_escaper(
    self.escaped_chars, self.escaped_strings)
  local escape_programmatic_text = create_escaper(
    self.escaped_uri_chars, self.escaped_minimal_strings)
  local escape_minimal = create_escaper(
    {}, self.escaped_minimal_strings)
  self.escape = escape_typographic_text
  self.math = escape_minimal
  if options.hybrid then
    self.identifier = escape_minimal
    self.string = escape_minimal
    self.uri = escape_minimal
    self.infostring = escape_minimal
  else
    self.identifier = escape_programmatic_text
    self.string = escape_typographic_text
    self.uri = escape_programmatic_text
    self.infostring = escape_programmatic_text
  end
  function self.code(s, attributes)
    if self.flatten_inlines then return s end
    local buf = {}
    if attributes ~= nil then
      table.insert(buf,
                   "\\markdownRendererCodeSpanAttributeContextBegin\n")
      table.insert(buf, self.attributes(attributes))
    end
    table.insert(buf,
                 {"\\markdownRendererCodeSpan{", self.escape(s), "}"})
    if attributes ~= nil then
      table.insert(buf,
                   "\\markdownRendererCodeSpanAttributeContextEnd{}")
    end
    return buf
  end
  function self.link(lab, src, tit, attributes)
    if self.flatten_inlines then return lab end
    local buf = {}
    if attributes ~= nil then
      table.insert(buf,
                   "\\markdownRendererLinkAttributeContextBegin\n")
      table.insert(buf, self.attributes(attributes))
    end
    table.insert(buf, {"\\markdownRendererLink{",lab,"}",
                       "{",self.escape(src),"}",
                       "{",self.uri(src),"}",
                       "{",self.string(tit or ""),"}"})
    if attributes ~= nil then
      table.insert(buf,
                   "\\markdownRendererLinkAttributeContextEnd{}")
    end
    return buf
  end
  function self.image(lab, src, tit, attributes)
    if self.flatten_inlines then return lab end
    local buf = {}
    if attributes ~= nil then
      table.insert(buf,
                   "\\markdownRendererImageAttributeContextBegin\n")
      table.insert(buf, self.attributes(attributes))
    end
    table.insert(buf, {"\\markdownRendererImage{",lab,"}",
                       "{",self.string(src),"}",
                       "{",self.uri(src),"}",
                       "{",self.string(tit or ""),"}"})
    if attributes ~= nil then
      table.insert(buf,
                   "\\markdownRendererImageAttributeContextEnd{}")
    end
    return buf
  end
  function self.bulletlist(items,tight)
    if not self.is_writing then return "" end
    local buffer = {}
    for _,item in ipairs(items) do
      if item ~= "" then
        buffer[#buffer + 1] = self.bulletitem(item)
      end
    end
    local contents = util.intersperse(buffer,"\n")
    if tight and options.tightLists then
      return {"\\markdownRendererUlBeginTight\n",contents,
        "\n\\markdownRendererUlEndTight "}
    else
      return {"\\markdownRendererUlBegin\n",contents,
        "\n\\markdownRendererUlEnd "}
    end
  end
  function self.bulletitem(s)
    return {"\\markdownRendererUlItem ",s,
            "\\markdownRendererUlItemEnd "}
  end
  function self.orderedlist(items,tight,startnum)
    if not self.is_writing then return "" end
    local buffer = {}
    local num = startnum
    for _,item in ipairs(items) do
      if item ~= "" then
        buffer[#buffer + 1] = self.ordereditem(item,num)
      end
      if num ~= nil and item ~= "" then
        num = num + 1
      end
    end
    local contents = util.intersperse(buffer,"\n")
    if tight and options.tightLists then
      return {"\\markdownRendererOlBeginTight\n",contents,
              "\n\\markdownRendererOlEndTight "}
    else
      return {"\\markdownRendererOlBegin\n",contents,
              "\n\\markdownRendererOlEnd "}
    end
  end
  function self.ordereditem(s,num)
    if num ~= nil then
      return {"\\markdownRendererOlItemWithNumber{",num,"}",s,
              "\\markdownRendererOlItemEnd "}
    else
      return {"\\markdownRendererOlItem ",s,
              "\\markdownRendererOlItemEnd "}
    end
  end
  function self.inline_html_comment(contents)
    if self.flatten_inlines then return contents end
    return {"\\markdownRendererInlineHtmlComment{",contents,"}"}
  end
  function self.inline_html_tag(contents)
    if self.flatten_inlines then return contents end
    return {"\\markdownRendererInlineHtmlTag{",self.string(contents),"}"}
  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")
    return {"\\markdownRendererInputBlockHtmlElement{",name,"}"}
  end
  function self.emphasis(s)
    if self.flatten_inlines then return s end
    return {"\\markdownRendererEmphasis{",s,"}"}
  end
  function self.tickbox(f)
    if f == 1.0 then
      return "☒ "
    elseif f == 0.0 then
      return "☐ "
    else
      return "⌛ "
    end
  end
  function self.strong(s)
    if self.flatten_inlines then return s end
    return {"\\markdownRendererStrongEmphasis{",s,"}"}
  end
  function self.blockquote(s)
    if not self.is_writing then return "" end
    return {"\\markdownRendererBlockQuoteBegin\n",s,
      "\\markdownRendererBlockQuoteEnd "}
  end
  function self.verbatim(s)
    if not self.is_writing then return "" end
    s = s:gsub("\n$", "")
    local name = util.cache_verbatim(options.cacheDir, s)
    return {"\\markdownRendererInputVerbatim{",name,"}"}
  end
  function self.document(d)
    local buf = {"\\markdownRendererDocumentBegin\n", d}

    -- pop all attributes
    table.insert(buf, self.pop_attributes())

    table.insert(buf, "\\markdownRendererDocumentEnd")

    return buf
  end
  local seen_identifiers = {}
  local key_value_regex = "([^= ]+)%s*=%s*(.*)"
  local function normalize_attributes(attributes, auto_identifiers)
    -- normalize attributes
    local normalized_attributes = {}
    local has_explicit_identifiers = false
    local key, value
    for _, attribute in ipairs(attributes or {}) do
      if attribute:sub(1, 1) == "#" then
        table.insert(normalized_attributes, attribute)
        has_explicit_identifiers = true
        seen_identifiers[attribute:sub(2)] = true
      elseif attribute:sub(1, 1) == "." then
        table.insert(normalized_attributes, attribute)
      else
        key, value = attribute:match(key_value_regex)
        if key:lower() == "id" then
          table.insert(normalized_attributes, "#" .. value)
        elseif key:lower() == "class" then
          local classes = {}
          for class in value:gmatch("%S+") do
            table.insert(classes, class)
          end
          table.sort(classes)
          for _, class in ipairs(classes) do
            table.insert(normalized_attributes, "." .. class)
          end
        else
          table.insert(normalized_attributes, attribute)
        end
      end
    end

    -- if no explicit identifiers exist, add auto identifiers
    if not has_explicit_identifiers and auto_identifiers ~= nil then
      local seen_auto_identifiers = {}
      for _, auto_identifier in ipairs(auto_identifiers) do
        if seen_auto_identifiers[auto_identifier] == nil then
          seen_auto_identifiers[auto_identifier] = true
          if seen_identifiers[auto_identifier] == nil then
            seen_identifiers[auto_identifier] = true
            table.insert(normalized_attributes,
                         "#" .. auto_identifier)
          else
            local auto_identifier_number = 1
            while true do
              local numbered_auto_identifier = auto_identifier .. "-"
                                             .. auto_identifier_number
              if seen_identifiers[numbered_auto_identifier] == nil then
                seen_identifiers[numbered_auto_identifier] = true
                table.insert(normalized_attributes,
                             "#" .. numbered_auto_identifier)
                break
              end
              auto_identifier_number = auto_identifier_number + 1
            end
          end
        end
      end
    end

    -- sort and deduplicate normalized attributes
    table.sort(normalized_attributes)
    local seen_normalized_attributes = {}
    local deduplicated_normalized_attributes = {}
    for _, attribute in ipairs(normalized_attributes) do
      if seen_normalized_attributes[attribute] == nil then
        seen_normalized_attributes[attribute] = true
        table.insert(deduplicated_normalized_attributes, attribute)
      end
    end

    return deduplicated_normalized_attributes
  end

  function self.attributes(attributes, should_normalize_attributes)
    local normalized_attributes
    if should_normalize_attributes == false then
      normalized_attributes = attributes
    else
      normalized_attributes = normalize_attributes(attributes)
    end

    local buf = {}
    local key, value
    for _, attribute in ipairs(normalized_attributes) do
      if attribute:sub(1, 1) == "#" then
        table.insert(buf, {"\\markdownRendererAttributeIdentifier{",
                           attribute:sub(2), "}"})
      elseif attribute:sub(1, 1) == "." then
        table.insert(buf, {"\\markdownRendererAttributeClassName{",
                           attribute:sub(2), "}"})
      else
        key, value = attribute:match(key_value_regex)
        table.insert(buf, {"\\markdownRendererAttributeKeyValue{",
                           key, "}{", value, "}"})
      end
    end

    return buf
  end
  self.active_attributes = {}
  self.attribute_type_levels = {}
  setmetatable(self.attribute_type_levels,
               { __index = function() return 0 end })
  local function apply_attributes()
    local buf = {}
    for i = 1, #self.active_attributes do
      local start_output = self.active_attributes[i][3]
      if start_output ~= nil then
        table.insert(buf, start_output)
      end
    end
    return buf
  end

  local function tear_down_attributes()
    local buf = {}
    for i = #self.active_attributes, 1, -1 do
      local end_output = self.active_attributes[i][4]
      if end_output ~= nil then
        table.insert(buf, end_output)
      end
    end
    return buf
  end
  function self.push_attributes(attribute_type, attributes,
                                start_output, end_output)
    local attribute_type_level = self.attribute_type_levels[attribute_type]
    self.attribute_type_levels[attribute_type] = attribute_type_level + 1

    -- index attributes in a hash table for easy lookup
    attributes = attributes or {}
    for i = 1, #attributes do
      attributes[attributes[i]] = true
    end

    local buf = {}
    -- handle slicing
    if attributes["#" .. self.slice_end_identifier] ~= nil and
       self.slice_end_type == "^" then
      if self.is_writing then
        table.insert(buf, self.undosep())
        table.insert(buf, tear_down_attributes())
      end
      self.is_writing = false
    end
    if attributes["#" .. self.slice_begin_identifier] ~= nil and
       self.slice_begin_type == "^" then
      table.insert(buf, apply_attributes())
      self.is_writing = true
    end
    if self.is_writing and start_output ~= nil then
      table.insert(buf, start_output)
    end
    table.insert(self.active_attributes,
                 {attribute_type, attributes,
                  start_output, end_output})
    return buf
  end

  function self.pop_attributes(attribute_type)
    local buf = {}
    -- pop attributes until we find attributes of correct type
    -- or until no attributes remain
    local current_attribute_type = false
    while current_attribute_type ~= attribute_type and
          #self.active_attributes > 0 do
      local attributes, _, end_output
      current_attribute_type, attributes, _, end_output = table.unpack(
        self.active_attributes[#self.active_attributes])
      local attribute_type_level = self.attribute_type_levels[current_attribute_type]
      self.attribute_type_levels[current_attribute_type] = attribute_type_level - 1
      if self.is_writing and end_output ~= nil then
        table.insert(buf, end_output)
      end
      table.remove(self.active_attributes, #self.active_attributes)
      -- handle slicing
      if attributes["#" .. self.slice_end_identifier] ~= nil
         and self.slice_end_type == "$" then
        if self.is_writing then
          table.insert(buf, self.undosep())
          table.insert(buf, tear_down_attributes())
        end
        self.is_writing = false
      end
      if attributes["#" .. self.slice_begin_identifier] ~= nil and
         self.slice_begin_type == "$" then
        self.is_writing = true
        table.insert(buf, apply_attributes())
      end
    end
    return buf
  end
  local function create_auto_identifier(s)
    local buffer = {}
    local prev_space = false
    local letter_found = false
    local normalized_s = s
    if not options.unicodeNormalization or options.unicodeNormalizationForm ~= "nfc" then
      normalized_s = uni_algos.normalize.NFC(normalized_s)
    end

    for _, code in utf8.codes(normalized_s) do
      local char = utf8.char(code)

      -- Remove everything up to the first letter.
      if not letter_found then
        local is_letter = unicode.utf8.match(char, "%a")
        if is_letter then
          letter_found = true
        else
          goto continue
        end
      end

      -- Remove all non-alphanumeric characters, except underscores, hyphens, and periods.
      if not unicode.utf8.match(char, "[%w_%-%.%s]") then
        goto continue
      end

      -- Replace all spaces and newlines with hyphens.
      if unicode.utf8.match(char, "[%s\n]") then
        char = "-"
        if prev_space then
          goto continue
        else
          prev_space = true
        end
      else
        -- Convert all alphabetic characters to lowercase.
        char = unicode.utf8.lower(char)
        prev_space = false
      end

      table.insert(buffer, char)

      ::continue::
    end

    if prev_space then
      table.remove(buffer)
    end

    local identifier = #buffer == 0 and "section" or table.concat(buffer, "")
    return identifier
  end
  local function create_gfm_auto_identifier(s)
    local buffer = {}
    local prev_space = false
    local letter_found = false
    local normalized_s = s
    if not options.unicodeNormalization or options.unicodeNormalizationForm ~= "nfc" then
      normalized_s = uni_algos.normalize.NFC(normalized_s)
    end

    for _, code in utf8.codes(normalized_s) do
      local char = utf8.char(code)

      -- Remove everything up to the first non-space.
      if not letter_found then
        local is_letter = unicode.utf8.match(char, "%S")
        if is_letter then
          letter_found = true
        else
          goto continue
        end
      end

      -- Remove all non-alphanumeric characters, except underscores and hyphens.
      if not unicode.utf8.match(char, "[%w_%-%s]") then
        prev_space = false
        goto continue
      end

      -- Replace all spaces and newlines with hyphens.
      if unicode.utf8.match(char, "[%s\n]") then
        char = "-"
        if prev_space then
          goto continue
        else
          prev_space = true
        end
      else
        -- Convert all alphabetic characters to lowercase.
        char = unicode.utf8.lower(char)
        prev_space = false
      end

      table.insert(buffer, char)

      ::continue::
    end

    if prev_space then
      table.remove(buffer)
    end

    local identifier = #buffer == 0 and "section" or table.concat(buffer, "")
    return identifier
  end
  self.secbegin_text = "\\markdownRendererSectionBegin\n"
  self.secend_text = "\n\\markdownRendererSectionEnd "
  function self.heading(s, level, attributes)
    local buf = {}
    local flat_text, inlines = table.unpack(s)

    -- push empty attributes for implied sections
    while self.attribute_type_levels["heading"] < level - 1 do
      table.insert(buf,
                   self.push_attributes("heading",
                                        nil,
                                        self.secbegin_text,
                                        self.secend_text))
    end

    -- pop attributes for sections that have ended
    while self.attribute_type_levels["heading"] >= level do
      table.insert(buf, self.pop_attributes("heading"))
    end

    -- construct attributes for the new section
    local auto_identifiers = {}
    if self.options.autoIdentifiers then
      table.insert(auto_identifiers, create_auto_identifier(flat_text))
    end
    if self.options.gfmAutoIdentifiers then
      table.insert(auto_identifiers, create_gfm_auto_identifier(flat_text))
    end
    local normalized_attributes = normalize_attributes(attributes, auto_identifiers)

    -- push attributes for the new section
    local start_output = {}
    local end_output = {}
    table.insert(start_output, self.secbegin_text)
    table.insert(end_output, self.secend_text)

    table.insert(buf, self.push_attributes("heading",
                                           normalized_attributes,
                                           start_output,
                                           end_output))
    assert(self.attribute_type_levels["heading"] == level)

    -- render the heading and its attributes
    if self.is_writing and #normalized_attributes > 0 then
      table.insert(buf, "\\markdownRendererHeaderAttributeContextBegin\n")
      table.insert(buf, self.attributes(normalized_attributes, false))
    end

    local cmd
    level = level + options.shiftHeadings
    if level <= 1 then
      cmd = "\\markdownRendererHeadingOne"
    elseif level == 2 then
      cmd = "\\markdownRendererHeadingTwo"
    elseif level == 3 then
      cmd = "\\markdownRendererHeadingThree"
    elseif level == 4 then
      cmd = "\\markdownRendererHeadingFour"
    elseif level == 5 then
      cmd = "\\markdownRendererHeadingFive"
    elseif level >= 6 then
      cmd = "\\markdownRendererHeadingSix"
    else
      cmd = ""
    end
    if self.is_writing then
      table.insert(buf, {cmd, "{", inlines, "}"})
    end

    if self.is_writing and #normalized_attributes > 0 then
      table.insert(buf, "\\markdownRendererHeaderAttributeContextEnd{}")
    end

    return buf
  end
  function self.get_state()
    return {
      is_writing=self.is_writing,
      flatten_inlines=self.flatten_inlines,
      active_attributes={table.unpack(self.active_attributes)},
    }
  end
  function self.set_state(s)
    local previous_state = self.get_state()
    for key, value in pairs(s) do
      self[key] = value
    end
    return previous_state
  end
  function self.defer_call(f)
    local previous_state = self.get_state()
    return function(...)
      local state = self.set_state(previous_state)
      local return_value = f(...)
      self.set_state(state)
      return return_value
    end
  end

  return self
end
local parsers                  = {}
parsers.percent                = P("%")
parsers.at                     = P("@")
parsers.comma                  = P(",")
parsers.asterisk               = P("*")
parsers.dash                   = P("-")
parsers.plus                   = P("+")
parsers.underscore             = P("_")
parsers.period                 = P(".")
parsers.hash                   = P("#")
parsers.dollar                 = P("$")
parsers.ampersand              = P("&")
parsers.backtick               = P("`")
parsers.less                   = P("<")
parsers.more                   = P(">")
parsers.space                  = P(" ")
parsers.squote                 = P("'")
parsers.dquote                 = P('"')
parsers.lparent                = P("(")
parsers.rparent                = P(")")
parsers.lbracket               = P("[")
parsers.rbracket               = P("]")
parsers.lbrace                 = P("{")
parsers.rbrace                 = P("}")
parsers.circumflex             = P("^")
parsers.slash                  = P("/")
parsers.equal                  = P("=")
parsers.colon                  = P(":")
parsers.semicolon              = P(";")
parsers.exclamation            = P("!")
parsers.pipe                   = P("|")
parsers.tilde                  = P("~")
parsers.backslash              = P("\\")
parsers.tab                    = P("\t")
parsers.newline                = P("\n")

parsers.digit                  = R("09")
parsers.hexdigit               = R("09","af","AF")
parsers.letter                 = R("AZ","az")
parsers.alphanumeric           = R("AZ","az","09")
parsers.keyword                = parsers.letter
                               * (parsers.alphanumeric + parsers.dash)^0

parsers.doubleasterisks        = P("**")
parsers.doubleunderscores      = P("__")
parsers.doubletildes           = P("~~")
parsers.fourspaces             = P("    ")

parsers.any                    = P(1)
parsers.succeed                = P(true)
parsers.fail                   = P(false)

parsers.internal_punctuation   = S(":;,.?")
parsers.ascii_punctuation      = S("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
parsers.punctuation            = {}
(function()
  local pathname = kpse.lookup("UnicodeData.txt")
  local file = assert(io.open(pathname, "r"),
    [[Could not open file "UnicodeData.txt"]])
  for line in file:lines() do
    local codepoint, major_category = line:match("^(%x+);[^;]*;(%a)")
    if major_category == "P" or major_category == "S" then
      local code = unicode.utf8.char(tonumber(codepoint, 16))
      if parsers.punctuation[#code] == nil then
        parsers.punctuation[#code] = parsers.fail
      end
      local code_parser = parsers.succeed
      for i = 1, #code do
        local byte = code:sub(i, i)
        local byte_parser = S(byte)
        code_parser = code_parser
                    * byte_parser
      end
      parsers.punctuation[#code] = parsers.punctuation[#code]
                                 + code_parser
    end
  end
  assert(file:close())
end)()

parsers.escapable              = parsers.ascii_punctuation
parsers.anyescaped             = parsers.backslash / "" * parsers.escapable
                               + parsers.any

parsers.spacechar              = S("\t ")
parsers.spacing                = S(" \n\r\t")
parsers.nonspacechar           = parsers.any - parsers.spacing
parsers.optionalspace          = parsers.spacechar^0

parsers.normalchar             = parsers.any - (V("SpecialChar")
                                                + parsers.spacing)
parsers.eof                    = -parsers.any
parsers.nonindentspace         = parsers.space^-3 * - parsers.spacechar
parsers.indent                 = parsers.space^-3 * parsers.tab
                               + parsers.fourspaces / ""
parsers.linechar               = P(1 - parsers.newline)

parsers.blankline              = parsers.optionalspace
                               * parsers.newline / "\n"
parsers.blanklines             = parsers.blankline^0
parsers.skipblanklines         = (parsers.optionalspace * parsers.newline)^0
parsers.indentedline           = parsers.indent    /""
                               * C(parsers.linechar^1 * parsers.newline^-1)
parsers.optionallyindentedline = parsers.indent^-1 /""
                               * C(parsers.linechar^1 * parsers.newline^-1)
parsers.sp                     = parsers.spacing^0
parsers.spnl                   = parsers.optionalspace
                               * (parsers.newline * parsers.optionalspace)^-1
parsers.line                   = parsers.linechar^0 * parsers.newline
parsers.nonemptyline           = parsers.line - parsers.blankline

parsers.leader      = parsers.space^-3

local function has_trail(indent_table)
  return indent_table ~= nil and
    indent_table.trail ~= nil and
    next(indent_table.trail) ~= nil
end

local function has_indents(indent_table)
  return indent_table ~= nil and
    indent_table.indents ~= nil and
    next(indent_table.indents) ~= nil
end

local function add_trail(indent_table, trail_info)
  indent_table.trail = trail_info
  return indent_table
end

local function remove_trail(indent_table)
  indent_table.trail = nil
  return indent_table
end

local function update_indent_table(indent_table, new_indent, add)
  indent_table = remove_trail(indent_table)

  if not has_indents(indent_table) then
    indent_table.indents = {}
  end

  if add then
    indent_table.indents[#indent_table.indents + 1] = new_indent
  else
    if indent_table.indents[#indent_table.indents].name == new_indent.name then
      indent_table.indents[#indent_table.indents] = nil
    end
  end

  return indent_table
end

local function remove_indent(name)
  local function remove_indent_level(s, i, indent_table) -- luacheck: ignore s i
    indent_table = update_indent_table(indent_table, {name=name}, false)
    return true, indent_table
  end

  return Cg(Cmt(Cb("indent_info"), remove_indent_level), "indent_info")
end

local function process_starter_spacing(indent, spacing, minimum, left_strip_length)
  left_strip_length = left_strip_length or 0

  local count = 0
  local tab_value = 4 - (indent) % 4

  local code_started, minimum_found = false, false
  local code_start, minimum_remainder = "", ""

  local left_total_stripped = 0
  local full_remainder = ""

  if spacing ~= nil then
    for i = 1, #spacing do
      local character = spacing:sub(i, i)

      if character == "\t" then
        count = count + tab_value
        tab_value = 4
      elseif character == " " then
        count = count + 1
        tab_value = 4 - (1 - tab_value) % 4
      end

      if (left_strip_length ~= 0) then
        local possible_to_strip = math.min(count, left_strip_length)
        count = count - possible_to_strip
        left_strip_length = left_strip_length - possible_to_strip
        left_total_stripped = left_total_stripped + possible_to_strip
      else
        full_remainder =  full_remainder .. character
      end

      if (minimum_found) then
        minimum_remainder = minimum_remainder .. character
      elseif (count >= minimum) then
        minimum_found = true
        minimum_remainder = minimum_remainder .. string.rep(" ", count - minimum)
      end

      if (code_started) then
        code_start = code_start .. character
      elseif (count >= minimum + 4) then
        code_started = true
        code_start = code_start .. string.rep(" ", count - (minimum + 4))
      end
    end
  end

  local remainder
  if (code_started) then
    remainder = code_start
  else
    remainder = string.rep(" ", count - minimum)
  end

  local is_minimum = count >= minimum
  return {
    is_code = code_started,
    remainder = remainder,
    left_total_stripped = left_total_stripped,
    is_minimum = is_minimum,
    minimum_remainder = minimum_remainder,
    total_length = count,
    full_remainder = full_remainder
  }
end

local function count_indent_tab_level(indent_table)
  local count = 0
  if not has_indents(indent_table) then
    return count
  end

  for i=1, #indent_table.indents do
    count = count + indent_table.indents[i].length
  end
  return count
end

local function total_delimiter_length(delimiter)
  local count = 0
  if type(delimiter) == "string" then return #delimiter end
  for _, value in pairs(delimiter) do
    count = count + total_delimiter_length(value)
  end
  return count
end

local function process_starter_indent(_, _, indent_table, starter, is_blank, indent_type, breakable)
  local last_trail = starter[1]
  local delimiter = starter[2]
  local raw_new_trail = starter[3]

  if indent_type == "bq" and not breakable then
    indent_table.ignore_blockquote_blank = true
  end

  if has_trail(indent_table) then
    local trail = indent_table.trail
    if trail.is_code then
      return false
    end
    last_trail = trail.remainder
  else
    local sp = process_starter_spacing(0, last_trail, 0, 0)

    if sp.is_code then
      return false
    end
    last_trail = sp.remainder
  end

  local preceding_indentation = count_indent_tab_level(indent_table) % 4
  local last_trail_length = #last_trail
  local delimiter_length = total_delimiter_length(delimiter)

  local total_indent_level = preceding_indentation + last_trail_length + delimiter_length

  local sp = {}
  if not is_blank then
    sp = process_starter_spacing(total_indent_level, raw_new_trail, 0, 1)
  end

  local del_trail_length = sp.left_total_stripped
  if is_blank then
    del_trail_length = 1
  elseif not sp.is_code then
    del_trail_length = del_trail_length + #sp.remainder
  end

  local indent_length = last_trail_length + delimiter_length + del_trail_length
  local new_indent_info = {name=indent_type, length=indent_length}

  indent_table = update_indent_table(indent_table, new_indent_info, true)
  indent_table = add_trail(indent_table, {is_code=sp.is_code, remainder=sp.remainder, total_length=sp.total_length,
                                          full_remainder=sp.full_remainder})

  return true, indent_table
end

local function decode_pattern(name)
  local delimeter = parsers.succeed
  if name == "bq" then
    delimeter = parsers.more
  end

  return C(parsers.optionalspace) * C(delimeter) * C(parsers.optionalspace) * Cp()
end

local function left_blank_starter(indent_table)
  local blank_starter_index

  if not has_indents(indent_table) then
    return
  end

  for i = #indent_table.indents,1,-1 do
    local value = indent_table.indents[i]
    if value.name == "li" then
      blank_starter_index = i
    else
      break
    end
  end

  return blank_starter_index
end

local function traverse_indent(s, i, indent_table, is_optional, is_blank, current_line_indents)
  local new_index = i

  local preceding_indentation = 0
  local current_trail = {}

  local blank_starter = left_blank_starter(indent_table)

  if current_line_indents == nil then
    current_line_indents = {}
  end

  for index = 1,#indent_table.indents do
    local value = indent_table.indents[index]
    local pattern = decode_pattern(value.name)

    -- match decoded pattern
    local new_indent_info = lpeg.match(Ct(pattern), s, new_index)
    if new_indent_info == nil then
      local blankline_end = lpeg.match(Ct(parsers.blankline * Cg(Cp(), "pos")), s, new_index)
      if is_optional or not indent_table.ignore_blockquote_blank or not blankline_end then
        return is_optional, new_index, current_trail, current_line_indents
      end

      return traverse_indent(s, tonumber(blankline_end.pos), indent_table, is_optional, is_blank, current_line_indents)
    end

    local raw_last_trail = new_indent_info[1]
    local delimiter = new_indent_info[2]
    local raw_new_trail = new_indent_info[3]
    local next_index = new_indent_info[4]

    local space_only = delimiter == ""

    -- check previous trail
    if not space_only and next(current_trail) == nil then
      local sp = process_starter_spacing(0, raw_last_trail, 0, 0)
      current_trail = {is_code=sp.is_code, remainder=sp.remainder, total_length=sp.total_length,
                       full_remainder=sp.full_remainder}
    end

    if next(current_trail) ~= nil then
      if not space_only and current_trail.is_code then
        return is_optional, new_index, current_trail, current_line_indents
      end
      if current_trail.internal_remainder ~= nil then
        raw_last_trail = current_trail.internal_remainder
      end
    end

    local raw_last_trail_length = 0
    local delimiter_length = 0

    if not space_only then
      delimiter_length = #delimiter
      raw_last_trail_length = #raw_last_trail
    end

    local total_indent_level = preceding_indentation + raw_last_trail_length + delimiter_length

    local spacing_to_process
    local minimum = 0
    local left_strip_length = 0

    if not space_only then
      spacing_to_process = raw_new_trail
      left_strip_length = 1
    else
      spacing_to_process = raw_last_trail
      minimum = value.length
    end

    local sp = process_starter_spacing(total_indent_level, spacing_to_process, minimum, left_strip_length)

    if space_only and not sp.is_minimum then
      return is_optional or (is_blank and blank_starter <= index), new_index, current_trail, current_line_indents
    end

    local indent_length = raw_last_trail_length + delimiter_length + sp.left_total_stripped

    -- update info for the next pattern
    if not space_only then
      preceding_indentation = preceding_indentation + indent_length
    else
      preceding_indentation = preceding_indentation + value.length
    end

    current_trail = {is_code=sp.is_code, remainder=sp.remainder, internal_remainder=sp.minimum_remainder,
                     total_length=sp.total_length, full_remainder=sp.full_remainder}

    current_line_indents[#current_line_indents + 1] = new_indent_info
    new_index = next_index
  end

  return true, new_index, current_trail, current_line_indents
end

local function check_trail(expect_code, is_code)
  return (expect_code and is_code) or (not expect_code and not is_code)
end

local function check_trail_joined(s, i, indent_table, spacing, expect_code, omit_remainder) -- luacheck: ignore s i
  local is_code
  local remainder

  if has_trail(indent_table) then
    local trail = indent_table.trail
    is_code = trail.is_code
    if is_code then
      remainder = trail.remainder
    else
      remainder = trail.full_remainder
    end
  else
    local sp = process_starter_spacing(0, spacing, 0, 0)
    is_code = sp.is_code
    if is_code then
      remainder = sp.remainder
    else
      remainder = sp.full_remainder
    end
  end

  local result = check_trail(expect_code, is_code)
  if omit_remainder then
    return result
  end
  return result, remainder
end

local function check_trail_length(s, i, indent_table, spacing, min, max) -- luacheck: ignore s i
  local trail

  if has_trail(indent_table) then
    trail = indent_table.trail
  else
    trail = process_starter_spacing(0, spacing, 0, 0)
  end

  local total_length = trail.total_length
  if total_length == nil then
    return false
  end

  return min <= total_length and total_length <= max
end

local function check_continuation_indentation(s, i, indent_table, is_optional, is_blank)
  if not has_indents(indent_table) then
    return true
  end

  local passes, new_index, current_trail, current_line_indents =
    traverse_indent(s, i, indent_table, is_optional, is_blank)

  if passes then
    indent_table.current_line_indents = current_line_indents
    indent_table = add_trail(indent_table, current_trail)
    return new_index, indent_table
  end
  return false
end

local function get_last_indent_name(indent_table)
  if has_indents(indent_table) then
    return indent_table.indents[#indent_table.indents].name
  end
end

local function remove_remainder_if_blank(indent_table, remainder)
  if get_last_indent_name(indent_table) == "li" then
    return ""
  end
  return remainder
end

local function check_trail_type(s, i, trail, spacing, trail_type) -- luacheck: ignore s i
  if trail == nil then
    trail = process_starter_spacing(0, spacing, 0, 0)
  end

  if trail_type == "non-code" then
    return check_trail(false, trail.is_code)
  end
  if trail_type == "code" then
    return check_trail(true, trail.is_code)
  end
  if trail_type == "full-code" then
    if (trail.is_code) then
      return i, trail.remainder
    end
    return i, ""
  end
  if trail_type == "full-any" then
    return i, trail.internal_remainder
  end
end

local function trail_freezing(s, i, indent_table, is_freezing) -- luacheck: ignore s i
  if is_freezing then
    if indent_table.is_trail_frozen then
      indent_table.trail = indent_table.frozen_trail
    else
      indent_table.frozen_trail = indent_table.trail
      indent_table.is_trail_frozen = true
    end
  else
    indent_table.frozen_trail = nil
    indent_table.is_trail_frozen = false
  end
  return true, indent_table
end

local function check_continuation_indentation_and_trail(s, i, indent_table, is_optional, is_blank, trail_type,
                                                        reset_rem, omit_remainder)
  if not has_indents(indent_table) then
    local spacing, new_index = lpeg.match(C(parsers.spacechar^0) * Cp(), s, i)
    local result, remainder = check_trail_type(s, i, indent_table.trail, spacing, trail_type)
    if remainder == nil then
      if result then
        return new_index
      end
      return false
    end
    if result then
      return new_index, remainder
    end
    return false
  end

  local passes, new_index, current_trail = traverse_indent(s, i, indent_table, is_optional, is_blank)

  if passes then
    local spacing
    if current_trail == nil then
      local newer_spacing, newer_index = lpeg.match(C(parsers.spacechar^0) * Cp(), s, i)
      current_trail = process_starter_spacing(0, newer_spacing, 0, 0)
      new_index = newer_index
      spacing = newer_spacing
    else
      spacing = current_trail.remainder
    end
    local result, remainder = check_trail_type(s, new_index, current_trail, spacing, trail_type)
    if remainder == nil or omit_remainder then
      if result then
        return new_index
      end
      return false
    end

    if is_blank and reset_rem then
      remainder = remove_remainder_if_blank(indent_table, remainder)
    end
    if result then
      return new_index, remainder
    end
    return false
  end
  return false
end

parsers.check_trail = Cmt(Cb("indent_info") * C(parsers.spacechar^0) * Cc(false), check_trail_joined)

parsers.check_trail_no_rem = Cmt(Cb("indent_info") * C(parsers.spacechar^0) * Cc(false) * Cc(true), check_trail_joined)

parsers.check_code_trail  = Cmt(Cb("indent_info") * C(parsers.spacechar^0) * Cc(true), check_trail_joined)

parsers.check_trail_length_range  = function(min, max)
  return Cmt(Cb("indent_info") * C(parsers.spacechar^0) * Cc(min) * Cc(max), check_trail_length)
end

parsers.check_trail_length = function(n)
  return parsers.check_trail_length_range(n, n)
end

parsers.freeze_trail = Cg(Cmt(Cb("indent_info") * Cc(true), trail_freezing), "indent_info")

parsers.unfreeze_trail = Cg(Cmt(Cb("indent_info") * Cc(false), trail_freezing), "indent_info")

parsers.check_minimal_indent = Cmt(Cb("indent_info") * Cc(false), check_continuation_indentation)

parsers.check_optional_indent = Cmt(Cb("indent_info") * Cc(true), check_continuation_indentation)

parsers.check_minimal_blank_indent = Cmt(Cb("indent_info") * Cc(false) * Cc(true), check_continuation_indentation)


parsers.check_minimal_indent_and_trail = Cmt( Cb("indent_info")
                                            * Cc(false) * Cc(false) * Cc("non-code") * Cc(true),
                                            check_continuation_indentation_and_trail)

parsers.check_minimal_indent_and_code_trail = Cmt( Cb("indent_info")
                                                 * Cc(false) * Cc(false) * Cc("code") * Cc(false),
                                                 check_continuation_indentation_and_trail)

parsers.check_minimal_blank_indent_and_full_code_trail = Cmt( Cb("indent_info")
                                                            * Cc(false) * Cc(true) * Cc("full-code") * Cc(true),
                                                            check_continuation_indentation_and_trail)

parsers.check_minimal_indent_and_any_trail = Cmt( Cb("indent_info")
                                                * Cc(false) * Cc(false) * Cc("full-any") * Cc(true) * Cc(false),
                                                check_continuation_indentation_and_trail)

parsers.check_minimal_blank_indent_and_any_trail = Cmt( Cb("indent_info")
                                                      * Cc(false) * Cc(true) * Cc("full-any") * Cc(true) * Cc(false),
                                                       check_continuation_indentation_and_trail)

parsers.check_minimal_blank_indent_and_any_trail_no_rem = Cmt( Cb("indent_info")
                                                        * Cc(false) * Cc(true) * Cc("full-any") * Cc(true) * Cc(true),
                                                        check_continuation_indentation_and_trail)

parsers.check_optional_indent_and_any_trail = Cmt( Cb("indent_info")
                                                * Cc(true) * Cc(false) * Cc("full-any") * Cc(true) * Cc(false),
                                                check_continuation_indentation_and_trail)

parsers.check_optional_blank_indent_and_any_trail = Cmt( Cb("indent_info")
                                                      * Cc(true) * Cc(true) * Cc("full-any") * Cc(true) * Cc(false),
                                                       check_continuation_indentation_and_trail)


parsers.spnlc_noexc = parsers.optionalspace
                    * (parsers.newline * parsers.check_minimal_indent_and_any_trail)^-1

parsers.spnlc = parsers.optionalspace
              * (V("EndlineNoSub"))^-1

parsers.spnlc_sep  = parsers.optionalspace * V("EndlineNoSub")
                   + parsers.spacechar^1

parsers.only_blank = parsers.spacechar^0 * (parsers.newline + parsers.eof)

parsers.commented_line_letter  = parsers.linechar
                               + parsers.newline
                               - parsers.backslash
                               - parsers.percent
parsers.commented_line         = Cg(Cc(""), "backslashes")
                               * ((#(parsers.commented_line_letter
                                    - parsers.newline)
                                  * Cb("backslashes")
                                  * Cs(parsers.commented_line_letter
                                    - parsers.newline)^1  -- initial
                                  * Cg(Cc(""), "backslashes"))
                                 + #(parsers.backslash * parsers.backslash)
                                 * Cg((parsers.backslash  -- even backslash
                                      * parsers.backslash)^1, "backslashes")
                                 + (parsers.backslash
                                   * (#parsers.percent
                                     * Cb("backslashes")
                                     / function(backslashes)
                                       return string.rep("\\", #backslashes / 2)
                                     end
                                     * C(parsers.percent)
                                     + #parsers.commented_line_letter
                                     * Cb("backslashes")
                                     * Cc("\\")
                                     * C(parsers.commented_line_letter))
                                   * Cg(Cc(""), "backslashes")))^0
                               * (#parsers.percent
                                 * Cb("backslashes")
                                 / function(backslashes)
                                   return string.rep("\\", #backslashes / 2)
                                 end
                                 * ((parsers.percent  -- comment
                                    * parsers.line
                                    * #parsers.blankline) -- blank line
                                   / "\n"
                                   + parsers.percent  -- comment
                                   * parsers.line
                                   * parsers.optionalspace)  -- leading tabs and spaces
                                 + #(parsers.newline)
                                 * Cb("backslashes")
                                 * C(parsers.newline))

parsers.chunk                  = parsers.line * (parsers.optionallyindentedline
                                                - parsers.blankline)^0

parsers.attribute_key_char     = parsers.alphanumeric + S("-_:.")
parsers.attribute_raw_char     = parsers.alphanumeric + S("-_")
parsers.attribute_key          = (parsers.attribute_key_char
                                 - parsers.dash - parsers.digit)
                               * parsers.attribute_key_char^0
parsers.attribute_value        = ( (parsers.dquote / "")
                                 * (parsers.anyescaped - parsers.dquote)^0
                                 * (parsers.dquote / ""))
                               + ( (parsers.squote / "")
                                 * (parsers.anyescaped - parsers.squote)^0
                                 * (parsers.squote / ""))
                               + ( parsers.anyescaped - parsers.dquote - parsers.rbrace
                                 - parsers.space)^0
parsers.attribute_identifier   = parsers.attribute_key_char^1
parsers.attribute_classname    = parsers.letter
                               * parsers.attribute_key_char^0
parsers.attribute_raw          = parsers.attribute_raw_char^1

parsers.attribute = (parsers.dash * Cc(".unnumbered"))
                  + C( parsers.hash
                     * parsers.attribute_identifier)
                  + C( parsers.period
                     * parsers.attribute_classname)
                  + Cs( parsers.attribute_key
                      * parsers.optionalspace * parsers.equal * parsers.optionalspace
                      * parsers.attribute_value)
parsers.attributes = parsers.lbrace
                   * parsers.optionalspace
                   * parsers.attribute
                   * (parsers.spacechar^1
                     * parsers.attribute)^0
                   * parsers.optionalspace
                   * parsers.rbrace

parsers.raw_attribute = parsers.lbrace
                      * parsers.optionalspace
                      * parsers.equal
                      * C(parsers.attribute_raw)
                      * parsers.optionalspace
                      * parsers.rbrace

-- block followed by 0 or more optionally
-- indented blocks with first line indented.
parsers.indented_blocks = function(bl)
  return Cs( bl
         * (parsers.blankline^1 * parsers.indent * -parsers.blankline * bl)^0
         * (parsers.blankline^1 + parsers.eof) )
end
local function repeat_between(pattern, min, max)
  return -pattern^(max + 1) * pattern^min
end

parsers.hexentity = parsers.ampersand * parsers.hash * C(S("Xx"))
                  * C(repeat_between(parsers.hexdigit, 1, 6)) * parsers.semicolon
parsers.decentity = parsers.ampersand * parsers.hash
                  * C(repeat_between(parsers.digit, 1, 7)) * parsers.semicolon
parsers.tagentity = parsers.ampersand * C(parsers.alphanumeric^1)
                  * parsers.semicolon

parsers.html_entities = parsers.hexentity / entities.hex_entity_with_x_char
                      + parsers.decentity / entities.dec_entity
                      + parsers.tagentity / entities.char_entity
parsers.bullet = function(bullet_char, interrupting)
  local allowed_end
  if interrupting then
    allowed_end = C(parsers.spacechar^1) * #parsers.linechar
  else
    allowed_end = C(parsers.spacechar^1) + #(parsers.newline + parsers.eof)
  end
  return parsers.check_trail
        * Ct(C(bullet_char) * Cc(""))
        * allowed_end
end

local function tickbox(interior)
  return parsers.optionalspace * parsers.lbracket
       * interior * parsers.rbracket * parsers.spacechar^1
end

parsers.ticked_box = tickbox(S("xX")) * Cc(1.0)
parsers.halfticked_box = tickbox(S("./")) * Cc(0.5)
parsers.unticked_box = tickbox(parsers.spacechar^1) * Cc(0.0)

parsers.openticks   = Cg(parsers.backtick^1, "ticks")

local function captures_equal_length(_,i,a,b)
  return #a == #b and i
end

parsers.closeticks  = Cmt(C(parsers.backtick^1)
                          * Cb("ticks"), captures_equal_length)

parsers.intickschar = (parsers.any - S("\n\r`"))
                    + V("NoSoftLineBreakEndline")
                    + (parsers.backtick^1 - parsers.closeticks)

local function process_inticks(s)
  s = s:gsub("\n", " ")
  s = s:gsub("^ (.*) $", "%1")
  return s
end

parsers.inticks = parsers.openticks
                * C(parsers.space^0)
                * parsers.closeticks
                + parsers.openticks
                * Cs(Cs(parsers.intickschar^0) / process_inticks)
                * parsers.closeticks

-- case-insensitive match (we assume s is lowercase). must be single byte encoding
parsers.keyword_exact = function(s)
  local parser = P(0)
  for i=1,#s do
    local c = s:sub(i,i)
    local m = c .. upper(c)
    parser = parser * S(m)
  end
  return parser
end

parsers.special_block_keyword =
    parsers.keyword_exact("pre") +
    parsers.keyword_exact("script") +
    parsers.keyword_exact("style") +
    parsers.keyword_exact("textarea")

parsers.block_keyword =
    parsers.keyword_exact("address") +
    parsers.keyword_exact("article") +
    parsers.keyword_exact("aside") +
    parsers.keyword_exact("base") +
    parsers.keyword_exact("basefont") +
    parsers.keyword_exact("blockquote") +
    parsers.keyword_exact("body") +
    parsers.keyword_exact("caption") +
    parsers.keyword_exact("center") +
    parsers.keyword_exact("col") +
    parsers.keyword_exact("colgroup") +
    parsers.keyword_exact("dd") +
    parsers.keyword_exact("details") +
    parsers.keyword_exact("dialog") +
    parsers.keyword_exact("dir") +
    parsers.keyword_exact("div") +
    parsers.keyword_exact("dl") +
    parsers.keyword_exact("dt") +
    parsers.keyword_exact("fieldset") +
    parsers.keyword_exact("figcaption") +
    parsers.keyword_exact("figure") +
    parsers.keyword_exact("footer") +
    parsers.keyword_exact("form") +
    parsers.keyword_exact("frame") +
    parsers.keyword_exact("frameset") +
    parsers.keyword_exact("h1") +
    parsers.keyword_exact("h2") +
    parsers.keyword_exact("h3") +
    parsers.keyword_exact("h4") +
    parsers.keyword_exact("h5") +
    parsers.keyword_exact("h6") +
    parsers.keyword_exact("head") +
    parsers.keyword_exact("header") +
    parsers.keyword_exact("hr") +
    parsers.keyword_exact("html") +
    parsers.keyword_exact("iframe") +
    parsers.keyword_exact("legend") +
    parsers.keyword_exact("li") +
    parsers.keyword_exact("link") +
    parsers.keyword_exact("main") +
    parsers.keyword_exact("menu") +
    parsers.keyword_exact("menuitem") +
    parsers.keyword_exact("nav") +
    parsers.keyword_exact("noframes") +
    parsers.keyword_exact("ol") +
    parsers.keyword_exact("optgroup") +
    parsers.keyword_exact("option") +
    parsers.keyword_exact("p") +
    parsers.keyword_exact("param") +
    parsers.keyword_exact("section") +
    parsers.keyword_exact("source") +
    parsers.keyword_exact("summary") +
    parsers.keyword_exact("table") +
    parsers.keyword_exact("tbody") +
    parsers.keyword_exact("td") +
    parsers.keyword_exact("tfoot") +
    parsers.keyword_exact("th") +
    parsers.keyword_exact("thead") +
    parsers.keyword_exact("title") +
    parsers.keyword_exact("tr") +
    parsers.keyword_exact("track") +
    parsers.keyword_exact("ul")

-- end conditions
parsers.html_blankline_end_condition  = parsers.linechar^0
                                      * ( parsers.newline
                                        * (parsers.check_minimal_blank_indent_and_any_trail
                                          * #parsers.blankline
                                          + parsers.check_minimal_indent_and_any_trail)
                                        * parsers.linechar^1)^0
                                      * (parsers.newline^-1 / "")

local function remove_trailing_blank_lines(s)
  return s:gsub("[\n\r]+%s*$", "")
end

parsers.html_until_end = function(end_marker)
  return Cs(Cs((parsers.newline
          * (parsers.check_minimal_blank_indent_and_any_trail
            * #parsers.blankline
            + parsers.check_minimal_indent_and_any_trail)
          + parsers.linechar - end_marker)^0
          * parsers.linechar^0 * parsers.newline^-1)
         / remove_trailing_blank_lines)
end

-- attributes
parsers.html_attribute_spacing  = parsers.optionalspace
                                * V("NoSoftLineBreakEndline")
                                * parsers.optionalspace
                                + parsers.spacechar^1

parsers.html_attribute_name = (parsers.letter + parsers.colon + parsers.underscore)
                            * (parsers.alphanumeric + parsers.colon + parsers.underscore
                            + parsers.period + parsers.dash)^0

parsers.html_attribute_value  = parsers.squote
                              * (parsers.linechar - parsers.squote)^0
                              * parsers.squote
                              + parsers.dquote
                              * (parsers.linechar - parsers.dquote)^0
                              * parsers.dquote
                              + ( parsers.any - parsers.spacechar - parsers.newline
                                - parsers.dquote - parsers.squote - parsers.backtick
                                - parsers.equal - parsers.less - parsers.more)^1

parsers.html_inline_attribute_value = parsers.squote
                                    * (V("NoSoftLineBreakEndline")
                                      + parsers.any
                                      - parsers.blankline^2
                                      - parsers.squote)^0
                                    * parsers.squote
                                    + parsers.dquote
                                    * (V("NoSoftLineBreakEndline")
                                      + parsers.any
                                      - parsers.blankline^2
                                      - parsers.dquote)^0
                                    * parsers.dquote
                                    + (parsers.any - parsers.spacechar - parsers.newline
                                      - parsers.dquote - parsers.squote - parsers.backtick
                                      - parsers.equal - parsers.less - parsers.more)^1

parsers.html_attribute_value_specification  = parsers.optionalspace
                                            * parsers.equal
                                            * parsers.optionalspace
                                            * parsers.html_attribute_value

parsers.html_spnl = parsers.optionalspace
                  * (V("NoSoftLineBreakEndline") * parsers.optionalspace)^-1

parsers.html_inline_attribute_value_specification = parsers.html_spnl
                                                  * parsers.equal
                                                  * parsers.html_spnl
                                                  * parsers.html_inline_attribute_value

parsers.html_attribute  = parsers.html_attribute_spacing
                        * parsers.html_attribute_name
                        * parsers.html_inline_attribute_value_specification^-1

parsers.html_non_newline_attribute  = parsers.spacechar^1
                                    * parsers.html_attribute_name
                                    * parsers.html_attribute_value_specification^-1

parsers.nested_breaking_blank = parsers.newline
                              * parsers.check_minimal_blank_indent
                              * parsers.blankline

parsers.html_comment_start = P("<!--")

parsers.html_comment_end = P("-->")

parsers.html_comment = Cs( parsers.html_comment_start
                         * parsers.html_until_end(parsers.html_comment_end))

parsers.html_inline_comment = (parsers.html_comment_start / "")
                            * -P(">") * -P("->")
                            * Cs((V("NoSoftLineBreakEndline") + parsers.any
                                - parsers.nested_breaking_blank - parsers.html_comment_end)^0)
                            * (parsers.html_comment_end / "")

parsers.html_cdatasection_start = P("<![CDATA[")

parsers.html_cdatasection_end = P("]]>")

parsers.html_cdatasection = Cs( parsers.html_cdatasection_start
                              * parsers.html_until_end(parsers.html_cdatasection_end))

parsers.html_inline_cdatasection  = parsers.html_cdatasection_start
                                  * Cs(V("NoSoftLineBreakEndline") + parsers.any
                                      - parsers.nested_breaking_blank - parsers.html_cdatasection_end)^0
                                  * parsers.html_cdatasection_end

parsers.html_declaration_start = P("<!") * parsers.letter

parsers.html_declaration_end = P(">")

parsers.html_declaration  = Cs( parsers.html_declaration_start
                              * parsers.html_until_end(parsers.html_declaration_end))

parsers.html_inline_declaration = parsers.html_declaration_start
                                * Cs(V("NoSoftLineBreakEndline") + parsers.any
                                    - parsers.nested_breaking_blank - parsers.html_declaration_end)^0
                                * parsers.html_declaration_end

parsers.html_instruction_start = P("<?")

parsers.html_instruction_end = P("?>")

parsers.html_instruction  = Cs( parsers.html_instruction_start
                              * parsers.html_until_end(parsers.html_instruction_end))

parsers.html_inline_instruction = parsers.html_instruction_start
                                * Cs(V("NoSoftLineBreakEndline") + parsers.any
                                    - parsers.nested_breaking_blank - parsers.html_instruction_end)^0
                                * parsers.html_instruction_end

parsers.html_blankline  = parsers.newline
                        * parsers.optionalspace
                        * parsers.newline

parsers.html_tag_start = parsers.less

parsers.html_tag_closing_start  = parsers.less
                                * parsers.slash

parsers.html_tag_end  = parsers.html_spnl
                      * parsers.more

parsers.html_empty_tag_end  = parsers.html_spnl
                            * parsers.slash
                            * parsers.more

-- opening tags
parsers.html_any_open_inline_tag  = parsers.html_tag_start
                                  * parsers.keyword
                                  * parsers.html_attribute^0
                                  * parsers.html_tag_end

parsers.html_any_open_tag = parsers.html_tag_start
                          * parsers.keyword
                          * parsers.html_non_newline_attribute^0
                          * parsers.html_tag_end

parsers.html_open_tag = parsers.html_tag_start
                      * parsers.block_keyword
                      * parsers.html_attribute^0
                      * parsers.html_tag_end

parsers.html_open_special_tag = parsers.html_tag_start
                              * parsers.special_block_keyword
                              * parsers.html_attribute^0
                              * parsers.html_tag_end

-- incomplete tags
parsers.incomplete_tag_following  = parsers.spacechar
                                  + parsers.more
                                  + parsers.slash * parsers.more
                                  + #(parsers.newline + parsers.eof)

parsers.incomplete_special_tag_following  = parsers.spacechar
                                          + parsers.more
                                          + #(parsers.newline + parsers.eof)

parsers.html_incomplete_open_tag  = parsers.html_tag_start
                                  * parsers.block_keyword
                                  * parsers.incomplete_tag_following

parsers.html_incomplete_open_special_tag  = parsers.html_tag_start
                                          * parsers.special_block_keyword
                                          * parsers.incomplete_special_tag_following

parsers.html_incomplete_close_tag = parsers.html_tag_closing_start
                                  * parsers.block_keyword
                                  * parsers.incomplete_tag_following

parsers.html_incomplete_close_special_tag = parsers.html_tag_closing_start
                                          * parsers.special_block_keyword
                                          * parsers.incomplete_tag_following

-- closing tags
parsers.html_close_tag  = parsers.html_tag_closing_start
                        * parsers.block_keyword
                        * parsers.html_tag_end

parsers.html_any_close_tag  = parsers.html_tag_closing_start
                            * parsers.keyword
                            * parsers.html_tag_end

parsers.html_close_special_tag = parsers.html_tag_closing_start
                               * parsers.special_block_keyword
                               * parsers.html_tag_end

-- empty tags
parsers.html_any_empty_inline_tag = parsers.html_tag_start
                                  * parsers.keyword
                                  * parsers.html_attribute^0
                                  * parsers.html_empty_tag_end

parsers.html_any_empty_tag  = parsers.html_tag_start
                            * parsers.keyword
                            * parsers.html_non_newline_attribute^0
                            * parsers.optionalspace
                            * parsers.slash
                            * parsers.more

parsers.html_empty_tag  = parsers.html_tag_start
                        * parsers.block_keyword
                        * parsers.html_attribute^0
                        * parsers.html_empty_tag_end

parsers.html_empty_special_tag  = parsers.html_tag_start
                                * parsers.special_block_keyword
                                * parsers.html_attribute^0
                                * parsers.html_empty_tag_end

parsers.html_incomplete_blocks  = parsers.html_incomplete_open_tag
                                + parsers.html_incomplete_open_special_tag
                                + parsers.html_incomplete_close_tag

-- parse special html blocks
parsers.html_blankline_ending_special_block_opening = (parsers.html_close_special_tag
                                                      + parsers.html_empty_special_tag)
                                                    * #(parsers.optionalspace
                                                       * (parsers.newline + parsers.eof))

parsers.html_blankline_ending_special_block = parsers.html_blankline_ending_special_block_opening
                                            * parsers.html_blankline_end_condition

parsers.html_special_block_opening  = parsers.html_incomplete_open_special_tag
                                    - parsers.html_empty_special_tag

parsers.html_closing_special_block  = parsers.html_special_block_opening
                                    * parsers.html_until_end(parsers.html_close_special_tag)

parsers.html_special_block  = parsers.html_blankline_ending_special_block
                            + parsers.html_closing_special_block

-- parse html blocks
parsers.html_block_opening  = parsers.html_incomplete_open_tag
                            + parsers.html_incomplete_close_tag

parsers.html_block  = parsers.html_block_opening
                    * parsers.html_blankline_end_condition

-- parse any html blocks
parsers.html_any_block_opening  = (parsers.html_any_open_tag
                                + parsers.html_any_close_tag
                                + parsers.html_any_empty_tag)
                                * #(parsers.optionalspace * (parsers.newline + parsers.eof))

parsers.html_any_block  = parsers.html_any_block_opening
                        * parsers.html_blankline_end_condition

parsers.html_inline_comment_full  = parsers.html_comment_start
                                  * -P(">") * -P("->")
                                  * Cs((V("NoSoftLineBreakEndline") + parsers.any - P("--")
                                      - parsers.nested_breaking_blank - parsers.html_comment_end)^0)
                                  * parsers.html_comment_end

parsers.html_inline_tags  = parsers.html_inline_comment_full
                          + parsers.html_any_empty_inline_tag
                          + parsers.html_inline_instruction
                          + parsers.html_inline_cdatasection
                          + parsers.html_inline_declaration
                          + parsers.html_any_open_inline_tag
                          + parsers.html_any_close_tag

parsers.urlchar = parsers.anyescaped
                - parsers.newline
                - parsers.more

parsers.auto_link_scheme_part = parsers.alphanumeric
                              + parsers.plus
                              + parsers.period
                              + parsers.dash

parsers.auto_link_scheme  = parsers.letter
                          * parsers.auto_link_scheme_part
                          * parsers.auto_link_scheme_part^-30

parsers.absolute_uri  = parsers.auto_link_scheme * parsers.colon
                      * (parsers.any - parsers.spacing - parsers.less - parsers.more)^0

parsers.printable_characters = S(".!#$%&'*+/=?^_`{|}~-")

parsers.email_address_local_part_char = parsers.alphanumeric
                                      + parsers.printable_characters

parsers.email_address_local_part = parsers.email_address_local_part_char^1

parsers.email_address_dns_label = parsers.alphanumeric
                                * (parsers.alphanumeric + parsers.dash)^-62
                                * B(parsers.alphanumeric)

parsers.email_address_domain  = parsers.email_address_dns_label
                              * (parsers.period * parsers.email_address_dns_label)^0

parsers.email_address = parsers.email_address_local_part
                      * parsers.at
                      * parsers.email_address_domain

parsers.auto_link_url = parsers.less
                      * C(parsers.absolute_uri)
                      * parsers.more

parsers.auto_link_email = parsers.less
                        * C(parsers.email_address)
                        * parsers.more

parsers.auto_link_relative_reference = parsers.less
                                    * C(parsers.urlchar^1)
                                    * parsers.more

parsers.autolink  = parsers.auto_link_url
                  + parsers.auto_link_email

-- content in balanced brackets, parentheses, or quotes:
parsers.bracketed   = P{ parsers.lbracket
                       * (( parsers.backslash / "" * parsers.rbracket
                          + parsers.any - (parsers.lbracket
                                          + parsers.rbracket
                                          + parsers.blankline^2)
                          ) + V(1))^0
                       * parsers.rbracket }

parsers.inparens    = P{ parsers.lparent
                       * ((parsers.anyescaped - (parsers.lparent
                                                + parsers.rparent
                                                + parsers.blankline^2)
                          ) + V(1))^0
                       * parsers.rparent }

parsers.squoted     = P{ parsers.squote * parsers.alphanumeric
                       * ((parsers.anyescaped - (parsers.squote
                                                + parsers.blankline^2)
                          ) + V(1))^0
                       * parsers.squote }

parsers.dquoted     = P{ parsers.dquote * parsers.alphanumeric
                       * ((parsers.anyescaped - (parsers.dquote
                                                + parsers.blankline^2)
                          ) + V(1))^0
                       * parsers.dquote }

parsers.link_text  = parsers.lbracket
                    * Cs((parsers.alphanumeric^1
                        + parsers.bracketed
                        + parsers.inticks
                        + parsers.autolink
                        + V("InlineHtml")
                        + ( parsers.backslash * parsers.backslash)
                        + ( parsers.backslash * (parsers.lbracket + parsers.rbracket)
                          + V("NoSoftLineBreakSpace")
                          + V("NoSoftLineBreakEndline")
                          + (parsers.any
                            - (parsers.newline + parsers.lbracket + parsers.rbracket + parsers.blankline^2))))^0)
                    * parsers.rbracket

parsers.link_label_body = -#(parsers.sp * parsers.rbracket)
                        * #((parsers.any - parsers.rbracket)^-999 * parsers.rbracket)
                        * Cs((parsers.alphanumeric^1
                            + parsers.inticks
                            + parsers.autolink
                            + V("InlineHtml")
                            + ( parsers.backslash * parsers.backslash)
                            + ( parsers.backslash * (parsers.lbracket + parsers.rbracket)
                              + V("NoSoftLineBreakSpace")
                              + V("NoSoftLineBreakEndline")
                              + (parsers.any
                                - (parsers.newline + parsers.lbracket + parsers.rbracket + parsers.blankline^2))))^1)

parsers.link_label  = parsers.lbracket
                    * parsers.link_label_body
                    * parsers.rbracket

parsers.inparens_url  = P{ parsers.lparent
                         * ((parsers.anyescaped - (parsers.lparent
                                                  + parsers.rparent
                                                  + parsers.spacing)
                            ) + V(1))^0
                         * parsers.rparent }

-- url for markdown links, allowing nested brackets:
parsers.url         = parsers.less * Cs((parsers.anyescaped
                                        - parsers.newline
                                        - parsers.less
                                        - parsers.more)^0)
                                   * parsers.more
                    + -parsers.less
                    * Cs((parsers.inparens_url + (parsers.anyescaped
                                                 - parsers.spacing
                                                 - parsers.lparent
                                                 - parsers.rparent))^1)

-- quoted text:
parsers.title_s     = parsers.squote
                    * Cs((parsers.html_entities
                         + V("NoSoftLineBreakSpace")
                         + V("NoSoftLineBreakEndline")
                         + (parsers.anyescaped - parsers.newline - parsers.squote - parsers.blankline^2))^0)
                    * parsers.squote

parsers.title_d     = parsers.dquote
                    * Cs((parsers.html_entities
                         + V("NoSoftLineBreakSpace")
                         + V("NoSoftLineBreakEndline")
                         + (parsers.anyescaped - parsers.newline - parsers.dquote - parsers.blankline^2))^0)
                    * parsers.dquote

parsers.title_p     = parsers.lparent
                    * Cs((parsers.html_entities
                         + V("NoSoftLineBreakSpace")
                         + V("NoSoftLineBreakEndline")
                         + (parsers.anyescaped - parsers.newline - parsers.lparent - parsers.rparent
                           - parsers.blankline^2))^0)
                    * parsers.rparent

parsers.title       = parsers.title_d + parsers.title_s + parsers.title_p

parsers.optionaltitle
                    = parsers.spnlc * parsers.title * parsers.spacechar^0
                    + Cc("")

-- parse a reference definition:  [foo]: /bar "title"
parsers.define_reference_parser = (parsers.check_trail / "") * parsers.link_label * parsers.colon
                                * parsers.spnlc * parsers.url
                                * ( parsers.spnlc_sep * parsers.title * parsers.only_blank
                                  + Cc("") * parsers.only_blank)
parsers.Inline         = V("Inline")

-- parse many p between starter and ender
parsers.between = function(p, starter, ender)
  local ender2 = B(parsers.nonspacechar) * ender
  return (starter * #parsers.nonspacechar * Ct(p * (p - ender2)^0) * ender2)
end

parsers.lineof = function(c)
    return (parsers.check_trail_no_rem * (P(c) * parsers.optionalspace)^3
           * (parsers.newline + parsers.eof))
end

parsers.thematic_break_lines = parsers.lineof(parsers.asterisk)
                             + parsers.lineof(parsers.dash)
                             + parsers.lineof(parsers.underscore)
-- parse Atx heading start and return level
parsers.heading_start = #parsers.hash * C(parsers.hash^-6)
                      * -parsers.hash / length

-- parse setext header ending and return level
parsers.heading_level = parsers.nonindentspace * parsers.equal^1 * parsers.optionalspace * #parsers.newline * Cc(1)
                      + parsers.nonindentspace * parsers.dash^1 * parsers.optionalspace * #parsers.newline * Cc(2)

local function strip_atx_end(s)
  return s:gsub("%s+#*%s*\n$","")
end

parsers.atx_heading = parsers.check_trail_no_rem
                    * Cg(parsers.heading_start, "level")
                    * (C( parsers.optionalspace
                        * parsers.hash^0
                        * parsers.optionalspace
                        * parsers.newline)
                      + parsers.spacechar^1
                      * C(parsers.line))
M.reader = {}
function M.reader.new(writer, options)
  local self = {}
  self.writer = writer
  self.options = options
  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)
    tag = util.rope_to_string(tag)
    tag = tag:gsub("[ \n\r\t]+", " ")
    tag = tag:gsub("^ ", ""):gsub(" $", "")
    tag = uni_algos.case.casefold(tag, true, false)
    return tag
  end
  local function iterlines(s, f)
    local rope = lpeg.match(Ct((parsers.line / f)^1), s)
    return util.rope_to_string(rope)
  end
  if options.preserveTabs then
    self.expandtabs = function(s) return s end
  else
    self.expandtabs = function(s)
                        if s:find("\t") then
                          return iterlines(s, util.expand_tabs_in_line)
                        else
                          return s
                        end
                      end
  end
  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)
              if lpeg.match(parsers.nonemptyline, line) == nil then
                  return line
              end
              line = util.expand_tabs_in_line(line)
              local prefix = lpeg.match(C(parsers.optionalspace), line)
              local prefix_length = #prefix
              local is_shorter = min_prefix_length == nil
              is_shorter = is_shorter or prefix_length < min_prefix_length
              if is_shorter then
                  min_prefix_length, min_prefix = prefix_length, prefix
              end
              return line
          end)
          str = str:gsub('^' .. min_prefix, '')
      end
      if toplevel and (options.texComments or options.hybrid) then
        str = lpeg.match(Ct(parsers.commented_line^1), str)
        str = util.rope_to_string(str)
      end
      local res = lpeg.match(grammar(), str)
      if res == nil then
        error(format("%s failed on:\n%s", name, str:sub(1,20)))
      else
        return res
      end
    end
  end

  self.create_parser("parse_blocks",
                     function()
                       return parsers.blocks
                     end, true)

  self.create_parser("parse_blocks_nested",
                     function()
                       return parsers.blocks_nested
                     end, false)

  self.create_parser("parse_inlines",
                     function()
                       return parsers.inlines
                     end, false)

  self.create_parser("parse_inlines_no_inline_note",
                     function()
                       return parsers.inlines_no_inline_note
                     end, false)

  self.create_parser("parse_inlines_no_html",
                     function()
                       return parsers.inlines_no_html
                     end, false)

  self.create_parser("parse_inlines_nbsp",
                     function()
                       return parsers.inlines_nbsp
                     end, false)
  self.create_parser("parse_inlines_no_link_or_emphasis",
                    function()
                      return parsers.inlines_no_link_or_emphasis
                    end, false)
  parsers.minimally_indented_blankline = parsers.check_minimal_indent * (parsers.blankline / "")

  parsers.minimally_indented_block = parsers.check_minimal_indent * V("Block")

  parsers.minimally_indented_block_or_paragraph = parsers.check_minimal_indent * V("BlockOrParagraph")

  parsers.minimally_indented_paragraph = parsers.check_minimal_indent * V("Paragraph")

  parsers.minimally_indented_plain = parsers.check_minimal_indent * V("Plain")

  parsers.minimally_indented_par_or_plain = parsers.minimally_indented_paragraph
                                          + parsers.minimally_indented_plain

  parsers.minimally_indented_par_or_plain_no_blank  = parsers.minimally_indented_par_or_plain
                                                    - parsers.minimally_indented_blankline

  parsers.minimally_indented_ref = parsers.check_minimal_indent * V("Reference")

  parsers.minimally_indented_blank = parsers.check_minimal_indent * V("Blank")

  parsers.conditionally_indented_blankline = parsers.check_minimal_blank_indent * (parsers.blankline / "")

  parsers.minimally_indented_ref_or_block = parsers.minimally_indented_ref
                                          + parsers.minimally_indented_block
                                          - parsers.minimally_indented_blankline

  parsers.minimally_indented_ref_or_block_or_par  = parsers.minimally_indented_ref
                                                  + parsers.minimally_indented_block_or_paragraph
                                                  - parsers.minimally_indented_blankline


  parsers.separator_loop = function(separated_block, paragraph, block_separator, paragraph_separator)
    return  separated_block
          + block_separator
            * paragraph
            * separated_block
          + paragraph_separator
          * paragraph
  end

  parsers.create_loop_body_pair = function(separated_block, paragraph, block_separator, paragraph_separator)
    return {
      block = parsers.separator_loop(separated_block, paragraph, block_separator, block_separator),
      par = parsers.separator_loop(separated_block, paragraph, block_separator, paragraph_separator)
    }
  end

  parsers.block_sep_group = function(blank)
    return  blank^0 * parsers.eof
          + ( blank^2 / writer.paragraphsep
            + blank^0 / writer.interblocksep
            )
  end

  parsers.par_sep_group = function(blank)
    return  blank^0 * parsers.eof
          + blank^0 / writer.paragraphsep
  end

  parsers.sep_group_no_output = function(blank)
    return  blank^0 * parsers.eof
          + blank^0
  end

  parsers.content_blank = parsers.minimally_indented_blankline

  parsers.ref_or_block_separated  = parsers.sep_group_no_output(parsers.content_blank)
                                  * ( parsers.minimally_indented_ref
                                    - parsers.content_blank)
                                  + parsers.block_sep_group(parsers.content_blank)
                                  * ( parsers.minimally_indented_block
                                    - parsers.content_blank)

  parsers.loop_body_pair  =
    parsers.create_loop_body_pair(parsers.ref_or_block_separated,
                                  parsers.minimally_indented_par_or_plain_no_blank,
                                  parsers.block_sep_group(parsers.content_blank),
                                  parsers.par_sep_group(parsers.content_blank))

  parsers.content_loop  = ( V("Block")
                          * parsers.loop_body_pair.block^0
                          + (V("Paragraph") + V("Plain"))
                          * parsers.ref_or_block_separated
                          * parsers.loop_body_pair.block^0
                          + (V("Paragraph") + V("Plain"))
                          * parsers.loop_body_pair.par^0)
                        * parsers.content_blank^0

  parsers.indented_content = function()
    return  Ct( (V("Reference") + (parsers.blankline / ""))
              * parsers.content_blank^0
              * parsers.check_minimal_indent
              * parsers.content_loop
              + (V("Reference") + (parsers.blankline / ""))
              * parsers.content_blank^0
              + parsers.content_loop)
  end

  parsers.add_indent = function(pattern, name, breakable)
    return  Cg(Cmt( Cb("indent_info")
                  * Ct(pattern)
                  * (#parsers.linechar * Cc(false) + Cc(true)) -- check if starter is blank
                  * Cc(name)
                  * Cc(breakable),
              process_starter_indent), "indent_info")
  end

  if options.hashEnumerators then
    parsers.dig = parsers.digit + parsers.hash
  else
    parsers.dig = parsers.digit
  end

  parsers.enumerator = function(delimiter_type, interrupting)
    local delimiter_range
    local allowed_end
    if interrupting then
      delimiter_range = P("1")
      allowed_end = C(parsers.spacechar^1) * #parsers.linechar
    else
      delimiter_range = parsers.dig * parsers.dig^-8
      allowed_end = C(parsers.spacechar^1) + #(parsers.newline + parsers.eof)
    end

    return parsers.check_trail
             * Ct(C(delimiter_range) * C(delimiter_type))
             * allowed_end
  end

  parsers.starter = parsers.bullet(parsers.dash)
                  + parsers.bullet(parsers.asterisk)
                  + parsers.bullet(parsers.plus)
                  + parsers.enumerator(parsers.period)
                  + parsers.enumerator(parsers.rparent)

  parsers.blockquote_start = parsers.check_trail * C(parsers.more) * C(parsers.spacechar^0)

  parsers.blockquote_body = parsers.add_indent(parsers.blockquote_start, "bq", true)
                          * parsers.indented_content()
                          * remove_indent("bq")

  if not options.breakableBlockquotes then
    parsers.blockquote_body = parsers.add_indent(parsers.blockquote_start, "bq", false)
                            * parsers.indented_content()
                            * remove_indent("bq")
  end
  local function parse_content_part(content_part)
    local rope = util.rope_to_string(content_part)
    local parsed = self.parser_functions.parse_inlines_no_link_or_emphasis(rope)
    parsed.indent_info = nil
    return parsed
  end

  local function collect_emphasis_content(t, opening_index, closing_index)
    local content = {}

    local content_part = {}
    for i = opening_index, closing_index do
      local value = t[i]

      if value.rendered ~= nil then
        content[#content + 1] = parse_content_part(content_part)
        content_part = {}
        content[#content + 1] = value.rendered
        value.rendered = nil
      else
        if value.type == "delimiter" and value.element == "emphasis" then
          if value.is_active then
            content_part[#content_part + 1] = string.rep(value.character, value.current_count)
          end
        else
          content_part[#content_part + 1] = value.content
        end
        value.content = ''
        value.is_active = false
      end
    end

    if next(content_part) ~= nil then
      content[#content + 1] = parse_content_part(content_part)
    end

    return content
  end

  local function fill_emph(t, opening_index, closing_index)
    local content = collect_emphasis_content(t, opening_index + 1, closing_index - 1)
    t[opening_index + 1].is_active = true
    t[opening_index + 1].rendered = writer.emphasis(content)
  end

  local function fill_strong(t, opening_index, closing_index)
    local content = collect_emphasis_content(t, opening_index + 1, closing_index - 1)
    t[opening_index + 1].is_active = true
    t[opening_index + 1].rendered = writer.strong(content)
  end

  local function breaks_three_rule(opening_delimiter, closing_delimiter)
    return (opening_delimiter.is_closing or closing_delimiter.is_opening) and
      ((opening_delimiter.original_count + closing_delimiter.original_count) % 3 == 0) and
      (opening_delimiter.original_count % 3 ~= 0 or closing_delimiter.original_count % 3 ~= 0)
  end

  local function find_emphasis_opener(t, bottom_index, latest_index, character, closing_delimiter)
    for i = latest_index, bottom_index, -1 do
      local value = t[i]
      if value.is_active and
         value.is_opening and
         value.type == "delimiter" and
         value.element == "emphasis" and
         (value.character == character) and
         (value.current_count > 0) then
        if not breaks_three_rule(value, closing_delimiter) then
          return i
        end
      end
    end
  end

  local function process_emphasis(t, opening_index, closing_index)
    for i = opening_index, closing_index do
      local value = t[i]
      if value.type == "delimiter" and value.element == "emphasis" then
          local delimiter_length = string.len(value.content)
          value.character = string.sub(value.content, 1, 1)
          value.current_count = delimiter_length
          value.original_count = delimiter_length
      end
    end

    local openers_bottom = {
      ['*'] = {
        [true] = {opening_index, opening_index, opening_index},
        [false] = {opening_index, opening_index, opening_index}
      },
      ['_'] = {
        [true] = {opening_index, opening_index, opening_index},
        [false] = {opening_index, opening_index, opening_index}
      }
    }

    local current_position = opening_index
    local max_position = closing_index

    while current_position <= max_position do
      local value = t[current_position]

      if value.type ~= "delimiter" or
        value.element ~= "emphasis" or
        not value.is_active or
        not value.is_closing or
        (value.current_count <= 0) then
        current_position = current_position + 1
        goto continue
      end

      local character = value.character
      local is_opening = value.is_opening
      local closing_length_modulo_three = value.original_count % 3

      local current_openers_bottom = openers_bottom[character][is_opening][closing_length_modulo_three + 1]

      local opener_position = find_emphasis_opener(t, current_openers_bottom, current_position - 1, character, value)

      if (opener_position == nil) then
        openers_bottom[character][is_opening][closing_length_modulo_three + 1] = current_position
        current_position = current_position + 1
        goto continue
      end

      local opening_delimiter = t[opener_position]

      local current_opening_count = opening_delimiter.current_count
      local current_closing_count = t[current_position].current_count

      if (current_opening_count >= 2) and (current_closing_count >= 2) then
        opening_delimiter.current_count = current_opening_count - 2
        t[current_position].current_count = current_closing_count - 2
        fill_strong(t, opener_position, current_position)
      else
        opening_delimiter.current_count = current_opening_count - 1
        t[current_position].current_count = current_closing_count - 1
        fill_emph(t, opener_position, current_position)
      end

      ::continue::
    end
  end

  local cont = lpeg.R("\128\191") -- continuation byte

  local function utf8_by_byte_count(n)
    if (n == 1) then
      return lpeg.R("\0\127")
    end
    if (n == 2) then
      return lpeg.R("\194\223") * cont
    end
    if (n == 3) then
      return lpeg.R("\224\239") * cont * cont
    end
    if (n == 4) then
      return lpeg.R("\240\244") * cont * cont * cont
    end
  end
  local function check_unicode_type(s, i, start_pos, end_pos, chartype)
    local c
    local char_length
    for pos = start_pos, end_pos, 1 do
      if (start_pos < 0) then
        char_length = -pos
      else
        char_length = pos + 1
      end

      if (chartype == "punctuation") then
        if lpeg.match(parsers.punctuation[char_length], s, i+pos) then
          return i
        end
      else
        c = lpeg.match({ C(utf8_by_byte_count(char_length)) },s,i+pos)
        if (c ~= nil) and (unicode.utf8.match(c, chartype)) then
          return i
        end
      end
    end
  end

  local function check_preceding_unicode_punctuation(s, i)
    return check_unicode_type(s, i, -4, -1, "punctuation")
  end

  local function check_preceding_unicode_whitespace(s, i)
    return check_unicode_type(s, i, -4, -1, "%s")
  end

  local function check_following_unicode_punctuation(s, i)
    return check_unicode_type(s, i, 0, 3, "punctuation")
  end

  local function check_following_unicode_whitespace(s, i)
    return check_unicode_type(s, i, 0, 3, "%s")
  end

  parsers.unicode_preceding_punctuation = B(parsers.escapable)
                                        + Cmt(parsers.succeed, check_preceding_unicode_punctuation)

  parsers.unicode_preceding_whitespace = Cmt(parsers.succeed, check_preceding_unicode_whitespace)

  parsers.unicode_following_punctuation = #parsers.escapable
                                        + Cmt(parsers.succeed, check_following_unicode_punctuation)

  parsers.unicode_following_whitespace = Cmt(parsers.succeed, check_following_unicode_whitespace)

  parsers.delimiter_run = function(character)
    return  (B(parsers.backslash * character) + -B(character))
          * character^1
          * -#character
  end

  parsers.left_flanking_delimiter_run = function(character)
    return  (B( parsers.any)
              * (parsers.unicode_preceding_punctuation + parsers.unicode_preceding_whitespace)
             + -B(parsers.any))
            * parsers.delimiter_run(character)
            * parsers.unicode_following_punctuation
            + parsers.delimiter_run(character)
            * -#(parsers.unicode_following_punctuation + parsers.unicode_following_whitespace
                + parsers.eof)
  end

  parsers.right_flanking_delimiter_run = function(character)
    return  parsers.unicode_preceding_punctuation
          * parsers.delimiter_run(character)
          * (parsers.unicode_following_punctuation + parsers.unicode_following_whitespace
            + parsers.eof)
          + (B(parsers.any)
            * -(parsers.unicode_preceding_punctuation + parsers.unicode_preceding_whitespace))
          * parsers.delimiter_run(character)
  end

  if options.underscores then
    parsers.emph_start = parsers.left_flanking_delimiter_run(parsers.asterisk)
                      + (-#parsers.right_flanking_delimiter_run(parsers.underscore)
                          + (parsers.unicode_preceding_punctuation
                            * #parsers.right_flanking_delimiter_run(parsers.underscore)))
                      * parsers.left_flanking_delimiter_run(parsers.underscore)

    parsers.emph_end  = parsers.right_flanking_delimiter_run(parsers.asterisk)
                      + (-#parsers.left_flanking_delimiter_run(parsers.underscore)
                        + #(parsers.left_flanking_delimiter_run(parsers.underscore)
                          * parsers.unicode_following_punctuation))
                      * parsers.right_flanking_delimiter_run(parsers.underscore)
  else
    parsers.emph_start = parsers.left_flanking_delimiter_run(parsers.asterisk)

    parsers.emph_end  = parsers.right_flanking_delimiter_run(parsers.asterisk)
  end

  parsers.emph_capturing_open_and_close = #parsers.emph_start * #parsers.emph_end
                                        * Ct( Cg(Cc("delimiter"), "type")
                                            * Cg(Cc("emphasis"), "element")
                                            * Cg(C(parsers.emph_start), "content")
                                            * Cg(Cc(true), "is_opening")
                                            * Cg(Cc(true), "is_closing"))

  parsers.emph_capturing_open = Ct( Cg(Cc("delimiter"), "type")
                                  * Cg(Cc("emphasis"), "element")
                                  * Cg(C(parsers.emph_start), "content")
                                  * Cg(Cc(true), "is_opening")
                                  * Cg(Cc(false), "is_closing"))

  parsers.emph_capturing_close = Ct( Cg(Cc("delimiter"), "type")
                                   * Cg(Cc("emphasis"), "element")
                                   * Cg(C(parsers.emph_end), "content")
                                   * Cg(Cc(false), "is_opening")
                                   * Cg(Cc(true), "is_closing"))

  parsers.emph_open_or_close  = parsers.emph_capturing_open_and_close
                              + parsers.emph_capturing_open
                              + parsers.emph_capturing_close

  parsers.emph_open = parsers.emph_capturing_open_and_close
                    + parsers.emph_capturing_open

  parsers.emph_close  = parsers.emph_capturing_open_and_close
                      + parsers.emph_capturing_close

  -- List of references defined in the document
  local references

  -- List of note references defined in the document
  parsers.rawnotes = {}

  function self.register_link(_, tag, url, title,
                              attributes)
    local normalized_tag = self.normalize_tag(tag)
      if references[normalized_tag] == nil then
        references[normalized_tag] = {
          url = url,
          title = title,
          attributes = attributes
        }
      end
    return ""
  end

  function self.lookup_reference(tag)
    return references[self.normalize_tag(tag)]
  end

  function self.lookup_note_reference(tag)
    return parsers.rawnotes[self.normalize_tag(tag)]
  end

  parsers.title_s_direct_ref  = parsers.squote
                              * Cs((parsers.html_entities
                                  + (parsers.anyescaped - parsers.squote - parsers.blankline^2))^0)
                              * parsers.squote

  parsers.title_d_direct_ref  = parsers.dquote
                              * Cs((parsers.html_entities
                                  + (parsers.anyescaped - parsers.dquote - parsers.blankline^2))^0)
                              * parsers.dquote

  parsers.title_p_direct_ref  = parsers.lparent
                              * Cs((parsers.html_entities
                                  + (parsers.anyescaped - parsers.lparent - parsers.rparent - parsers.blankline^2))^0)
                              * parsers.rparent

  parsers.title_direct_ref  = parsers.title_s_direct_ref
                            + parsers.title_d_direct_ref
                            + parsers.title_p_direct_ref

  parsers.inline_direct_ref_inside  = parsers.lparent * parsers.spnl
                                    * Cg(parsers.url + Cc(""), "url")
                                    * parsers.spnl
                                    * Cg(parsers.title_direct_ref + Cc(""), "title")
                                    * parsers.spnl * parsers.rparent

  parsers.inline_direct_ref = parsers.lparent * parsers.spnlc
                            * Cg(parsers.url + Cc(""), "url")
                            * parsers.spnlc
                            * Cg(parsers.title + Cc(""), "title")
                            * parsers.spnlc * parsers.rparent

  parsers.empty_link  = parsers.lbracket
                      * parsers.rbracket

  parsers.inline_link = parsers.link_text
                      * parsers.inline_direct_ref

  parsers.full_link = parsers.link_text
                    * parsers.link_label

  parsers.shortcut_link = parsers.link_label
                        * -(parsers.empty_link + parsers.link_label)

  parsers.collapsed_link  = parsers.link_label
                          * parsers.empty_link

  parsers.image_opening = #(parsers.exclamation * parsers.inline_link)
                        * Cg(Cc("inline"), "link_type")
                        + #(parsers.exclamation * parsers.full_link)
                        * Cg(Cc("full"), "link_type")
                        + #(parsers.exclamation * parsers.collapsed_link)
                        * Cg(Cc("collapsed"), "link_type")
                        + #(parsers.exclamation * parsers.shortcut_link)
                        * Cg(Cc("shortcut"), "link_type")
                        + #(parsers.exclamation * parsers.empty_link)
                        * Cg(Cc("empty"), "link_type")

  parsers.link_opening  = #parsers.inline_link
                        * Cg(Cc("inline"), "link_type")
                        + #parsers.full_link
                        * Cg(Cc("full"), "link_type")
                        + #parsers.collapsed_link
                        * Cg(Cc("collapsed"), "link_type")
                        + #parsers.shortcut_link
                        * Cg(Cc("shortcut"), "link_type")
                        + #parsers.empty_link
                        * Cg(Cc("empty_link"), "link_type")
                        + #parsers.link_text
                        * Cg(Cc("link_text"), "link_type")

  parsers.note_opening  = #(parsers.circumflex * parsers.shortcut_link)
                        * Cg(Cc("note_inline"), "link_type")

  parsers.raw_note_opening  = #( parsers.lbracket
                               * parsers.circumflex
                               * parsers.link_label_body
                               * parsers.rbracket)
                            * Cg(Cc("raw_note"), "link_type")

  local inline_note_element = Cg(Cc("note"), "element")
                            * parsers.note_opening
                            * Cg(parsers.circumflex * parsers.lbracket, "content")

  local image_element = Cg(Cc("image"), "element")
                      * parsers.image_opening
                      * Cg(parsers.exclamation * parsers.lbracket, "content")

  local note_element  = Cg(Cc("note"), "element")
                      * parsers.raw_note_opening
                      * Cg(parsers.lbracket * parsers.circumflex, "content")

  local link_element  = Cg(Cc("link"), "element")
                      * parsers.link_opening
                      * Cg(parsers.lbracket, "content")

  local opening_elements = parsers.fail

  if options.inlineNotes then
    opening_elements = opening_elements + inline_note_element
  end

  opening_elements = opening_elements + image_element

  if options.notes then
    opening_elements = opening_elements + note_element
  end

  opening_elements = opening_elements + link_element

  parsers.link_image_opening  = Ct( Cg(Cc("delimiter"), "type")
                                  * Cg(Cc(true), "is_opening")
                                  * Cg(Cc(false), "is_closing")
                                  * opening_elements)

  parsers.link_image_closing  = Ct( Cg(Cc("delimiter"), "type")
                                  * Cg(Cc("link"), "element")
                                  * Cg(Cc(false), "is_opening")
                                  * Cg(Cc(true), "is_closing")
                                  * ( Cg(Cc(true), "is_direct")
                                    * Cg(parsers.rbracket * #parsers.inline_direct_ref, "content")
                                    + Cg(Cc(false), "is_direct")
                                    * Cg(parsers.rbracket, "content")))

  parsers.link_image_open_or_close  = parsers.link_image_opening
                                    + parsers.link_image_closing

  if options.html then
    parsers.link_emph_precedence  = parsers.inticks
                                  + parsers.autolink
                                  + parsers.html_inline_tags
  else
    parsers.link_emph_precedence  = parsers.inticks
                                  + parsers.autolink
  end

  parsers.link_and_emph_endline = parsers.newline
                                * ((parsers.check_minimal_indent
                                  * -V("EndlineExceptions")
                                  + parsers.check_optional_indent
                                  * -V("EndlineExceptions")
                                  * -parsers.starter) / "")
                                * parsers.spacechar^0 / "\n"

  parsers.link_and_emph_content = Ct( Cg(Cc("content"), "type")
                                    * Cg(Cs(( parsers.link_emph_precedence
                                            + parsers.backslash * parsers.any
                                            + parsers.link_and_emph_endline
                                            + (parsers.linechar
                                              - parsers.blankline^2
                                              - parsers.link_image_open_or_close
                                              - parsers.emph_open_or_close))^0), "content"))

  parsers.link_and_emph_table = (parsers.link_image_opening + parsers.emph_open)
                              * parsers.link_and_emph_content
                              * ((parsers.link_image_open_or_close + parsers.emph_open_or_close)
                                * parsers.link_and_emph_content)^1

  local function collect_link_content(t, opening_index, closing_index)
    local content = {}
    for i = opening_index, closing_index do
      content[#content + 1] = t[i].content
    end
    return util.rope_to_string(content)
  end

  local function find_link_opener(t, bottom_index, latest_index)
    for i = latest_index, bottom_index, -1 do
      local value = t[i]
      if value.type == "delimiter" and
         value.is_opening and
         (value.element == "link" or value.element == "image" or value.element == "note")
         and not value.removed then
        if value.is_active then
          return i
        end
        value.removed = true
        return nil
      end
    end
  end

  local function find_next_link_closing_index(t, latest_index)
    for i = latest_index, #t do
      local value = t[i]
      if value.is_closing and
         value.element == "link" and
         not value.removed then
        return i
      end
    end
  end

  local function disable_previous_link_openers(t, opening_index)
    if t[opening_index].element == "image" then
      return
    end

    for i = opening_index, 1, -1 do
      local value = t[i]
      if value.is_active and
         value.type == "delimiter" and
         value.is_opening and
         value.element == "link" then
        value.is_active = false
      end
    end
  end

  local function disable_range(t, opening_index, closing_index)
    for i = opening_index, closing_index do
      local value = t[i]
      if value.is_active then
        value.is_active = false
        if value.type == "delimiter" then
          value.removed = true
        end
      end
    end
  end

  local function delete_parsed_content_in_range(t, opening_index, closing_index)
    for i = opening_index, closing_index do
      t[i].rendered = nil
    end
  end

  local function empty_content_in_range(t, opening_index, closing_index)
    for i = opening_index, closing_index do
      t[i].content = ''
    end
  end

  local function join_attributes(reference_attributes, own_attributes)
    local merged_attributes = {}
    for _, attribute in ipairs(reference_attributes or {}) do
      table.insert(merged_attributes, attribute)
    end
    for _, attribute in ipairs(own_attributes or {}) do
      table.insert(merged_attributes, attribute)
    end
    if next(merged_attributes) == nil then
      merged_attributes = nil
    end
    return merged_attributes
  end

  local function render_link_or_image(t, opening_index, closing_index, content_end_index, reference)
    process_emphasis(t, opening_index, content_end_index)
    local mapped = collect_emphasis_content(t, opening_index + 1, content_end_index - 1)

    local rendered = {}
    if (t[opening_index].element == "link") then
      rendered = writer.link(mapped, reference.url, reference.title, reference.attributes)
    end

    if (t[opening_index].element == "image") then
      rendered = writer.image(mapped, reference.url, reference.title, reference.attributes)
    end

    if (t[opening_index].element == "note") then
      if (t[opening_index].link_type == "note_inline") then
        rendered = writer.note(mapped)
      end
      if (t[opening_index].link_type == "raw_note") then
        rendered = writer.note(reference)
      end
    end

    t[opening_index].rendered = rendered
    delete_parsed_content_in_range(t, opening_index + 1, closing_index)
    empty_content_in_range(t, opening_index, closing_index)
    disable_previous_link_openers(t, opening_index)
    disable_range(t, opening_index, closing_index)
  end

  local function resolve_inline_following_content(t, closing_index, match_reference, match_link_attributes)
    local content = ""
    for i = closing_index + 1, #t do
      content = content .. t[i].content
    end

    local matching_content = parsers.succeed

    if match_reference then
      matching_content = matching_content * parsers.inline_direct_ref_inside
    end

    if match_link_attributes then
      matching_content = matching_content * Cg(Ct(parsers.attributes^-1), "attributes")
    end

    local matched = lpeg.match(Ct(matching_content * Cg(Cp(), "end_position")), content)

    local matched_count = matched.end_position - 1
    for i = closing_index + 1, #t do
      local value = t[i]

      local chars_left = matched_count
      matched_count = matched_count - #value.content

      if matched_count <= 0 then
        value.content = value.content:sub(chars_left + 1)
        break
      end

      value.content = ''
      value.is_active = false
    end

    local attributes = matched.attributes
    if attributes == nil or next(attributes) == nil then
      attributes = nil
    end

    return {
      url = matched.url or "",
      title = matched.title or "",
      attributes = attributes
    }
  end

  local function resolve_inline_link(t, opening_index, closing_index)
    local inline_content = resolve_inline_following_content(t, closing_index, true, t.match_link_attributes)
    render_link_or_image(t, opening_index, closing_index, closing_index, inline_content)
  end

  local function resolve_note_inline_link(t, opening_index, closing_index)
    local inline_content = resolve_inline_following_content(t, closing_index, false, false)
    render_link_or_image(t, opening_index, closing_index, closing_index, inline_content)
  end

  local function resolve_shortcut_link(t, opening_index, closing_index)
    local content = collect_link_content(t, opening_index + 1, closing_index - 1)
    local r = self.lookup_reference(content)

    if r then
      local inline_content = resolve_inline_following_content(t, closing_index, false, t.match_link_attributes)
      r.attributes = join_attributes(r.attributes, inline_content.attributes)
      render_link_or_image(t, opening_index, closing_index, closing_index, r)
    end
  end

  local function resolve_raw_note_link(t, opening_index, closing_index)
    local content = collect_link_content(t, opening_index + 1, closing_index - 1)
    local r = self.lookup_note_reference(content)

    if r then
      local parsed_ref = self.parser_functions.parse_blocks_nested(r)
      render_link_or_image(t, opening_index, closing_index, closing_index, parsed_ref)
    end
  end

  local function resolve_full_link(t, opening_index, closing_index)
    local next_link_closing_index = find_next_link_closing_index(t, closing_index + 4)
    local next_link_content = collect_link_content(t, closing_index + 3, next_link_closing_index - 1)
    local r = self.lookup_reference(next_link_content)

    if r then
      local inline_content = resolve_inline_following_content(t, next_link_closing_index, false,
                                                              t.match_link_attributes)
      r.attributes = join_attributes(r.attributes, inline_content.attributes)
      render_link_or_image(t, opening_index, next_link_closing_index, closing_index, r)
    end
  end

  local function resolve_collapsed_link(t, opening_index, closing_index)
    local next_link_closing_index = find_next_link_closing_index(t, closing_index + 4)
    local content = collect_link_content(t, opening_index + 1, closing_index - 1)
    local r = self.lookup_reference(content)

    if r then
      local inline_content = resolve_inline_following_content(t, closing_index, false, t.match_link_attributes)
      r.attributes = join_attributes(r.attributes, inline_content.attributes)
      render_link_or_image(t, opening_index, next_link_closing_index, closing_index, r)
    end
  end

  local function process_links_and_emphasis(t)
    for _,value in ipairs(t) do
      value.is_active = true
    end

    for i,value in ipairs(t) do
      if not value.is_closing or
        value.type ~= "delimiter" or
        not (value.element == "link" or value.element == "image" or value.element == "note") then
        goto continue
      end

      local opener_position = find_link_opener(t, 1, i - 1)
      if (opener_position == nil) then
        goto continue
      end

      local opening_delimiter = t[opener_position]
      opening_delimiter.removed = true

      local link_type = opening_delimiter.link_type

      if (link_type == "inline") then
        resolve_inline_link(t, opener_position, i)
      end
      if (link_type == "shortcut") then
        resolve_shortcut_link(t, opener_position, i)
      end
      if (link_type == "full") then
        resolve_full_link(t, opener_position, i)
      end
      if (link_type == "collapsed") then
        resolve_collapsed_link(t, opener_position, i)
      end
      if (link_type == "note_inline") then
        resolve_note_inline_link(t, opener_position, i)
      end
      if (link_type == "raw_note") then
        resolve_raw_note_link(t, opener_position, i)
      end

      ::continue::
    end

    t[#t].content = t[#t].content:gsub("%s*$","")

    process_emphasis(t, 1, #t)
    local final_result = collect_emphasis_content(t, 1, #t)
    return final_result
  end

  function self.defer_link_and_emphasis_processing(delimiter_table)
    return writer.defer_call(function()
      return process_links_and_emphasis(delimiter_table)
    end)
  end

  parsers.Str      = (parsers.normalchar * (parsers.normalchar + parsers.at)^0)
                   / writer.string

  parsers.Symbol   = (parsers.backtick^1 + V("SpecialChar"))
                   / writer.string

  parsers.Ellipsis = P("...") / writer.ellipsis

  parsers.Smart    = parsers.Ellipsis

  parsers.Code     = parsers.inticks / writer.code

  if options.blankBeforeBlockquote then
    parsers.bqstart = parsers.fail
  else
    parsers.bqstart = parsers.blockquote_start
  end

  if options.blankBeforeHeading then
    parsers.headerstart = parsers.fail
  else
    parsers.headerstart = parsers.atx_heading
  end

  if options.blankBeforeList then
    parsers.interrupting_bullets = parsers.fail
    parsers.interrupting_enumerators = parsers.fail
  else
    parsers.interrupting_bullets  = parsers.bullet(parsers.dash, true)
                                  + parsers.bullet(parsers.asterisk, true)
                                  + parsers.bullet(parsers.plus, true)

    parsers.interrupting_enumerators  = parsers.enumerator(parsers.period, true)
                                      + parsers.enumerator(parsers.rparent, true)
  end

  if options.html then
    parsers.html_interrupting = parsers.check_trail
                              * ( parsers.html_incomplete_open_tag
                                + parsers.html_incomplete_close_tag
                                + parsers.html_incomplete_open_special_tag
                                + parsers.html_comment_start
                                + parsers.html_cdatasection_start
                                + parsers.html_declaration_start
                                + parsers.html_instruction_start
                                - parsers.html_close_special_tag
                                - parsers.html_empty_special_tag)
  else
    parsers.html_interrupting = parsers.fail
  end

  parsers.EndlineExceptions
                     = parsers.blankline -- paragraph break
                     + parsers.eof       -- end of document
                     + parsers.bqstart
                     + parsers.thematic_break_lines
                     + parsers.interrupting_bullets
                     + parsers.interrupting_enumerators
                     + parsers.headerstart
                     + parsers.html_interrupting

  parsers.NoSoftLineBreakEndlineExceptions = parsers.EndlineExceptions

  parsers.endline = parsers.newline
                  * (parsers.check_minimal_indent
                    * -V("EndlineExceptions")
                    + parsers.check_optional_indent
                    * -V("EndlineExceptions")
                    * -parsers.starter)
                  * parsers.spacechar^0

  parsers.Endline = parsers.endline
                  / writer.soft_line_break

  parsers.EndlineNoSub = parsers.endline

  parsers.NoSoftLineBreakEndline
                        = parsers.newline
                        * (parsers.check_minimal_indent
                          * -V("NoSoftLineBreakEndlineExceptions")
                          + parsers.check_optional_indent
                          * -V("NoSoftLineBreakEndlineExceptions")
                          * -parsers.starter)
                        * parsers.spacechar^0
                        / writer.space

  parsers.EndlineBreak = parsers.backslash * parsers.Endline
                                           / writer.hard_line_break

  parsers.OptionalIndent
                     = parsers.spacechar^1 / writer.space

  parsers.Space      = parsers.spacechar^2 * parsers.Endline
                                           / writer.hard_line_break
                     + parsers.spacechar^1 * parsers.Endline^-1 * parsers.eof / self.expandtabs
                     + parsers.spacechar^1 * parsers.Endline
                                           / writer.soft_line_break
                     + parsers.spacechar^1 * -parsers.newline / self.expandtabs

  parsers.NoSoftLineBreakSpace
                     = parsers.spacechar^2 * parsers.Endline
                                           / writer.hard_line_break
                     + parsers.spacechar^1 * parsers.Endline^-1 * parsers.eof / self.expandtabs
                     + parsers.spacechar^1 * parsers.Endline
                                           / writer.soft_line_break
                     + parsers.spacechar^1 * -parsers.newline / self.expandtabs

  parsers.NonbreakingEndline
                    = parsers.endline
                    / writer.soft_line_break

  parsers.NonbreakingSpace
                  = parsers.spacechar^2 * parsers.Endline
                                        / writer.hard_line_break
                  + parsers.spacechar^1 * parsers.Endline^-1 * parsers.eof / ""
                  + parsers.spacechar^1 * parsers.Endline
                                        * parsers.optionalspace
                                        / writer.soft_line_break
                  + parsers.spacechar^1 * parsers.optionalspace
                                        / writer.nbsp

function self.auto_link_url(url, attributes)
  return writer.link(writer.escape(url),
                     url, nil, attributes)
end
function self.auto_link_email(email, attributes)
  return writer.link(writer.escape(email),
                     "mailto:"..email,
                     nil, attributes)
end

  parsers.AutoLinkUrl = parsers.auto_link_url
                      / self.auto_link_url

  parsers.AutoLinkEmail
                      = parsers.auto_link_email
                      / self.auto_link_email

  parsers.AutoLinkRelativeReference
                      = parsers.auto_link_relative_reference
                      / self.auto_link_url

  parsers.LinkAndEmph = Ct(parsers.link_and_emph_table)
                      / self.defer_link_and_emphasis_processing

  parsers.EscapedChar   = parsers.backslash * C(parsers.escapable) / writer.string

  parsers.InlineHtml    = Cs(parsers.html_inline_comment) / writer.inline_html_comment
                        + Cs(parsers.html_any_empty_inline_tag
                            + parsers.html_inline_instruction
                            + parsers.html_inline_cdatasection
                            + parsers.html_inline_declaration
                            + parsers.html_any_open_inline_tag
                            + parsers.html_any_close_tag)
                          / writer.inline_html_tag

  parsers.HtmlEntity    = parsers.html_entities / writer.string
  parsers.DisplayHtml = Cs(parsers.check_trail
                          * ( parsers.html_comment
                            + parsers.html_special_block
                            + parsers.html_block
                            + parsers.html_any_block
                            + parsers.html_instruction
                            + parsers.html_cdatasection
                            + parsers.html_declaration))
                        / writer.block_html_element

  parsers.indented_non_blank_line = parsers.indentedline - parsers.blankline

  parsers.Verbatim  = Cs(
                        parsers.check_code_trail
                      * (parsers.line - parsers.blankline)
                      * ((parsers.check_minimal_blank_indent_and_full_code_trail * parsers.blankline)^0
                        * ((parsers.check_minimal_indent / "") * parsers.check_code_trail
                           * (parsers.line - parsers.blankline))^1)^0
                      ) / self.expandtabs / writer.verbatim

  parsers.Blockquote   = parsers.blockquote_body
                       / writer.blockquote

  parsers.ThematicBreak = parsers.thematic_break_lines
                        / writer.thematic_break

  parsers.Reference    = parsers.define_reference_parser
                       / self.register_link

  parsers.Paragraph   = parsers.freeze_trail
                      * (Ct((parsers.Inline)^1)
                      * (parsers.newline + parsers.eof)
                      * parsers.unfreeze_trail
                      / writer.paragraph)

  parsers.Plain        = parsers.nonindentspace * Ct(parsers.Inline^1)
                       / writer.plain

  if options.taskLists then
    parsers.tickbox = ( parsers.ticked_box
                      + parsers.halfticked_box
                      + parsers.unticked_box
                      ) / writer.tickbox
  else
     parsers.tickbox = parsers.fail
  end

  parsers.list_blank = parsers.conditionally_indented_blankline

  parsers.ref_or_block_list_separated = parsers.sep_group_no_output(parsers.list_blank)
                                      * parsers.minimally_indented_ref
                                      + parsers.block_sep_group(parsers.list_blank)
                                      * parsers.minimally_indented_block

  parsers.ref_or_block_non_separated  = parsers.minimally_indented_ref
                                      + (parsers.succeed / writer.interblocksep)
                                      * parsers.minimally_indented_block
                                      - parsers.minimally_indented_blankline

  parsers.tight_list_loop_body_pair  =
    parsers.create_loop_body_pair(parsers.ref_or_block_non_separated,
                                  parsers.minimally_indented_par_or_plain_no_blank,
                                  (parsers.succeed / writer.interblocksep),
                                  (parsers.succeed / writer.paragraphsep))

  parsers.loose_list_loop_body_pair  =
    parsers.create_loop_body_pair(parsers.ref_or_block_list_separated,
                                  parsers.minimally_indented_par_or_plain,
                                  parsers.block_sep_group(parsers.list_blank),
                                  parsers.par_sep_group(parsers.list_blank))

  parsers.tight_list_content_loop = V("Block")
                                  * parsers.tight_list_loop_body_pair.block^0
                                  + (V("Paragraph") + V("Plain"))
                                  * parsers.ref_or_block_non_separated
                                  * parsers.tight_list_loop_body_pair.block^0
                                  +  (V("Paragraph") + V("Plain"))
                                  * parsers.tight_list_loop_body_pair.par^0

  parsers.loose_list_content_loop = V("Block")
                                  * parsers.loose_list_loop_body_pair.block^0
                                  + (V("Paragraph") + V("Plain"))
                                  * parsers.ref_or_block_list_separated
                                  * parsers.loose_list_loop_body_pair.block^0
                                  + (V("Paragraph") + V("Plain"))
                                  * parsers.loose_list_loop_body_pair.par^0

  parsers.list_item_tightness_condition = -#( parsers.list_blank^0
                                            * parsers.minimally_indented_ref_or_block_or_par)
                                        * remove_indent("li")
                                        + remove_indent("li")
                                        * parsers.fail

  parsers.indented_content_tight  = Ct( (parsers.blankline / "")
                                      * #parsers.list_blank
                                      * remove_indent("li")
                                      + ( (V("Reference") + (parsers.blankline / ""))
                                        * parsers.check_minimal_indent
                                        * parsers.tight_list_content_loop
                                        + (V("Reference") + (parsers.blankline / ""))
                                        + (parsers.tickbox^-1 / writer.escape)
                                        * parsers.tight_list_content_loop
                                        )
                                      * parsers.list_item_tightness_condition
                                  )

  parsers.indented_content_loose  = Ct( (parsers.blankline / "")
                                      * #parsers.list_blank
                                      + ( (V("Reference") + (parsers.blankline / ""))
                                        * parsers.check_minimal_indent
                                        * parsers.loose_list_content_loop
                                        + (V("Reference") + (parsers.blankline / ""))
                                        + (parsers.tickbox^-1 / writer.escape)
                                        * parsers.loose_list_content_loop
                                        )
                                  )

  parsers.TightListItem = function(starter)
    return  -parsers.ThematicBreak
            * parsers.add_indent(starter, "li")
            * parsers.indented_content_tight
  end

  parsers.LooseListItem = function(starter)
    return  -parsers.ThematicBreak
            * parsers.add_indent(starter, "li")
            * parsers.indented_content_loose
            * remove_indent("li")
  end

  parsers.BulletListOfType = function(bullet_type)
    local bullet = parsers.bullet(bullet_type)
    return  ( Ct( parsers.TightListItem(bullet)
                * ( (parsers.check_minimal_indent / "")
                  * parsers.TightListItem(bullet)
                  )^0
              )
              * Cc(true)
              * -#( (parsers.list_blank^0 / "")
                  * parsers.check_minimal_indent
                  * (bullet - parsers.ThematicBreak)
              )
              + Ct( parsers.LooseListItem(bullet)
                  * ( (parsers.list_blank^0 / "")
                    * (parsers.check_minimal_indent / "")
                    * parsers.LooseListItem(bullet)
                    )^0
              )
              * Cc(false)
            ) / writer.bulletlist
  end

  parsers.BulletList = parsers.BulletListOfType(parsers.dash)
                     + parsers.BulletListOfType(parsers.asterisk)
                     + parsers.BulletListOfType(parsers.plus)

  local function ordered_list(items,tight,starter)
    local startnum = starter[2][1]
    if options.startNumber then
      startnum = tonumber(startnum) or 1  -- fallback for '#'
      if startnum ~= nil then
        startnum = math.floor(startnum)
      end
    else
      startnum = nil
    end
    return writer.orderedlist(items,tight,startnum)
  end

  parsers.OrderedListOfType = function(delimiter_type)
    local enumerator = parsers.enumerator(delimiter_type)
    return  Cg(enumerator, "listtype")
          * (Ct( parsers.TightListItem(Cb("listtype"))
               * ((parsers.check_minimal_indent / "") * parsers.TightListItem(enumerator))^0)
          * Cc(true)
          * -#((parsers.list_blank^0 / "")
              * parsers.check_minimal_indent * enumerator)
          + Ct( parsers.LooseListItem(Cb("listtype"))
              * ((parsers.list_blank^0 / "")
                * (parsers.check_minimal_indent / "") * parsers.LooseListItem(enumerator))^0)
          * Cc(false)
          ) * Ct(Cb("listtype")) / ordered_list
  end

  parsers.OrderedList = parsers.OrderedListOfType(parsers.period)
                      + parsers.OrderedListOfType(parsers.rparent)
  parsers.Blank        = parsers.blankline / ""
                       + V("Reference")
  function parsers.parse_heading_text(s)
    local inlines = self.parser_functions.parse_inlines(s)
    local flatten_inlines = self.writer.flatten_inlines
    self.writer.flatten_inlines = true
    local flat_text = self.parser_functions.parse_inlines(s)
    flat_text = util.rope_to_string(flat_text)
    self.writer.flatten_inlines = flatten_inlines
    return {flat_text, inlines}
  end

  -- parse atx header
  parsers.AtxHeading = parsers.check_trail_no_rem
                     * Cg(parsers.heading_start, "level")
                     * ((C( parsers.optionalspace
                          * parsers.hash^0
                          * parsers.optionalspace
                          * parsers.newline)
                        + parsers.spacechar^1
                        * C(parsers.line))
                       / strip_atx_end
                       / parsers.parse_heading_text)
                     * Cb("level")
                     / writer.heading

  parsers.heading_line  = parsers.linechar^1
                        - parsers.thematic_break_lines

  parsers.heading_text = parsers.heading_line
                       * ((V("Endline") / "\n") * (parsers.heading_line - parsers.heading_level))^0
                       * parsers.newline^-1

  parsers.SetextHeading = parsers.freeze_trail * parsers.check_trail_no_rem
                        * #(parsers.heading_text
                           * parsers.check_minimal_indent * parsers.check_trail * parsers.heading_level)
                        * Cs(parsers.heading_text)
                        / parsers.parse_heading_text
                        * parsers.check_minimal_indent_and_trail * parsers.heading_level
                        * parsers.newline
                        * parsers.unfreeze_trail
                        / writer.heading

  parsers.Heading = parsers.AtxHeading + parsers.SetextHeading
  function self.finalize_grammar(extensions)
    local walkable_syntax = (function(global_walkable_syntax)
      local local_walkable_syntax = {}
      for lhs, rule in pairs(global_walkable_syntax) do
        local_walkable_syntax[lhs] = util.table_copy(rule)
      end
      return local_walkable_syntax
    end)(walkable_syntax)
    local current_extension_name = nil
    self.insert_pattern = function(selector, pattern, pattern_name)
      assert(pattern_name == nil or type(pattern_name) == "string")
      local _, _, lhs, pos, rhs = selector:find("^(%a+)%s+([%a%s]+%a+)%s+(%a+)$")
      assert(lhs ~= nil,
        [[Expected selector in form "LHS (before|after|instead of) RHS", not "]]
        .. selector .. [["]])
      assert(walkable_syntax[lhs] ~= nil,
        [[Rule ]] .. lhs .. [[ -> ... does not exist in markdown grammar]])
      assert(pos == "before" or pos == "after" or pos == "instead of",
        [[Expected positional specifier "before", "after", or "instead of", not "]]
        .. pos .. [["]])
      local rule = walkable_syntax[lhs]
      local index = nil
      for current_index, current_rhs in ipairs(rule) do
        if type(current_rhs) == "string" and current_rhs == rhs then
          index = current_index
          if pos == "after" then
            index = index + 1
          end
          break
        end
      end
      assert(index ~= nil,
        [[Rule ]] .. lhs .. [[ -> ]] .. rhs
          .. [[ does not exist in markdown grammar]])
      local accountable_pattern
      if current_extension_name then
        accountable_pattern = { pattern, current_extension_name, pattern_name }
      else
        assert(type(pattern) == "string",
          [[reader->insert_pattern() was called outside an extension with ]]
          .. [[a PEG pattern instead of a rule name]])
        accountable_pattern = pattern
      end
      if pos == "instead of" then
        rule[index] = accountable_pattern
      else
        table.insert(rule, index, accountable_pattern)
      end
    end
    local syntax =
      { "Blocks",

        Blocks                = V("InitializeState")
                              * ( V("ExpectedJekyllData")
                                * (V("Blank")^0 / writer.interblocksep)
                                )^-1
                              * V("Blank")^0
                              * ( V("Block")
                                * ( V("Blank")^0 * parsers.eof
                                  + ( V("Blank")^2 / writer.paragraphsep
                                    + V("Blank")^0 / writer.interblocksep
                                    )
                                  )
                                + ( V("Paragraph") + V("Plain") )
                                * ( V("Blank")^0 * parsers.eof
                                  + ( V("Blank")^2 / writer.paragraphsep
                                    + V("Blank")^0 / writer.interblocksep
                                    )
                                  )
                                * V("Block")
                                * ( V("Blank")^0 * parsers.eof
                                  + ( V("Blank")^2 / writer.paragraphsep
                                    + V("Blank")^0 / writer.interblocksep
                                    )
                                  )
                                + ( V("Paragraph") + V("Plain") )
                                * ( V("Blank")^0 * parsers.eof
                                  + V("Blank")^0 / writer.paragraphsep
                                  )
                                )^0,

        ExpectedJekyllData    = parsers.fail,

        Blank                 = parsers.Blank,
        Reference             = parsers.Reference,

        Blockquote            = parsers.Blockquote,
        Verbatim              = parsers.Verbatim,
        ThematicBreak         = parsers.ThematicBreak,
        BulletList            = parsers.BulletList,
        OrderedList           = parsers.OrderedList,
        DisplayHtml           = parsers.DisplayHtml,
        Heading               = parsers.Heading,
        Paragraph             = parsers.Paragraph,
        Plain                 = parsers.Plain,

        EndlineExceptions     = parsers.EndlineExceptions,
        NoSoftLineBreakEndlineExceptions
                              = parsers.NoSoftLineBreakEndlineExceptions,

        Str                   = parsers.Str,
        Space                 = parsers.Space,
        NoSoftLineBreakSpace  = parsers.NoSoftLineBreakSpace,
        OptionalIndent        = parsers.OptionalIndent,
        Endline               = parsers.Endline,
        EndlineNoSub          = parsers.EndlineNoSub,
        NoSoftLineBreakEndline
                              = parsers.NoSoftLineBreakEndline,
        EndlineBreak          = parsers.EndlineBreak,
        LinkAndEmph           = parsers.LinkAndEmph,
        Code                  = parsers.Code,
        AutoLinkUrl           = parsers.AutoLinkUrl,
        AutoLinkEmail         = parsers.AutoLinkEmail,
        AutoLinkRelativeReference
                              = parsers.AutoLinkRelativeReference,
        InlineHtml            = parsers.InlineHtml,
        HtmlEntity            = parsers.HtmlEntity,
        EscapedChar           = parsers.EscapedChar,
        Smart                 = parsers.Smart,
        Symbol                = parsers.Symbol,
        SpecialChar           = parsers.fail,
        InitializeState       = parsers.succeed,
      }
    self.update_rule = function(rule_name, get_pattern)
      assert(current_extension_name ~= nil)
      assert(syntax[rule_name] ~= nil,
        [[Rule ]] .. rule_name .. [[ -> ... does not exist in markdown grammar]])
      local previous_pattern
      local extension_name
      if walkable_syntax[rule_name] then
        local previous_accountable_pattern = walkable_syntax[rule_name][1]
        previous_pattern = previous_accountable_pattern[1]
        extension_name = previous_accountable_pattern[2] .. ", " .. current_extension_name
      else
        previous_pattern = nil
        extension_name = current_extension_name
      end
      local pattern
      if type(get_pattern) == "function" then
        pattern = get_pattern(previous_pattern)
      else
        assert(previous_pattern == nil,
               [[Rule ]] .. rule_name ..
               [[ has already been updated by ]] .. extension_name)
        pattern = get_pattern
      end
      local accountable_pattern = { pattern, extension_name, rule_name }
      walkable_syntax[rule_name] = { accountable_pattern }
    end
    local special_characters = {}
    self.add_special_character = function(c)
      table.insert(special_characters, c)
      syntax.SpecialChar = S(table.concat(special_characters, ""))
    end

    self.add_special_character("*")
    self.add_special_character("[")
    self.add_special_character("]")
    self.add_special_character("<")
    self.add_special_character("!")
    self.add_special_character("\\")
    self.initialize_named_group = function(name, value)
      local pattern = Ct("")
      if value ~= nil then
        pattern = pattern / value
      end
      syntax.InitializeState = syntax.InitializeState
                             * Cg(pattern, name)
    end
    self.initialize_named_group("indent_info")
    for _, extension in ipairs(extensions) do
      current_extension_name = extension.name
      extension.extend_writer(writer)
      extension.extend_reader(self)
    end
    current_extension_name = nil
    if options.debugExtensions then
      local sorted_lhs = {}
      for lhs, _ in pairs(walkable_syntax) do
        table.insert(sorted_lhs, lhs)
      end
      table.sort(sorted_lhs)

      local output_lines = {"{"}
      for lhs_index, lhs in ipairs(sorted_lhs) do
        local encoded_lhs = util.encode_json_string(lhs)
        table.insert(output_lines, [[    ]] ..encoded_lhs .. [[: []])
        local rule = walkable_syntax[lhs]
        for rhs_index, rhs in ipairs(rule) do
          local human_readable_rhs
          if type(rhs) == "string" then
            human_readable_rhs = rhs
          else
            local pattern_name
            if rhs[3] then
              pattern_name = rhs[3]
            else
              pattern_name = "Anonymous Pattern"
            end
            local extension_name = rhs[2]
            human_readable_rhs = pattern_name .. [[ (]] .. extension_name .. [[)]]
          end
          local encoded_rhs = util.encode_json_string(human_readable_rhs)
          local output_line = [[        ]] .. encoded_rhs
          if rhs_index < #rule then
            output_line = output_line .. ","
          end
          table.insert(output_lines, output_line)
        end
        local output_line = "    ]"
        if lhs_index < #sorted_lhs then
          output_line = output_line .. ","
        end
        table.insert(output_lines, output_line)
      end
      table.insert(output_lines, "}")

      local output = table.concat(output_lines, "\n")
      local output_filename = options.debugExtensionsFileName
      local output_file = assert(io.open(output_filename, "w"),
        [[Could not open file "]] .. output_filename .. [[" for writing]])
      assert(output_file:write(output))
      assert(output_file:close())
    end
    for lhs, rule in pairs(walkable_syntax) do
      syntax[lhs] = parsers.fail
      for _, rhs in ipairs(rule) do
        local pattern
        if type(rhs) == "string" then
          pattern = V(rhs)
        else
          pattern = rhs[1]
          if type(pattern) == "string" then
            pattern = V(pattern)
          end
        end
        syntax[lhs] = syntax[lhs] + pattern
      end
    end
    if options.underscores then
      self.add_special_character("_")
    end

    if not options.codeSpans then
      syntax.Code = parsers.fail
    else
      self.add_special_character("`")
    end

    if not options.html then
      syntax.DisplayHtml = parsers.fail
      syntax.InlineHtml = parsers.fail
      syntax.HtmlEntity  = parsers.fail
    else
      self.add_special_character("&")
    end

    if options.preserveTabs then
      options.stripIndent = false
    end

    if not options.smartEllipses then
      syntax.Smart = parsers.fail
    else
      self.add_special_character(".")
    end

    if not options.relativeReferences then
      syntax.AutoLinkRelativeReference = parsers.fail
    end

    if options.contentLevel == "inline" then
      syntax[1] = "Inlines"
      syntax.Inlines = V("InitializeState")
                      * parsers.Inline^0
                      * ( parsers.spacing^0
                        * parsers.eof / "")
      syntax.Space = parsers.Space + parsers.blankline / writer.space
    end

    local blocks_nested_t = util.table_copy(syntax)
    blocks_nested_t.ExpectedJekyllData = parsers.fail
    parsers.blocks_nested = Ct(blocks_nested_t)

    parsers.blocks = Ct(syntax)

    local inlines_t = util.table_copy(syntax)
    inlines_t[1] = "Inlines"
    inlines_t.Inlines = V("InitializeState")
                      * parsers.Inline^0
                      * ( parsers.spacing^0
                        * parsers.eof / "")
    parsers.inlines = Ct(inlines_t)

    local inlines_no_inline_note_t = util.table_copy(inlines_t)
    inlines_no_inline_note_t.InlineNote = parsers.fail
    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
    parsers.inlines_no_html = Ct(inlines_no_html_t)

    local inlines_nbsp_t = util.table_copy(inlines_t)
    inlines_nbsp_t.Endline = parsers.NonbreakingEndline
    inlines_nbsp_t.Space = parsers.NonbreakingSpace
    parsers.inlines_nbsp = Ct(inlines_nbsp_t)

    local inlines_no_link_or_emphasis_t = util.table_copy(inlines_t)
    inlines_no_link_or_emphasis_t.LinkAndEmph = parsers.fail
    inlines_no_link_or_emphasis_t.EndlineExceptions = parsers.EndlineExceptions - parsers.eof
    parsers.inlines_no_link_or_emphasis = Ct(inlines_no_link_or_emphasis_t)
    return function(input)
      if options.unicodeNormalization then
        local form = options.unicodeNormalizationForm
        if form == "nfc" then
          input = uni_algos.normalize.NFC(input)
        elseif form == "nfd" then
          input = uni_algos.normalize.NFD(input)
        elseif form == "nfkc" then
          input = uni_algos.normalize.NFKC(input)
        elseif form == "nfkd" then
          input = uni_algos.normalize.NFKD(input)
        else
          error(format("Unknown normalization form %s", form))
        end
      end
      input = input:gsub("\r\n?", "\n")
      if input:sub(-1) ~= "\n" then
        input = input .. "\n"
      end
      references = {}
      local opt_string = {}
      for k, _ in pairs(defaultOptions) do
        local v = options[k]
        if type(v) == "table" then
          for _, i in ipairs(v) do
            opt_string[#opt_string+1] = k .. "=" .. tostring(i)
          end
        elseif k ~= "cacheDir" then
          opt_string[#opt_string+1] = k .. "=" .. tostring(v)
        end
      end
      table.sort(opt_string)
      local salt = table.concat(opt_string, ",") .. "," .. metadata.version
      local output
      local function convert(input)
        local document = self.parser_functions.parse_blocks(input)
        local output = util.rope_to_string(writer.document(document))
        local undosep_start, undosep_end
        local potential_secend_start, secend_start
        local potential_sep_start, sep_start
        while true do
          -- find a `writer->undosep`
          undosep_start, undosep_end = output:find(writer.undosep_text, 1, true)
          if undosep_start == nil then break end
          -- skip any preceding section ends
          secend_start = undosep_start
          while true do
            potential_secend_start = secend_start - #writer.secend_text
            if potential_secend_start < 1
               or output:sub(potential_secend_start, secend_start - 1) ~= writer.secend_text then
              break
            end
            secend_start = potential_secend_start
          end
          -- find an immediately preceding block element / paragraph separator
          sep_start = secend_start
          potential_sep_start = sep_start - #writer.interblocksep_text
          if potential_sep_start >= 1
             and output:sub(potential_sep_start, sep_start - 1) == writer.interblocksep_text then
            sep_start = potential_sep_start
          else
            potential_sep_start = sep_start - #writer.paragraphsep_text
            if potential_sep_start >= 1
               and output:sub(potential_sep_start, sep_start - 1) == writer.paragraphsep_text then
              sep_start = potential_sep_start
            end
          end
          -- remove `writer->undosep` and immediately preceding block element / paragraph separator
          output = output:sub(1, sep_start - 1)
                .. output:sub(secend_start, undosep_start - 1)
                .. output:sub(undosep_end + 1)
        end
        return output
      end
      if options.eagerCache or options.finalizeCache then
        local name = util.cache(options.cacheDir, input, salt, convert,
                                ".md" .. writer.suffix)
        output = writer.pack(name)
      else
        output = convert(input)
      end
      if options.finalizeCache then
        local file, mode
        if options.frozenCacheCounter > 0 then
          mode = "a"
        else
          mode = "w"
        end
        file = assert(io.open(options.frozenCacheFileName, mode),
          [[Could not open file "]] .. options.frozenCacheFileName
          .. [[" for writing]])
        assert(file:write([[\expandafter\global\expandafter\def\csname ]]
          .. [[markdownFrozenCache]] .. options.frozenCacheCounter
          .. [[\endcsname{]] .. output .. [[}]] .. "\n"))
        assert(file:close())
      end
      return output
    end
  end
  return self
end
M.extensions = {}
M.extensions.bracketed_spans = function()
  return {
    name = "built-in bracketed_spans syntax extension",
    extend_writer = function(self)
      function self.span(s, attr)
        if self.flatten_inlines then return s end
        return {"\\markdownRendererBracketedSpanAttributeContextBegin",
                self.attributes(attr),
                s,
                "\\markdownRendererBracketedSpanAttributeContextEnd{}"}
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local span_label  = parsers.lbracket
                          * (Cs((parsers.alphanumeric^1
                              + parsers.inticks
                              + parsers.autolink
                              + V("InlineHtml")
                              + ( parsers.backslash * parsers.backslash)
                              + ( parsers.backslash * (parsers.lbracket + parsers.rbracket)
                                + V("Space") + V("Endline")
                                + (parsers.any
                                  - (parsers.newline + parsers.lbracket + parsers.rbracket
                                    + parsers.blankline^2))))^1)
                          / self.parser_functions.parse_inlines)
                          * parsers.rbracket

      local Span = span_label
                 * Ct(parsers.attributes)
                 / writer.span

      self.insert_pattern("Inline before LinkAndEmph",
                          Span, "Span")
    end
  }
end
M.extensions.citations = function(citation_nbsps)
  return {
    name = "built-in citations syntax extension",
    extend_writer = function(self)
      function self.citations(text_cites, cites)
        local buffer = {}
        if self.flatten_inlines then
          for _,cite in ipairs(cites) do
            if cite.prenote then
              table.insert(buffer, {cite.prenote, " "})
            end
            table.insert(buffer, cite.name)
            if cite.postnote then
              table.insert(buffer, {" ", cite.postnote})
            end
          end
        else
          table.insert(buffer, {"\\markdownRenderer", text_cites and "TextCite" or "Cite",
            "{", #cites, "}"})
          for _,cite in ipairs(cites) do
            table.insert(buffer, {cite.suppress_author and "-" or "+", "{",
              cite.prenote or "", "}{", cite.postnote or "", "}{", cite.name, "}"})
          end
        end
        return buffer
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      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.autolink
                         + V("InlineHtml")
                         + V("Space") + V("Endline")
                         + (parsers.anyescaped
                           - (parsers.newline + parsers.rbracket + parsers.blankline^2))
                         - (parsers.spnl * parsers.dash^-1 * parsers.at))^1)

      local citation_body_postnote
                    = Cs((parsers.alphanumeric^1
                         + parsers.bracketed
                         + parsers.inticks
                         + parsers.autolink
                         + V("InlineHtml")
                         + V("Space") + V("Endline")
                         + (parsers.anyescaped
                           - (parsers.newline + parsers.rbracket + parsers.semicolon
                             + parsers.blankline^2))
                         - (parsers.spnl * parsers.rbracket))^1)

      local citation_body_chunk
                    = ( citation_body_prenote
                      * parsers.spnlc_sep
                      + Cc("")
                      * parsers.spnlc
                    )
                    * citation_name
                    * (parsers.internal_punctuation - parsers.semicolon)^-1
                    * ( parsers.spnlc
                      * citation_body_postnote
                      + Cc("")
                      * parsers.spnlc
                    )

      local citation_body
                    = citation_body_chunk
                    * ( parsers.semicolon
                      * parsers.spnlc
                      * citation_body_chunk
                    )^0

      local citation_headless_body_postnote
                    = Cs((parsers.alphanumeric^1
                         + parsers.bracketed
                         + parsers.inticks
                         + parsers.autolink
                         + V("InlineHtml")
                         + V("Space") + V("Endline")
                         + (parsers.anyescaped
                           - (parsers.newline + parsers.rbracket + parsers.at
                             + parsers.semicolon + parsers.blankline^2))
                         - (parsers.spnl * parsers.rbracket))^0)

      local citation_headless_body
                    = citation_headless_body_postnote
                    * ( parsers.semicolon
                      * parsers.spnlc
                      * 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.identifier(raw_cites[i+2]),
                  postnote = normalize(raw_cites[i+3]),
              }
          end
          return writer.citations(text_cites, cites)
      end

      local TextCitations
                    = Ct((parsers.spnlc
                    * Cc("")
                    * citation_name
                    * ((parsers.spnlc
                        * parsers.lbracket
                        * citation_headless_body
                        * parsers.rbracket) + Cc("")))^1)
                    / function(raw_cites)
                        return citations(true, raw_cites)
                      end

      local ParenthesizedCitations
                    = Ct((parsers.spnlc
                    * parsers.lbracket
                    * citation_body
                    * parsers.rbracket)^1)
                    / function(raw_cites)
                        return citations(false, raw_cites)
                      end

      local Citations = TextCitations + ParenthesizedCitations

      self.insert_pattern("Inline before LinkAndEmph",
                          Citations, "Citations")

      self.add_special_character("@")
      self.add_special_character("-")
    end
  }
end
M.extensions.content_blocks = function(language_map)
  local languages_json = (function()
    local base, prev, curr
    for _, pathname in ipairs{kpse.lookup(language_map, { all=true })} do
      local file = io.open(pathname, "r")
      if not file then goto continue end
      local input = assert(file:read("*a"))
      assert(file:close())
      local json = input:gsub('("[^\n]-"):','[%1]=')
      curr = load("_ENV = {}; return "..json)()
      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 {
    name = "built-in content_blocks syntax extension",
    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 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.spacing
                           - #(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.check_trail_no_rem
                    * (localfilepath + onlineimageurl)
                    * contentblock_tail
                    / writer.contentblock

      self.insert_pattern("Block before Blockquote",
                          ContentBlock, "ContentBlock")
    end
  }
end
M.extensions.definition_lists = function(tight_lists)
  return {
    name = "built-in definition_lists syntax extension",
    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 writer = self.writer

      local defstartchar = S("~:")

      local defstart  = parsers.check_trail_length(0) * defstartchar * #parsers.spacing
                                      * (parsers.tab + parsers.space^-3)
                      + parsers.check_trail_length(1) * defstartchar * #parsers.spacing
                                       * (parsers.tab + parsers.space^-2)
                      + parsers.check_trail_length(2) * defstartchar * #parsers.spacing
                                       * (parsers.tab + parsers.space^-1)
                      + parsers.check_trail_length(3) * defstartchar * #parsers.spacing

      local indented_line = (parsers.check_minimal_indent / "") * parsers.check_code_trail * parsers.line

      local blank = parsers.check_minimal_blank_indent_and_any_trail * parsers.optionalspace * parsers.newline

      local dlchunk = Cs(parsers.line * (indented_line - blank)^0)

      local indented_blocks = function(bl)
        return Cs( bl
              * (blank^1 * (parsers.check_minimal_indent / "")
                * parsers.check_code_trail * -parsers.blankline * bl)^0
              * (blank^1 + parsers.eof))
      end

      local function definition_list_item(term, defs, _)
        return { term = self.parser_functions.parse_inlines(term),
                 definitions = defs }
      end

      local DefinitionListItemLoose
                    = C(parsers.line) * blank^0
                    * Ct((parsers.check_minimal_indent * (defstart
                         * indented_blocks(dlchunk)
                         / self.parser_functions.parse_blocks_nested))^1)
                    * Cc(false) / definition_list_item

      local DefinitionListItemTight
                    = C(parsers.line)
                    * Ct((parsers.check_minimal_indent * (defstart * dlchunk
                         / self.parser_functions.parse_blocks_nested))^1)
                    * Cc(true) / definition_list_item

      local DefinitionList
                    = ( Ct(DefinitionListItemLoose^1) * Cc(false)
                      + Ct(DefinitionListItemTight^1)
                      * (blank^0
                        * -DefinitionListItemLoose * Cc(true))
                      ) / writer.definitionlist

      self.insert_pattern("Block after Heading",
                          DefinitionList, "DefinitionList")
    end
  }
end
M.extensions.fancy_lists = function()
  return {
    name = "built-in fancy_lists syntax extension",
    extend_writer = function(self)
      local options = self.options

      function self.fancylist(items,tight,startnum,numstyle,numdelim)
        if not self.is_writing then return "" end
        local buffer = {}
        local num = startnum
        for _,item in ipairs(items) do
          if item ~= "" then
            buffer[#buffer + 1] = self.fancyitem(item,num)
          end
          if num ~= nil and item ~= "" then
            num = num + 1
          end
        end
        local contents = util.intersperse(buffer,"\n")
        if tight and options.tightLists then
          return {"\\markdownRendererFancyOlBeginTight{",
                  numstyle,"}{",numdelim,"}",contents,
                  "\n\\markdownRendererFancyOlEndTight "}
        else
          return {"\\markdownRendererFancyOlBegin{",
                  numstyle,"}{",numdelim,"}",contents,
                  "\n\\markdownRendererFancyOlEnd "}
        end
      end
      function self.fancyitem(s,num)
        if num ~= nil then
          return {"\\markdownRendererFancyOlItemWithNumber{",num,"}",s,
                  "\\markdownRendererFancyOlItemEnd "}
        else
          return {"\\markdownRendererFancyOlItem ",s,"\\markdownRendererFancyOlItemEnd "}
        end
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local options = self.options
      local writer = self.writer

      local function combine_markers_and_delims(markers, delims)
        local markers_table = {}
        for _,marker in ipairs(markers) do
          local start_marker
          local continuation_marker
          if type(marker) == "table" then
            start_marker = marker[1]
            continuation_marker = marker[2]
          else
            start_marker = marker
            continuation_marker = marker
          end
          for _,delim in ipairs(delims) do
            table.insert(markers_table, {start_marker, continuation_marker, delim})
          end
        end
        return markers_table
      end

      local function join_table_with_func(func, markers_table)
        local pattern = func(table.unpack(markers_table[1]))
        for i = 2, #markers_table do
          pattern = pattern + func(table.unpack(markers_table[i]))
        end
        return pattern
      end

      local lowercase_letter_marker = R("az")
      local uppercase_letter_marker = R("AZ")

      local roman_marker = function(chars)
        local m, d, c = P(chars[1]), P(chars[2]), P(chars[3])
        local l, x, v, i = P(chars[4]), P(chars[5]), P(chars[6]), P(chars[7])
        return  m^-3
                * (c*m + c*d + d^-1 * c^-3)
                * (x*c + x*l + l^-1 * x^-3)
                * (i*x + i*v + v^-1 * i^-3)
      end

      local lowercase_roman_marker  = roman_marker({"m", "d", "c", "l", "x", "v", "i"})
      local uppercase_roman_marker  = roman_marker({"M", "D", "C", "L", "X", "V", "I"})

      local lowercase_opening_roman_marker  = P("i")
      local uppercase_opening_roman_marker  = P("I")

      local digit_marker = parsers.dig * parsers.dig^-8

      local markers = {
        {lowercase_opening_roman_marker, lowercase_roman_marker},
        {uppercase_opening_roman_marker, uppercase_roman_marker},
        lowercase_letter_marker,
        uppercase_letter_marker,
        lowercase_roman_marker,
        uppercase_roman_marker,
        digit_marker
      }

      local delims = {
        parsers.period,
        parsers.rparent
      }

      local markers_table = combine_markers_and_delims(markers, delims)

      local function enumerator(start_marker, _, delimiter_type, interrupting)
        local delimiter_range
        local allowed_end
        if interrupting then
          delimiter_range = P("1")
          allowed_end = C(parsers.spacechar^1) * #parsers.linechar
        else
          delimiter_range = start_marker
          allowed_end = C(parsers.spacechar^1) + #(parsers.newline + parsers.eof)
        end

        return parsers.check_trail
                * Ct(C(delimiter_range) * C(delimiter_type))
                * allowed_end
      end

      local starter = join_table_with_func(enumerator, markers_table)

      local TightListItem = function(starter)
        return  parsers.add_indent(starter, "li")
                * parsers.indented_content_tight
      end

      local LooseListItem = function(starter)
        return  parsers.add_indent(starter, "li")
                * parsers.indented_content_loose
                * remove_indent("li")
      end

      local function roman2number(roman)
        local romans = { ["M"] = 1000, ["D"] = 500, ["C"] = 100, ["L"] = 50, ["X"] = 10, ["V"] = 5, ["I"] = 1 }
        local numeral = 0

        local i = 1
        local len = string.len(roman)
        while i < len do
          local z1, z2 = romans[ string.sub(roman, i, i) ], romans[ string.sub(roman, i+1, i+1) ]
          if z1 < z2 then
              numeral = numeral + (z2 - z1)
              i = i + 2
          else
              numeral = numeral + z1
              i = i + 1
          end
        end
        if i <= len then numeral = numeral + romans[ string.sub(roman,i,i) ] end
        return numeral
      end

      local function sniffstyle(numstr, delimend)
        local numdelim
        if delimend == ")" then
          numdelim = "OneParen"
        elseif delimend == "." then
          numdelim = "Period"
        else
          numdelim = "Default"
        end

        local num
        num = numstr:match("^([I])$")
        if num then
          return roman2number(num), "UpperRoman", numdelim
        end
        num = numstr:match("^([i])$")
        if num then
          return roman2number(string.upper(num)), "LowerRoman", numdelim
        end
        num = numstr:match("^([A-Z])$")
        if num then
          return string.byte(num) - string.byte("A") + 1, "UpperAlpha", numdelim
        end
        num = numstr:match("^([a-z])$")
        if num then
          return string.byte(num) - string.byte("a") + 1, "LowerAlpha", numdelim
        end
        num = numstr:match("^([IVXLCDM]+)")
        if num then
          return roman2number(num), "UpperRoman", numdelim
        end
        num = numstr:match("^([ivxlcdm]+)")
        if num then
          return roman2number(string.upper(num)), "LowerRoman", numdelim
        end
        return math.floor(tonumber(numstr) or 1), "Decimal", numdelim
      end

      local function fancylist(items,tight,start)
        local startnum, numstyle, numdelim = sniffstyle(start[2][1], start[2][2])
        return writer.fancylist(items,tight,
                                options.startNumber and startnum or 1,
                                numstyle or "Decimal",
                                numdelim or "Default")
      end

      local FancyListOfType = function(start_marker, continuation_marker, delimiter_type)
        local enumerator_start = enumerator(start_marker, continuation_marker, delimiter_type)
        local enumerator_cont = enumerator(continuation_marker, continuation_marker, delimiter_type)
        return Cg(enumerator_start, "listtype")
             * (Ct( TightListItem(Cb("listtype"))
                 * ((parsers.check_minimal_indent / "") * TightListItem(enumerator_cont))^0)
             * Cc(true)
             * -#((parsers.conditionally_indented_blankline^0 / "")
                 * parsers.check_minimal_indent * enumerator_cont)
             + Ct( LooseListItem(Cb("listtype"))
                 * ((parsers.conditionally_indented_blankline^0 / "")
                   * (parsers.check_minimal_indent / "") * LooseListItem(enumerator_cont))^0)
             * Cc(false)
             ) * Ct(Cb("listtype")) / fancylist
      end

      local FancyList = join_table_with_func(FancyListOfType, markers_table)

      local Endline   = parsers.newline
                      * (parsers.check_minimal_indent
                        * -parsers.EndlineExceptions
                        + parsers.check_optional_indent
                        * -parsers.EndlineExceptions
                        * -starter)
                      * parsers.spacechar^0
                      / writer.soft_line_break

      self.update_rule("OrderedList", FancyList)
      self.update_rule("Endline", Endline)
    end
  }
end
M.extensions.fenced_code = function(blank_before_code_fence,
                                    allow_attributes,
                                    allow_raw_blocks)
  return {
    name = "built-in fenced_code syntax extension",
    extend_writer = function(self)
      local options = self.options

      function self.fencedCode(s, i, attr)
        if not self.is_writing then return "" end
        s = s:gsub("\n$", "")
        local buf = {}
        if attr ~= nil then
          table.insert(buf, {"\\markdownRendererFencedCodeAttributeContextBegin",
                             self.attributes(attr)})
        end
        local name = util.cache_verbatim(options.cacheDir, s)
        table.insert(buf, {"\\markdownRendererInputFencedCode{",
                           name,"}{",self.string(i),"}{",self.infostring(i),"}"})
        if attr ~= nil then
          table.insert(buf, "\\markdownRendererFencedCodeAttributeContextEnd{}")
        end
        return buf
      end

      if allow_raw_blocks then
        function self.rawBlock(s, attr)
          if not self.is_writing then return "" end
          s = s:gsub("\n$", "")
          local name = util.cache_verbatim(options.cacheDir, s)
          return {"\\markdownRendererInputRawBlock{",
                  name,"}{", self.string(attr),"}"}
        end
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local function captures_geq_length(_,i,a,b)
        return #a >= #b and i
      end

      local function strip_enclosing_whitespaces(str)
        return str:gsub("^%s*(.-)%s*$", "%1")
      end

      local tilde_infostring = Cs(Cs((V("HtmlEntity")
                                     + parsers.anyescaped
                                     - parsers.newline)^0)
                                 / strip_enclosing_whitespaces)

      local backtick_infostring = Cs(Cs((V("HtmlEntity")
                                        + (-#(parsers.backslash * parsers.backtick) * parsers.anyescaped)
                                         - parsers.newline
                                         - parsers.backtick)^0)
                                    / strip_enclosing_whitespaces)

      local fenceindent

      local function has_trail(indent_table)
        return indent_table ~= nil and
          indent_table.trail ~= nil and
          next(indent_table.trail) ~= nil
      end

      local function has_indents(indent_table)
        return indent_table ~= nil and
          indent_table.indents ~= nil and
          next(indent_table.indents) ~= nil
      end

      local function get_last_indent_name(indent_table)
        if has_indents(indent_table) then
          return indent_table.indents[#indent_table.indents].name
        end
      end

      local function count_fenced_start_indent(_, _, indent_table, trail)
        local last_indent_name = get_last_indent_name(indent_table)
        fenceindent = 0
        if last_indent_name ~= "li" then
          fenceindent = #trail
        end
        return true
      end

      local fencehead      = function(char, infostring)
        return               Cmt(Cb("indent_info") * parsers.check_trail, count_fenced_start_indent)
                           * Cg(char^3, "fencelength")
                           * parsers.optionalspace
                           * infostring
                           * (parsers.newline + parsers.eof)
      end

      local fencetail      = function(char)
        return               parsers.check_trail_no_rem
                           * Cmt(C(char^3) * Cb("fencelength"), captures_geq_length)
                           * parsers.optionalspace * (parsers.newline + parsers.eof)
                           + parsers.eof
      end

      local function process_fenced_line(s, i, indent_table, line_content, is_blank) -- luacheck: ignore s i
        local remainder = ""
        if has_trail(indent_table) then
          remainder = indent_table.trail.internal_remainder
        end

        if is_blank and get_last_indent_name(indent_table) == "li" then
          remainder = ""
        end

        local str = remainder .. line_content
        local index = 1
        local remaining = fenceindent

        while true do
          local c = str:sub(index, index)
          if c == " " and remaining > 0 then
            remaining = remaining - 1
            index = index + 1
          elseif c == "\t" and remaining > 3 then
            remaining = remaining - 4
            index = index + 1
          else
            break
          end
        end

        return true, str:sub(index)
      end

      local fencedline = function(char)
        return Cmt(Cb("indent_info") * C(parsers.line - fencetail(char)) * Cc(false), process_fenced_line)
      end

      local blankfencedline = Cmt(Cb("indent_info") * C(parsers.blankline) * Cc(true), process_fenced_line)

      local TildeFencedCode
             = fencehead(parsers.tilde, tilde_infostring)
             * Cs(((parsers.check_minimal_blank_indent / "") * blankfencedline
                   + (parsers.check_minimal_indent / "") * fencedline(parsers.tilde))^0)
             * ((parsers.check_minimal_indent / "") * fencetail(parsers.tilde) + parsers.succeed)

      local BacktickFencedCode
             = fencehead(parsers.backtick, backtick_infostring)
             * Cs(((parsers.check_minimal_blank_indent / "") * blankfencedline
                   + (parsers.check_minimal_indent / "") * fencedline(parsers.backtick))^0)
             * ((parsers.check_minimal_indent / "") * fencetail(parsers.backtick) + parsers.succeed)

      local infostring_with_attributes
                        = Ct(C((parsers.linechar
                              - ( parsers.optionalspace
                                * parsers.attributes))^0)
                            * parsers.optionalspace
                            * Ct(parsers.attributes))

      local FencedCode
               = ((TildeFencedCode + BacktickFencedCode)
               / function(infostring, code)
                   local expanded_code = self.expandtabs(code)

                   if allow_raw_blocks then
                     local raw_attr = lpeg.match(parsers.raw_attribute,
                                                 infostring)
                     if raw_attr then
                       return writer.rawBlock(expanded_code, raw_attr)
                     end
                   end

                   local attr = nil
                   if allow_attributes then
                     local match = lpeg.match(infostring_with_attributes,
                                              infostring)
                     if match then
                       infostring, attr = table.unpack(match)
                     end
                   end
                   return writer.fencedCode(expanded_code, infostring, attr)
                 end)

      self.insert_pattern("Block after Verbatim",
                          FencedCode, "FencedCode")

      local fencestart
      if blank_before_code_fence then
        fencestart = parsers.fail
      else
        fencestart = fencehead(parsers.backtick, backtick_infostring)
                   + fencehead(parsers.tilde, tilde_infostring)
      end

      self.update_rule("EndlineExceptions", function(previous_pattern)
        if previous_pattern == nil then
          previous_pattern = parsers.EndlineExceptions
        end
        return previous_pattern + fencestart
      end)

      self.add_special_character("`")
      self.add_special_character("~")
    end
  }
end
M.extensions.fenced_divs = function(blank_before_div_fence)
  return {
    name = "built-in fenced_divs syntax extension",
    extend_writer = function(self)
      function self.div_begin(attributes)
        local start_output = {"\\markdownRendererFencedDivAttributeContextBegin\n",
                              self.attributes(attributes)}
        local end_output = {"\\markdownRendererFencedDivAttributeContextEnd{}"}
        return self.push_attributes("div", attributes, start_output, end_output)
      end
      function self.div_end()
        return self.pop_attributes("div")
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer
      local fenced_div_infostring
                             = C((parsers.linechar
                                - ( parsers.spacechar^1
                                  * parsers.colon^1))^1)

      local fenced_div_begin = parsers.nonindentspace
                             * parsers.colon^3
                             * parsers.optionalspace
                             * fenced_div_infostring
                             * ( parsers.spacechar^1
                               * parsers.colon^1)^0
                             * parsers.optionalspace
                             * (parsers.newline + parsers.eof)

      local fenced_div_end = parsers.nonindentspace
                           * parsers.colon^3
                           * parsers.optionalspace
                           * (parsers.newline + parsers.eof)
      self.initialize_named_group("fenced_div_level", "0")
      self.initialize_named_group("fenced_div_num_opening_indents")

      local function increment_div_level()
        local function push_indent_table(s, i, indent_table, -- luacheck: ignore s i
                                         fenced_div_num_opening_indents, fenced_div_level)
          fenced_div_level = tonumber(fenced_div_level) + 1
          local num_opening_indents = 0
          if indent_table.indents ~= nil then
            num_opening_indents = #indent_table.indents
          end
          fenced_div_num_opening_indents[fenced_div_level] = num_opening_indents
          return true, fenced_div_num_opening_indents
        end

        local function increment_level(s, i, fenced_div_level) -- luacheck: ignore s i
          fenced_div_level = tonumber(fenced_div_level) + 1
          return true, tostring(fenced_div_level)
        end

        return Cg( Cmt( Cb("indent_info")
                      * Cb("fenced_div_num_opening_indents")
                      * Cb("fenced_div_level"), push_indent_table)
                 , "fenced_div_num_opening_indents")
             * Cg( Cmt( Cb("fenced_div_level"), increment_level)
                 , "fenced_div_level")
      end

      local function decrement_div_level()
        local function pop_indent_table(s, i, fenced_div_indent_table, fenced_div_level) -- luacheck: ignore s i
          fenced_div_level = tonumber(fenced_div_level)
          fenced_div_indent_table[fenced_div_level] = nil
          return true, tostring(fenced_div_level - 1)
        end

        return Cg( Cmt( Cb("fenced_div_num_opening_indents")
                      * Cb("fenced_div_level"), pop_indent_table)
                 , "fenced_div_level")
      end

      local non_fenced_div_block  = parsers.check_minimal_indent * V("Block")
                                  - parsers.check_minimal_indent_and_trail * fenced_div_end

      local non_fenced_div_paragraph  = parsers.check_minimal_indent * V("Paragraph")
                                      - parsers.check_minimal_indent_and_trail * fenced_div_end

      local blank = parsers.minimally_indented_blank

      local block_separated  = parsers.block_sep_group(blank)
                             * non_fenced_div_block

      local loop_body_pair  = parsers.create_loop_body_pair(block_separated,
                                                            non_fenced_div_paragraph,
                                                            parsers.block_sep_group(blank),
                                                            parsers.par_sep_group(blank))

      local content_loop  = ( non_fenced_div_block
                            * loop_body_pair.block^0
                            + non_fenced_div_paragraph
                            * block_separated
                            * loop_body_pair.block^0
                            + non_fenced_div_paragraph
                            * loop_body_pair.par^0)
                          * blank^0

      local FencedDiv = fenced_div_begin
                      / function (infostring)
                          local attr = lpeg.match(Ct(parsers.attributes), infostring)
                          if attr == nil then
                            attr = {"." .. infostring}
                          end
                          return attr
                        end
                      / writer.div_begin
                      * increment_div_level()
                      * parsers.skipblanklines
                      * Ct(content_loop)
                      * parsers.minimally_indented_blank^0
                      * parsers.check_minimal_indent_and_trail * fenced_div_end
                      * decrement_div_level()
                      * (Cc("") / writer.div_end)

      self.insert_pattern("Block after Verbatim",
                          FencedDiv, "FencedDiv")

      self.add_special_character(":")

      local function is_inside_div()
        local function check_div_level(s, i, fenced_div_level) -- luacheck: ignore s i
          fenced_div_level = tonumber(fenced_div_level)
          return fenced_div_level > 0
        end

        return Cmt(Cb("fenced_div_level"), check_div_level)
      end

      local function check_indent()
        local function compare_indent(s, i, indent_table, -- luacheck: ignore s i
                                      fenced_div_num_opening_indents, fenced_div_level)
          fenced_div_level = tonumber(fenced_div_level)
          local num_current_indents = (indent_table.current_line_indents ~= nil and
                                      #indent_table.current_line_indents) or 0
          local num_opening_indents = fenced_div_num_opening_indents[fenced_div_level]
          return num_current_indents == num_opening_indents
        end

        return Cmt( Cb("indent_info")
                  * Cb("fenced_div_num_opening_indents")
                  * Cb("fenced_div_level"), compare_indent)
      end

      local fencestart = is_inside_div()
                       * fenced_div_end
                       * check_indent()

      if not blank_before_div_fence then
        self.update_rule("EndlineExceptions", function(previous_pattern)
          if previous_pattern == nil then
            previous_pattern = parsers.EndlineExceptions
          end
          return previous_pattern + fencestart
        end)
      end
    end
  }
end
M.extensions.header_attributes = function()
  return {
    name = "built-in header_attributes syntax extension",
    extend_writer = function()
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local function strip_atx_end(s)
        return s:gsub("%s+#*%s*$","")
      end

      local AtxHeading = Cg(parsers.heading_start, "level")
                       * parsers.optionalspace
                       * (C(((parsers.linechar
                              - (parsers.attributes
                                * parsers.optionalspace
                                * parsers.newline))
                             * (parsers.linechar
                               - parsers.lbrace)^0)^1)
                             / strip_atx_end
                             / parsers.parse_heading_text)
                       * Cg(Ct(parsers.newline
                              + (parsers.attributes
                                * parsers.optionalspace
                                * parsers.newline)), "attributes")
                       * Cb("level")
                       * Cb("attributes")
                       / writer.heading

      local function strip_trailing_spaces(s)
        return s:gsub("%s*$","")
      end

      local heading_line  = (parsers.linechar
                            - (parsers.attributes
                              * parsers.optionalspace
                              * parsers.newline))^1
                          - parsers.thematic_break_lines

      local heading_text  = heading_line
                          * ((V("Endline") / "\n") * (heading_line - parsers.heading_level))^0
                          * parsers.newline^-1

      local SetextHeading  = parsers.freeze_trail * parsers.check_trail_no_rem
                           * #(heading_text
                              * (parsers.attributes
                                * parsers.optionalspace
                                * parsers.newline)^-1
                              * parsers.check_minimal_indent * parsers.check_trail * parsers.heading_level)
                           * Cs(heading_text) / strip_trailing_spaces
                           / parsers.parse_heading_text
                           * Cg(Ct((parsers.attributes
                                 * parsers.optionalspace
                                 * parsers.newline)^-1), "attributes")
                           * parsers.check_minimal_indent_and_trail * parsers.heading_level
                           * Cb("attributes")
                           * parsers.newline
                           * parsers.unfreeze_trail
                           / writer.heading

      local Heading = AtxHeading + SetextHeading
      self.update_rule("Heading", Heading)
    end
  }
end
M.extensions.inline_code_attributes = function()
  return {
    name = "built-in inline_code_attributes syntax extension",
    extend_writer = function()
    end, extend_reader = function(self)
      local writer = self.writer

      local CodeWithAttributes = parsers.inticks
                               * Ct(parsers.attributes)
                               / writer.code

      self.insert_pattern("Inline before Code",
                          CodeWithAttributes,
                          "CodeWithAttributes")
    end
  }
end
M.extensions.line_blocks = function()
  return {
    name = "built-in line_blocks syntax extension",
    extend_writer = function(self)
      function self.lineblock(lines)
        if not self.is_writing then return "" end
        local buffer = {}
        for i = 1, #lines - 1 do
          buffer[#buffer + 1] = { lines[i], self.hard_line_break }
        end
        buffer[#buffer + 1] = lines[#lines]

        return {"\\markdownRendererLineBlockBegin\n"
                  ,buffer,
                  "\n\\markdownRendererLineBlockEnd "}
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local LineBlock = Ct(
                        (Cs(
                          ( (parsers.pipe * parsers.space)/""
                          * ((parsers.space)/entities.char_entity("nbsp"))^0
                          * parsers.linechar^0 * (parsers.newline/""))
                          * (-parsers.pipe
                            * (parsers.space^1/" ")
                            * parsers.linechar^1
                            * (parsers.newline/"")
                            )^0
                          * (parsers.blankline/"")^0
                        ) / self.parser_functions.parse_inlines)^1) / writer.lineblock

      self.insert_pattern("Block after Blockquote",
                           LineBlock, "LineBlock")
    end
  }
end
M.extensions.mark = function()
  return {
    name = "built-in mark syntax extension",
    extend_writer = function(self)
      function self.mark(s)
        if self.flatten_inlines then return s end
        return {"\\markdownRendererMark{", s, "}"}
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local doubleequals = P("==")

      local Mark = parsers.between(V("Inline"), doubleequals, doubleequals)
                 / function (inlines) return writer.mark(inlines) end

      self.add_special_character("=")
      self.insert_pattern("Inline before LinkAndEmph",
                          Mark, "Mark")
    end
  }
end
M.extensions.link_attributes = function()
  return {
    name = "built-in link_attributes syntax extension",
    extend_writer = function()
    end, extend_reader = function(self)
      local parsers = self.parsers
      local options = self.options

      local define_reference_parser = (parsers.check_trail / "") * parsers.link_label * parsers.colon
                                    * parsers.spnlc * parsers.url
                                    * ( parsers.spnlc_sep * parsers.title * (parsers.spnlc * Ct(parsers.attributes))
                                      * parsers.only_blank
                                      + parsers.spnlc_sep * parsers.title * parsers.only_blank
                                      + Cc("") * (parsers.spnlc * Ct(parsers.attributes)) * parsers.only_blank
                                      + Cc("") * parsers.only_blank)

      local ReferenceWithAttributes = define_reference_parser
                                    / self.register_link

      self.update_rule("Reference", ReferenceWithAttributes)


      local LinkWithAttributesAndEmph = Ct(parsers.link_and_emph_table * Cg(Cc(true), "match_link_attributes"))
                                      / self.defer_link_and_emphasis_processing

      self.update_rule("LinkAndEmph", LinkWithAttributesAndEmph)

      local AutoLinkUrlWithAttributes
                      = parsers.auto_link_url
                      * Ct(parsers.attributes)
                      / self.auto_link_url

      self.insert_pattern("Inline before AutoLinkUrl",
                          AutoLinkUrlWithAttributes,
                          "AutoLinkUrlWithAttributes")

      local AutoLinkEmailWithAttributes
                      = parsers.auto_link_email
                      * Ct(parsers.attributes)
                      / self.auto_link_email

      self.insert_pattern("Inline before AutoLinkEmail",
                          AutoLinkEmailWithAttributes,
                          "AutoLinkEmailWithAttributes")

      if options.relativeReferences then

        local AutoLinkRelativeReferenceWithAttributes
                        = parsers.auto_link_relative_reference
                        * Ct(parsers.attributes)
                        / self.auto_link_url

        self.insert_pattern(
          "Inline before AutoLinkRelativeReference",
          AutoLinkRelativeReferenceWithAttributes,
          "AutoLinkRelativeReferenceWithAttributes")

      end

    end
  }
end
M.extensions.notes = function(notes, inline_notes)
  assert(notes or inline_notes)
  return {
    name = "built-in notes syntax extension",
    extend_writer = function(self)
      function self.note(s)
        if self.flatten_inlines then return "" end
        return {"\\markdownRendererNote{",s,"}"}
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local rawnotes = parsers.rawnotes

      if inline_notes then
        local InlineNote
                    = parsers.circumflex
                    * (parsers.link_label / self.parser_functions.parse_inlines_no_inline_note)
                    / writer.note

        self.insert_pattern("Inline after LinkAndEmph",
                            InlineNote, "InlineNote")
      end
      if notes then
        local function strip_first_char(s)
          return s:sub(2)
        end

        local RawNoteRef
                      = #(parsers.lbracket * parsers.circumflex)
                      * parsers.link_label / strip_first_char

        -- 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)
          local normalized_tag = self.normalize_tag(ref)
          if rawnotes[normalized_tag] == nil then
            rawnotes[normalized_tag] = rawnote
          end
          return ""
        end

        local NoteRef = RawNoteRef / lookup_note

        local optionally_indented_line = parsers.check_optional_indent_and_any_trail * parsers.line

        local blank = parsers.check_optional_blank_indent_and_any_trail * parsers.optionalspace * parsers.newline

        local chunk = Cs(parsers.line * (optionally_indented_line - blank)^0)

        local indented_blocks = function(bl)
          return Cs( bl
                * (blank^1 * (parsers.check_optional_indent / "")
                  * parsers.check_code_trail * -parsers.blankline * bl)^0)
        end

        local NoteBlock
                    = parsers.check_trail_no_rem * RawNoteRef * parsers.colon
                    * parsers.spnlc * indented_blocks(chunk)
                    / register_note

        local Reference = NoteBlock + parsers.Reference

        self.update_rule("Reference", Reference)
        self.insert_pattern("Inline before LinkAndEmph",
                            NoteRef, "NoteRef")
      end

      self.add_special_character("^")
    end
  }
end
M.extensions.pipe_tables = function(table_captions, table_attributes)

  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 {
    name = "built-in pipe_tables syntax extension",
    extend_writer = function(self)
      function self.table(rows, caption, attributes)
        if not self.is_writing then return "" end
        local buffer = {}
        if attributes ~= nil then
          table.insert(buffer,
                       "\\markdownRendererTableAttributeContextBegin\n")
          table.insert(buffer, self.attributes(attributes))
        end
        table.insert(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
        if attributes ~= nil then
          table.insert(buffer,
                       "\\markdownRendererTableAttributeContextEnd{}")
        end
        return buffer
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      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.check_minimal_blank_indent_and_any_trail_no_rem
                                      * parsers.optionalspace * parsers.newline)^0
                                    * parsers.check_minimal_indent_and_trail
                                    * (P("Table")^-1 * parsers.colon)
                                    * parsers.optionalspace

      local function strip_trailing_spaces(s)
        return s:gsub("%s*$","")
      end

      local table_row = pipe_table_row(true
                                      , (C((parsers.linechar - parsers.pipe)^1)
                                        / strip_trailing_spaces
                                        / self.parser_functions.parse_inlines)
                                      , parsers.pipe
                                      , (C((parsers.linechar - parsers.pipe)^0)
                                        / strip_trailing_spaces
                                        / self.parser_functions.parse_inlines))

      local table_caption
      if table_captions then
        table_caption = #table_caption_beginning
                      * table_caption_beginning
        if table_attributes then
          table_caption = table_caption
                        * (C(((( parsers.linechar
                               - (parsers.attributes
                                 * parsers.optionalspace
                                 * parsers.newline
                                 * -#( parsers.optionalspace
                                     * parsers.linechar)))
                              + ( parsers.newline
                                * #( parsers.optionalspace
                                   * parsers.linechar)
                                * C(parsers.optionalspace) / writer.space))
                             * (parsers.linechar
                               - parsers.lbrace)^0)^1)
                             / self.parser_functions.parse_inlines)
                        * (parsers.newline
                          + ( Ct(parsers.attributes)
                            * parsers.optionalspace
                            * parsers.newline))
        else
          table_caption = table_caption
                        * C(( parsers.linechar
                            + ( parsers.newline
                              * #( parsers.optionalspace
                                 * parsers.linechar)
                              * C(parsers.optionalspace) / writer.space))^1)
                        / self.parser_functions.parse_inlines
                        * parsers.newline
        end
      else
        table_caption = parsers.fail
      end

      local PipeTable = Ct(table_row * parsers.newline * (parsers.check_minimal_indent_and_trail / {})
                        * table_hline * parsers.newline
                        * ((parsers.check_minimal_indent / {}) * table_row * parsers.newline)^0)
                      / make_pipe_table_rectangular
                      * table_caption^-1
                      / writer.table

      self.insert_pattern("Block after Blockquote",
                          PipeTable, "PipeTable")
    end
  }
end
M.extensions.raw_inline = function()
  return {
    name = "built-in raw_inline syntax extension",
    extend_writer = function(self)
      local options = self.options

      function self.rawInline(s, attr)
        if not self.is_writing then return "" end
        if self.flatten_inlines then return s end
        local name = util.cache_verbatim(options.cacheDir, s)
        return {"\\markdownRendererInputRawInline{",
                name,"}{", self.string(attr),"}"}
      end
    end, extend_reader = function(self)
      local writer = self.writer

      local RawInline = parsers.inticks
                      * parsers.raw_attribute
                      / writer.rawInline

      self.insert_pattern("Inline before Code",
                          RawInline, "RawInline")
    end
  }
end
M.extensions.strike_through = function()
  return {
    name = "built-in strike_through syntax extension",
    extend_writer = function(self)
      function self.strike_through(s)
        if self.flatten_inlines then return s end
        return {"\\markdownRendererStrikeThrough{",s,"}"}
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local StrikeThrough = (
        parsers.between(parsers.Inline, parsers.doubletildes,
                        parsers.doubletildes)
      ) / writer.strike_through

      self.insert_pattern("Inline after LinkAndEmph",
                          StrikeThrough, "StrikeThrough")

      self.add_special_character("~")
    end
  }
end
M.extensions.subscripts = function()
  return {
    name = "built-in subscripts syntax extension",
    extend_writer = function(self)
      function self.subscript(s)
        if self.flatten_inlines then return s end
        return {"\\markdownRendererSubscript{",s,"}"}
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local Subscript = (
        parsers.between(parsers.Str, parsers.tilde, parsers.tilde)
      ) / writer.subscript

      self.insert_pattern("Inline after LinkAndEmph",
                          Subscript, "Subscript")

      self.add_special_character("~")
    end
  }
end
M.extensions.superscripts = function()
  return {
    name = "built-in superscripts syntax extension",
    extend_writer = function(self)
      function self.superscript(s)
        if self.flatten_inlines then return s end
        return {"\\markdownRendererSuperscript{",s,"}"}
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local Superscript = (
        parsers.between(parsers.Str, parsers.circumflex, parsers.circumflex)
      ) / writer.superscript

      self.insert_pattern("Inline after LinkAndEmph",
                          Superscript, "Superscript")

      self.add_special_character("^")
    end
  }
end
M.extensions.tex_math = function(tex_math_dollars,
                                 tex_math_single_backslash,
                                 tex_math_double_backslash)
  return {
    name = "built-in tex_math syntax extension",
    extend_writer = function(self)
      function self.display_math(s)
        if self.flatten_inlines then return s end
        return {"\\markdownRendererDisplayMath{",self.math(s),"}"}
      end
      function self.inline_math(s)
        if self.flatten_inlines then return s end
        return {"\\markdownRendererInlineMath{",self.math(s),"}"}
      end
    end, extend_reader = function(self)
      local parsers = self.parsers
      local writer = self.writer

      local function between(p, starter, ender)
        return (starter * Cs(p * (p - ender)^0) * ender)
      end

      local function strip_preceding_whitespaces(str)
        return str:gsub("^%s*(.-)$", "%1")
      end

      local allowed_before_closing = B( parsers.backslash * parsers.any
                                      + parsers.any * (parsers.any - parsers.backslash))

      local allowed_before_closing_no_space = B( parsers.backslash * parsers.any
                                               + parsers.any * (parsers.nonspacechar - parsers.backslash))

      local dollar_math_content = (parsers.newline * (parsers.check_optional_indent / "")
                                + parsers.backslash^-1
                                * parsers.linechar)
                                - parsers.blankline^2
                                - parsers.dollar

      local inline_math_opening_dollars = parsers.dollar
                                        * #(parsers.nonspacechar)

      local inline_math_closing_dollars = allowed_before_closing_no_space
                                        * parsers.dollar
                                        * -#(parsers.digit)

      local inline_math_dollars = between(Cs( dollar_math_content),
                                          inline_math_opening_dollars,
                                          inline_math_closing_dollars)

      local display_math_opening_dollars  = parsers.dollar
                                          * parsers.dollar

      local display_math_closing_dollars  = parsers.dollar
                                          * parsers.dollar

      local display_math_dollars = between(Cs( dollar_math_content),
                                           display_math_opening_dollars,
                                           display_math_closing_dollars)
      local backslash_math_content  = (parsers.newline * (parsers.check_optional_indent / "")
                                    + parsers.linechar)
                                    - parsers.blankline^2
      local inline_math_opening_double  = parsers.backslash
                                        * parsers.backslash
                                        * parsers.lparent

      local inline_math_closing_double  = allowed_before_closing
                                        * parsers.spacechar^0
                                        * parsers.backslash
                                        * parsers.backslash
                                        * parsers.rparent

      local inline_math_double  = between(Cs( backslash_math_content),
                                          inline_math_opening_double,
                                          inline_math_closing_double)
                                / strip_preceding_whitespaces

      local display_math_opening_double = parsers.backslash
                                        * parsers.backslash
                                        * parsers.lbracket

      local display_math_closing_double = allowed_before_closing
                                        * parsers.spacechar^0
                                        * parsers.backslash
                                        * parsers.backslash
                                        * parsers.rbracket

      local display_math_double = between(Cs( backslash_math_content),
                                          display_math_opening_double,
                                          display_math_closing_double)
                                / strip_preceding_whitespaces
      local inline_math_opening_single  = parsers.backslash
                                        * parsers.lparent

      local inline_math_closing_single  = allowed_before_closing
                                        * parsers.spacechar^0
                                        * parsers.backslash
                                        * parsers.rparent

      local inline_math_single  = between(Cs( backslash_math_content),
                                          inline_math_opening_single,
                                          inline_math_closing_single)
                                / strip_preceding_whitespaces

      local display_math_opening_single = parsers.backslash
                                        * parsers.lbracket

      local display_math_closing_single = allowed_before_closing
                                        * parsers.spacechar^0
                                        * parsers.backslash
                                        * parsers.rbracket

      local display_math_single = between(Cs( backslash_math_content),
                                          display_math_opening_single,
                                          display_math_closing_single)
                                / strip_preceding_whitespaces

      local display_math = parsers.fail

      local inline_math = parsers.fail

      if tex_math_dollars then
        display_math = display_math + display_math_dollars
        inline_math = inline_math + inline_math_dollars
      end

      if tex_math_double_backslash then
        display_math = display_math + display_math_double
        inline_math = inline_math + inline_math_double
      end

      if tex_math_single_backslash then
        display_math = display_math + display_math_single
        inline_math = inline_math + inline_math_single
      end

      local TexMath = display_math / writer.display_math
                    + inline_math / writer.inline_math

      self.insert_pattern("Inline after LinkAndEmph",
                          TexMath, "TexMath")

      if tex_math_dollars then
        self.add_special_character("$")
      end

      if tex_math_single_backslash or tex_math_double_backslash then
        self.add_special_character("\\")
        self.add_special_character("[")
        self.add_special_character("]")
        self.add_special_character(")")
        self.add_special_character("(")
      end
    end
  }
end
M.extensions.jekyll_data = function(expect_jekyll_data)
  return {
    name = "built-in jekyll_data syntax extension",
    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, function(first, second)
          if type(first) ~= type(second) then
            return type(first) < type(second)
          else
            return first < second
          end
        end)

        if not p then
          table.insert(buf, "\\markdownRendererJekyllDataBegin")
        end

        local is_sequence = false
        if #d > 0 and #d == #keys then
          for i=1, #d do
            if d[i] == nil then
              goto not_a_sequence
            end
          end
          is_sequence = true
        end
        ::not_a_sequence::

        if is_sequence then
            table.insert(buf, "\\markdownRendererJekyllDataSequenceBegin{")
            table.insert(buf, self.identifier(p or "null"))
            table.insert(buf, "}{")
            table.insert(buf, #keys)
            table.insert(buf, "}")
        else
            table.insert(buf, "\\markdownRendererJekyllDataMappingBegin{")
            table.insert(buf, self.identifier(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.identifier(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 is_sequence 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 writer = self.writer

      local JekyllData
                    = Cmt( C((parsers.line - P("---") - P("..."))^0)
                         , function(s, i, text) -- luacheck: ignore s i
                             local data
                             local ran_ok, _ = pcall(function()
                               -- TODO: Replace with `require("tinyyaml")` in TeX Live 2023
                               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 thematic break
                    * JekyllData
                    * (P("---") + P("..."))

      local ExpectedJekyllData
                    = ( P("---")
                      * parsers.blankline / 0
                      * #(-parsers.blankline)  -- if followed by blank, it's thematic break
                      )^-1
                    * JekyllData
                    * (P("---") + P("..."))^-1

      self.insert_pattern("Block before Blockquote",
                          UnexpectedJekyllData, "UnexpectedJekyllData")
      if expect_jekyll_data then
        self.update_rule("ExpectedJekyllData", ExpectedJekyllData)
      end
    end
  }
end
function M.new(options)
  options = options or {}
  setmetatable(options, { __index = function (_, key)
    return defaultOptions[key] end })
  if options.singletonCache and singletonCache.convert then
    for k, v in pairs(defaultOptions) do
      if type(v) == "table" then
        for i = 1, math.max(#singletonCache.options[k], #options[k]) do
          if singletonCache.options[k][i] ~= options[k][i] then
            goto miss
          end
        end
      elseif singletonCache.options[k] ~= options[k] then
        goto miss
      end
    end
    return singletonCache.convert
  end
  ::miss::
  local extensions = {}

  if options.bracketedSpans then
    local bracketed_spans_extension = M.extensions.bracketed_spans()
    table.insert(extensions, bracketed_spans_extension)
  end

  if options.contentBlocks then
    local content_blocks_extension = M.extensions.content_blocks(
      options.contentBlocksLanguageMap)
    table.insert(extensions, content_blocks_extension)
  end

  if options.definitionLists then
    local definition_lists_extension = M.extensions.definition_lists(
      options.tightLists)
    table.insert(extensions, definition_lists_extension)
  end

  if options.fencedCode then
    local fenced_code_extension = M.extensions.fenced_code(
      options.blankBeforeCodeFence,
      options.fencedCodeAttributes,
      options.rawAttribute)
    table.insert(extensions, fenced_code_extension)
  end

  if options.fencedDivs then
    local fenced_div_extension = M.extensions.fenced_divs(
      options.blankBeforeDivFence)
    table.insert(extensions, fenced_div_extension)
  end

  if options.headerAttributes then
    local header_attributes_extension = M.extensions.header_attributes()
    table.insert(extensions, header_attributes_extension)
  end

  if options.inlineCodeAttributes then
    local inline_code_attributes_extension =
      M.extensions.inline_code_attributes()
    table.insert(extensions, inline_code_attributes_extension)
  end

  if options.jekyllData then
    local jekyll_data_extension = M.extensions.jekyll_data(
      options.expectJekyllData)
    table.insert(extensions, jekyll_data_extension)
  end

  if options.linkAttributes then
    local link_attributes_extension =
      M.extensions.link_attributes()
    table.insert(extensions, link_attributes_extension)
  end

  if options.lineBlocks then
    local line_block_extension = M.extensions.line_blocks()
    table.insert(extensions, line_block_extension)
  end

  if options.mark then
    local mark_extension = M.extensions.mark()
    table.insert(extensions, mark_extension)
  end

  if options.pipeTables then
    local pipe_tables_extension = M.extensions.pipe_tables(
      options.tableCaptions, options.tableAttributes)
    table.insert(extensions, pipe_tables_extension)
  end

  if options.rawAttribute then
    local raw_inline_extension = M.extensions.raw_inline()
    table.insert(extensions, raw_inline_extension)
  end

  if options.strikeThrough then
    local strike_through_extension = M.extensions.strike_through()
    table.insert(extensions, strike_through_extension)
  end

  if options.subscripts then
    local subscript_extension = M.extensions.subscripts()
    table.insert(extensions, subscript_extension)
  end

  if options.superscripts then
    local superscript_extension = M.extensions.superscripts()
    table.insert(extensions, superscript_extension)
  end

  if options.texMathDollars or
     options.texMathSingleBackslash or
     options.texMathDoubleBackslash then
    local tex_math_extension = M.extensions.tex_math(
      options.texMathDollars,
      options.texMathSingleBackslash,
      options.texMathDoubleBackslash)
    table.insert(extensions, tex_math_extension)
  end

  if options.notes or options.inlineNotes then
    local notes_extension = M.extensions.notes(
      options.notes, options.inlineNotes)
    table.insert(extensions, notes_extension)
  end

  if options.citations then
    local citations_extension = M.extensions.citations(options.citationNbsps)
    table.insert(extensions, citations_extension)
  end

  if options.fancyLists then
    local fancy_lists_extension = M.extensions.fancy_lists()
    table.insert(extensions, fancy_lists_extension)
  end
  for _, user_extension_filename in ipairs(options.extensions) do
    local user_extension = (function(filename)
      local pathname = kpse.lookup(filename)
      local input_file = assert(io.open(pathname, "r"),
        [[Could not open user-defined syntax extension "]]
        .. pathname .. [[" for reading]])
      local input = assert(input_file:read("*a"))
      assert(input_file:close())
      local user_extension, err = load([[
        local sandbox = {}
        setmetatable(sandbox, {__index = _G})
        _ENV = sandbox
      ]] .. input)()
      assert(user_extension,
        [[Failed to compile user-defined syntax extension "]]
        .. pathname .. [[": ]] .. (err or [[]]))
      assert(user_extension.api_version ~= nil,
        [[User-defined syntax extension "]] .. pathname
        .. [[" does not specify mandatory field "api_version"]])
      assert(type(user_extension.api_version) == "number",
        [[User-defined syntax extension "]] .. pathname
        .. [[" specifies field "api_version" of type "]]
        .. type(user_extension.api_version)
        .. [[" but "number" was expected]])
      assert(user_extension.api_version > 0
         and user_extension.api_version <= metadata.user_extension_api_version,
        [[User-defined syntax extension "]] .. pathname
        .. [[" uses syntax extension API version "]]
        .. user_extension.api_version .. [[ but markdown.lua ]]
        .. metadata.version .. [[ uses API version ]]
        .. metadata.user_extension_api_version
        .. [[, which is incompatible]])

      assert(user_extension.grammar_version ~= nil,
        [[User-defined syntax extension "]] .. pathname
        .. [[" does not specify mandatory field "grammar_version"]])
      assert(type(user_extension.grammar_version) == "number",
        [[User-defined syntax extension "]] .. pathname
        .. [[" specifies field "grammar_version" of type "]]
        .. type(user_extension.grammar_version)
        .. [[" but "number" was expected]])
      assert(user_extension.grammar_version == metadata.grammar_version,
        [[User-defined syntax extension "]] .. pathname
        .. [[" uses grammar version "]] .. user_extension.grammar_version
        .. [[ but markdown.lua ]] .. metadata.version
        .. [[ uses grammar version ]] .. metadata.grammar_version
        .. [[, which is incompatible]])

      assert(user_extension.finalize_grammar ~= nil,
        [[User-defined syntax extension "]] .. pathname
        .. [[" does not specify mandatory "finalize_grammar" field]])
      assert(type(user_extension.finalize_grammar) == "function",
        [[User-defined syntax extension "]] .. pathname
        .. [[" specifies field "finalize_grammar" of type "]]
        .. type(user_extension.finalize_grammar)
        .. [[" but "function" was expected]])
      local extension = {
        name = [[user-defined "]] .. pathname .. [[" syntax extension]],
        extend_reader = user_extension.finalize_grammar,
        extend_writer = function() end,
      }
      return extension
    end)(user_extension_filename)
    table.insert(extensions, user_extension)
  end
  local writer = M.writer.new(options)
  local reader = M.reader.new(writer, options)
  local convert = reader.finalize_grammar(extensions)
  collectgarbage("collect")
  if options.singletonCache then
    local singletonCacheOptions = {}
    for k, v in pairs(options) do
      singletonCacheOptions[k] = v
    end
    setmetatable(singletonCacheOptions,
      { __index = function (_, key)
        return defaultOptions[key] end })
    singletonCache.options = singletonCacheOptions
    singletonCache.convert = convert
  end
  return convert
end

return M
