summaryrefslogtreecommitdiff
path: root/macros/generic/markdown/markdown.dtx
diff options
context:
space:
mode:
Diffstat (limited to 'macros/generic/markdown/markdown.dtx')
-rw-r--r--macros/generic/markdown/markdown.dtx1280
1 files changed, 887 insertions, 393 deletions
diff --git a/macros/generic/markdown/markdown.dtx b/macros/generic/markdown/markdown.dtx
index 4db4ecfcc3..f419af48a2 100644
--- a/macros/generic/markdown/markdown.dtx
+++ b/macros/generic/markdown/markdown.dtx
@@ -680,13 +680,13 @@ abbr {
date = {2017-01-19},
url = {https://github.com/iainc/Markdown-Content-Blocks},
urldate = {2018-01-08}}
-@book{luatex17,
+@book{luatex21,
author = {{Lua\TeX{} development team}},
title = {Lua\TeX{} reference manual},
- year = {2017},
- month = {2},
- url = {http://www.luatex.org/svn/trunk/manual/luatex.pdf},
- urldate = {2018-01-08}}
+ date = {2021-07-23},
+ note = {Version 1.10 (stable)},
+ url = {https://www.pragma-ade.com/general/manuals/luatex.pdf},
+ urldate = {2022-09-30}}
@book{latex17,
author = {Braams, Johannes and Carlisle, David and Jeffrey, Alan and
Lamport, Leslie and Mittelbach, Frank and Rowley, Chris and
@@ -1080,7 +1080,7 @@ local md5 = require("md5")
% \begin{markdown}
%
% All the abovelisted modules are statically linked into the current version of
-% the Lua\TeX{} engine~[@luatex17, Section 3.3]. Beside these, we also carry
+% the Lua\TeX{} engine~[@luatex21, Section 4.3]. Beside these, we also carry
% the following third-party Lua libraries:
%
% \pkg{api7/lua-tinyyaml}
@@ -1137,10 +1137,10 @@ local md5 = require("md5")
%
% The plain \TeX{} code makes use of the \luamref{isdir} method that was added
% to the \pkg{Lua File System} library by the Lua\TeX{} engine
-% developers~[@luatex17, Section 3.2].
+% developers~[@luatex21, Section 4.2.4].
%
% The \pkg{Lua File System} module is statically linked into the Lua\TeX{}
-% engine~[@luatex17, Section~3.3].
+% engine~[@luatex21, Section 4.3].
%
% Unless you convert markdown documents to \TeX{} manually using the Lua
% command-line interface (see Section <#sec:lua-cli-interface>), the plain
@@ -1585,12 +1585,12 @@ local M = {metadata = metadata}
%
%### Conversion from Markdown to Plain \TeX{} {#luaconversion}
%
-% The Lua interface exposes the \luamdef{new}`(options)` method. This
-% method creates converter functions that perform the conversion from markdown
-% to plain \TeX{} according to the table `options` that contains options
-% recognized by the Lua interface. (see Section <#sec:luaoptions>). The
-% `options` parameter is optional; when unspecified, the behaviour will be
-% the same as if `options` were an empty table.
+% The Lua interface exposes the \luamdef{new}`(options)` function. This
+% function returns a conversion function from markdown to plain \TeX{} according
+% to the table `options` that contains options recognized by the Lua interface
+% (see Section <#sec:luaoptions>). The `options` parameter is optional; when
+% unspecified, the behaviour will be the same as if `options` were an empty
+% table.
%
% The following example Lua code converts the markdown string `Hello
% *world*!` to a \TeX{} output using the default options and prints the \TeX{}
@@ -1601,6 +1601,70 @@ local M = {metadata = metadata}
% print(convert("Hello *world*!"))
% ```````
%
+%### User-Defined Syntax Extensions {#luauserextensions}
+%
+% For the purpose of user-defined syntax extensions, the Lua interface also
+% exposes the \luamdef{reader} object, which performs the lexical and
+% syntactic analysis of markdown text and which exposes the
+% \luamdef{reader->insert_pattern} and \luamdef{reader->add_special_character}
+% methods for extending the \acro{peg} grammar of markdown.
+%
+% The read-only \luamdef{walkable_syntax} hash table stores those rules of the
+% \acro{peg} grammar of markdown that can be represented as an ordered choice
+% of terminal symbols. These rules can be modified by user-defined syntax
+% extensions.
+%
+% \end{markdown}
+% \begin{macrocode}
+local walkable_syntax = {
+ Block = {
+ "Blockquote",
+ "Verbatim",
+ "HorizontalRule",
+ "BulletList",
+ "OrderedList",
+ "Heading",
+ "DisplayHtml",
+ "Paragraph",
+ "Plain",
+ },
+ Inline = {
+ "Str",
+ "Space",
+ "Endline",
+ "UlOrStarLine",
+ "Strong",
+ "Emph",
+ "Link",
+ "Image",
+ "Code",
+ "AutoLinkUrl",
+ "AutoLinkEmail",
+ "AutoLinkRelativeReference",
+ "InlineHtml",
+ "HtmlEntity",
+ "EscapedChar",
+ "Smart",
+ "Symbol",
+ },
+}
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% The \luamref{reader->insert_pattern} method inserts a \acro{peg} pattern into
+% the grammar of markdown. The method receives two arguments: a selector string
+% in the form `"`\meta{left-hand side terminal symbol} \meta{`before`, `after`,
+% or `instead of`} \meta{right-hand side terminal symbol}`"` and a \acro{peg}
+% pattarn to insert. For example. if we'd like to insert `pattern` into the
+% grammar between the `Inline -> Emph` and `Inline -> Link` rules, we would
+% call \luamref{reader->insert_pattern} with `"Inline after Emph"` (or `"Inline
+% before Link"`) and `pattern` as the arguments.
+%
+% The \luamdef{reader->add_special_character} method adds a new character with
+% special meaning to the grammar of markdown. The method receives the character
+% as its only argument.
+%
% \end{markdown}
% \iffalse
%</lua>
@@ -1897,6 +1961,8 @@ interfaces and all the way up to the \LaTeX{} and \Hologo{ConTeXt} interfaces.
\msg_error:nnnn
{ nnnV }
\seq_new:N \g_@@_option_types_seq
+\tl_const:Nn \c_@@_option_type_clist_tl { clist }
+\seq_put_right:NV \g_@@_option_types_seq \c_@@_option_type_clist_tl
\tl_const:Nn \c_@@_option_type_counter_tl { counter }
\seq_put_right:NV \g_@@_option_types_seq \c_@@_option_type_counter_tl
\tl_const:Nn \c_@@_option_type_boolean_tl { boolean }
@@ -4365,6 +4431,153 @@ defaultOptions.eagerCache = true
%</lua,lua-cli>
%<*manual-options>
+#### Option `extensions`
+
+`extensions` (default value: `{}`)
+
+% \fi
+% \begin{markdown}
+%
+% \Valitem[\{\}]{extensions}{filenames}
+%
+: The filenames of user-defined syntax extensions that will be applied to the
+ markdown reader. If the \pkg{kpathsea} library is available, files will be
+ searched for not only in the current working directory but also in the
+ \TeX{} directory structure.
+
+% A user-defined syntax extension is a Lua file in the following format:
+%
+% ``` lua
+% local strike_through = {
+% api_version = 1,
+% grammar_version = 1,
+% finalize_grammar = function(reader)
+% local nonspacechar = lpeg.P(1) - lpeg.S("\t ")
+% local doubleslashes = lpeg.P("//")
+% local function between(p, starter, ender)
+% ender = lpeg.B(nonspacechar) * ender
+% return (starter * #nonspacechar
+% * lpeg.Ct(p * (p - ender)^0) * ender)
+% end
+%
+% local read_strike_through = between(
+% lpeg.V("Inline"), doubletildes, doubletildes
+% ) / function(s) return {"\\st{", s, "}"} end
+%
+% reader.insert_pattern("Inline after Emph", read_strike_through)
+% reader.add_special_character("/")
+% end
+% }
+%
+% return strike_through
+% ```````
+%
+% The `api_version` and `grammar_version` fields specify the version of the
+% user-defined syntax extension API and the markdown grammar for which
+% the extension was written. See the current API and grammar versions
+% below:
+%
+% \end{markdown}
+% \iffalse
+%</manual-options>
+%<*lua>
+% \fi
+% \begin{macrocode}
+metadata.user_extension_api_version = 1
+metadata.grammar_version = 1
+% \end{macrocode}
+% \iffalse
+%</lua>
+%<*manual-options>
+% \fi
+% \begin{markdown}
+%
+% Any changes to the syntax extension API or grammar will cause the
+% corresponding current version to be incremented. After Markdown 3.0.0,
+% any changes to the API and the grammar will be either backwards-compatible
+% or constitute a breaking change that will cause the major version of the
+% Markdown package to increment (to 4.0.0).
+%
+% The `finalize_grammar` field is a function that finalizes the grammar of
+% markdown using the interface of a Lua \luamref{reader} object, such as
+% the \luamref{reader->insert_pattern} and
+% \luamref{reader->add_special_character} methods,
+% see Section <#luauserextensions>.
+%
+% \end{markdown}
+% \iffalse
+
+##### \LaTeX{} Example {.unnumbered}
+
+Using a text editor, create a text document named `strike-through.lua` with the
+following content:
+``` lua
+local strike_through = {
+ api_version = 1,
+ grammar_version = 1,
+ finalize_grammar = function(reader)
+ local nonspacechar = lpeg.P(1) - lpeg.S("\t ")
+ local doubleslashes = lpeg.P("//")
+ local function between(p, starter, ender)
+ ender = lpeg.B(nonspacechar) * ender
+ return (starter * #nonspacechar
+ * lpeg.Ct(p * (p - ender)^0) * ender)
+ end
+
+ local read_strike_through = between(
+ lpeg.V("Inline"), doubletildes, doubletildes
+ ) / function(s) return {"\\st{", s, "}"} end
+
+ reader.insert_pattern("Inline after Emph", read_strike_through)
+ reader.add_special_character("/")
+ end
+}
+```````
+Using a text editor, create also a text document named `document.tex` with the
+following content:
+``` tex
+\documentclass{article}
+\usepackage{soul}
+\usepackage[extension = strike-through.lua]{markdown}
+\begin{document}
+\begin{markdown}
+This is //a lunar roving vehicle// strike-through text.
+\end{markdown}
+\end{document}
+```````
+Next, invoke LuaTeX from the terminal:
+``` sh
+lualatex document.tex
+``````
+A PDF document named `document.pdf` should be produced and contain the
+following text:
+
+> This is ~~a lunar roving vehicle~~ strike-through text.
+
+%</manual-options>
+%<*tex>
+% \fi
+% \begin{macrocode}
+\cs_generate_variant:Nn
+ \@@_add_lua_option:nnn
+ { nnV }
+\@@_add_lua_option:nnV
+ { extensions }
+ { clist }
+ \c_empty_clist
+% \end{macrocode}
+% \iffalse
+%</tex>
+%<*lua,lua-cli>
+% \fi
+% \begin{macrocode}
+defaultOptions.extensions = {}
+% \end{macrocode}
+% \par
+% \iffalse
+%</lua,lua-cli>
+%<*manual-options>
+
#### Option `expectJekyllData`
`expectJekyllData` (default value: `false`)
@@ -8250,6 +8463,11 @@ for i = 1, #arg do
options[key] = (value == "true")
elseif default_type == "number" then
options[key] = tonumber(value)
+ elseif default_type == "table" then
+ options[key] = {}
+ for item in value:gmatch("[^ ,]+") do
+ table.insert(options[key], item)
+ end
else
if default_type ~= "string" then
if default_type == "nil" then
@@ -10461,11 +10679,11 @@ as \mref{markdownRendererContentBlock}.
The \mdef{markdownRendererContentBlockCode} macro represents an iA\,Writer
content block that was recognized as a file in a known programming language
by its filename extension $s$. If any `markdown-languages.json` file found
-by \pkg{kpathsea}\footnote{Local files take precedence. Filenames other
-than `markdown-languages.json` may be specified using the
-`contentBlocksLanguageMap` Lua option.} contains a record $(k, v)$, then a
-non-online-image content block with the filename extension $s,
-s$`:lower()`${}=k$ is considered to be in a known programming language $v$.
+by \pkg{kpathsea}\footnote{Filenames other than `markdown-languages.json` may
+be specified using the `contentBlocksLanguageMap` Lua option.} contains a
+record $(k, v)$, then a non-online-image content block with the filename
+extension $s, s$`:lower()`${}=k$ is considered to be in a known programming
+language $v$.
The macro receives five arguments: the local file name extension $s$ cast to
the lower case, the language $v$, the fully escaped \acro{uri} that can be
directly typeset, the raw \acro{uri} that can be used outside typesetting,
@@ -15615,8 +15833,8 @@ pdflatex --shell-escape document.tex
### \LaTeX{}
\LaTeX{} options allow us to disable the redefinition of the default renderer
-prototypes from plain \TeX{}, load user-contributed themes, and invoke
-user-defined setup snippets.
+prototypes from plain \TeX{}, load user-defined themes, and invoke user-defined
+setup snippets.
#### Setting Lua and plain \TeX{} options from \LaTeX{}
@@ -15720,20 +15938,19 @@ package. Setting the option after loading the package will have no effect.
#### \LaTeX{} themes {#latexthemes}
-User-contributed \LaTeX{} themes for the Markdown package provide a
-domain-specific interpretation of some Markdown tokens. Similarly to \LaTeX{}
-packages, themes allow the authors to achieve a specific look and other
-high-level goals without low-level programming.
-
-% The \LaTeX{} option with key `theme` loads a \LaTeX{} package (further
-% referred to as *a theme*) named `markdowntheme`\meta{munged theme
-% name}`.sty`, where the *munged theme name* is the *theme name* after a
-% substitution of all forward slashes (\texttt{/}) for an underscore
-% (\texttt{_}), the theme name is a value that is *qualified* and contains no
-% underscores, and a value is qualified if and only if it contains at least one
-% forward slash. Themes are inspired by the Beamer \LaTeX{} package, which
-% provides similar functionality with its \mref{usetheme} macro [@tantau21,
-% Section 15.1].
+User-defined \LaTeX{} themes for the Markdown package provide a domain-specific
+interpretation of Markdown tokens. Similarly to \LaTeX{} packages, themes
+allow the authors to achieve a specific look and other high-level goals
+without low-level programming.
+
+% The \LaTeX{} option `theme`=\meta{theme name} loads a \LaTeX{} package
+% (further referred to as *a theme*) named `markdowntheme`\meta{munged theme
+% name}`.sty`, where the *munged theme name* is the *theme name* after the
+% substitution of all forward slashes (`/`) for an underscore (`_`), the theme
+% *name* is *qualified* and contains no underscores, and a value is qualified
+% if and only if it contains at least one forward slash. Themes are inspired by
+% the Beamer \LaTeX{} package, which provides similar functionality with its
+% \mref{usetheme} macro [@tantau21, Section 15.1].
%
% Theme names must be qualified to minimize naming conflicts between different
% themes intended for a single \LaTeX{} document class or for a single \LaTeX{}
@@ -15790,13 +16007,21 @@ would use the following code in the preamble of your document:
\cs_new:Nn
\@@_set_latex_theme:n
{
- \str_if_in:NnF
+ \str_if_in:nnF
{ #1 }
{ / }
{
\markdownError
- { Won't load theme with unqualified name #1 }
- { Theme names must contain at least one forward slash }
+ { Won't~load~theme~with~unqualified~name~#1 }
+ { Theme~names~must~contain~at~least~one~forward~slash }
+ }
+ \str_if_in:nnT
+ { #1 }
+ { _ }
+ {
+ \markdownError
+ { Won't~load~theme~with~an~underscore~in~its~name~#1 }
+ { Theme~names~must~not~contain~underscores~in~their~names }
}
\tl_set:Nn \markdownLaTeXThemePackageName { #1 }
\str_replace_all:Nnn
@@ -16121,7 +16346,7 @@ following text, where the middot (`ยท`) denotes a non-breaking space:
% \end{markdown}
% \iffalse
-User-contributed \LaTeX{} themes provide global control over high-level goals.
+User-defined \LaTeX{} themes provide global control over high-level goals.
However, it is often desirable to change only some local aspects of a document.
\LaTeX{} setup snippets provide syntactic sugar for defining and invoking
various options locally.
@@ -16257,6 +16482,85 @@ The following ordered list will be preceded by roman numerals:
#2 .default:n = { true },
}
}
+% \end{macrocode}
+% \begin{markdown}
+%
+% For options of type `clist`, we assume that \meta{key} is a regular English
+% noun in plural (such as `extensions`) and we also define the
+% \meta{singular key}`=`\meta{value} interface, where \meta{singular key} is
+% \meta{key} after stripping the trailing -s (such as `extension`). Rather
+% than setting the option to \meta{value}, this interface appends \meta{value}
+% to the current value as the rightmost item in the list.
+%
+% \end{markdown}
+% \begin{macrocode}
+ \str_if_eq:VVT
+ \l_tmpa_tl
+ \c_@@_option_type_clist_tl
+ {
+ \tl_set:Nn
+ \l_tmpa_tl
+ { #2 }
+ \tl_reverse:N
+ \l_tmpa_tl
+ \str_if_eq:enF
+ {
+ \tl_head:V
+ \l_tmpa_tl
+ }
+ { s }
+ {
+ \msg_error:nnn
+ { @@ }
+ { malformed-name-for-clist-option }
+ { #2 }
+ }
+ \tl_set:Nx
+ \l_tmpa_tl
+ {
+ \tl_tail:V
+ \l_tmpa_tl
+ }
+ \tl_reverse:N
+ \l_tmpa_tl
+ \tl_put_right:Nn
+ \l_tmpa_tl
+ {
+ .code:n = {
+ \@@_get_option_value:nN
+ { #2 }
+ \l_tmpa_tl
+ \clist_set:NV
+ \l_tmpa_clist
+ { \l_tmpa_tl, { ##1 } }
+ \@@_set_option_value:nV
+ { #2 }
+ \l_tmpa_clist
+ }
+ }
+ \keys_define:nV
+ { markdown/latex-options }
+ \l_tmpa_tl
+ }
+ }
+\cs_generate_variant:Nn
+ \clist_set:Nn
+ { NV }
+\cs_generate_variant:Nn
+ \keys_define:nn
+ { nV }
+\cs_generate_variant:Nn
+ \@@_set_option_value:nn
+ { nV }
+\prg_generate_conditional_variant:Nnn
+ \str_if_eq:nn
+ { en }
+ { F }
+\msg_new:nnn
+ { @@ }
+ { malformed-name-for-clist-option }
+ {
+ Clist~option~name~#1~does~not~end~with~-s.
}
\@@_latex_define_option_commands_and_keyvals:
\ExplSyntaxOff
@@ -16722,7 +17026,7 @@ texexec --passon=--shell-escape document.tex
% Lua Implementation {#luaimplementation}
%--------------------
%
-% The Lua implementation implements \luamdef{writer} and \luamdef{reader}
+% The Lua implementation implements \luamdef{writer} and \luamref{reader}
% objects, which provide the conversion from markdown to plain \TeX, and
% \luamdef{extensions} objects, which provide syntax extensions for the
% \luamref{writer} and \luamref{reader} objects.
@@ -16784,7 +17088,7 @@ function util.cache(dir, string, salt, transform, 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]])
+ [[Could not open file "]] .. name .. [[" for writing]])
local result = string
if transform ~= nil then
result = transform(result)
@@ -16812,6 +17116,32 @@ end
% \par
% \begin{markdown}
%
+% The \luamdef{util.lookup_files} method looks up files with filename `f`
+% and returns its path. If the \pkg{kpathsea} library is available, it will
+% search for files not only in the current working directory but also in the
+% \TeX{} directory structure. Further options for \pkg{kpathsea} can be
+% specified in table `options`. [@luatex21, Section 10.7.4]
+%
+% \end{markdown}
+% \begin{macrocode}
+util.lookup_files = (function()
+ local ran_ok, kpse = pcall(require, "kpse")
+ if ran_ok then
+ kpse.set_program_name("luatex")
+ else
+ kpse = { lookup = function(f, _) return f end }
+ end
+
+ local function lookup_files(f, options)
+ return kpse.lookup(f, options)
+ end
+
+ return lookup_files
+end)()
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
% The \luamdef{util.expand_tabs_in_line} expands tabs in string `s`. If
% `tabstop` is specified, it is used as the tab stop width. Otherwise,
% the tab stop width of 4 characters is used. The method is a copy of the tab
@@ -20432,14 +20762,6 @@ end
% \par
% \begin{markdown}
%
-%#### Lists
-%
-% \end{markdown}
-% \begin{macrocode}
-% \end{macrocode}
-% \par
-% \begin{markdown}
-%
%#### Headings
%
% \end{markdown}
@@ -20479,12 +20801,6 @@ end
% markdown reader object that was located in the
% `lunamark/reader/markdown.lua` file in the Lunamark Lua module.
%
-% Although not specified in the Lua interface (see Section
-% <#sec:luainterface>), the \luamref{reader} object is exported, so that the
-% curious user could easily tinker with the methods of the objects produced by
-% the \luamref{reader.new} method described below. The user should be aware,
-% however, that the implementation may change in a future revision.
-%
% The \luamdef{reader.new} method creates and returns a new \TeX{} reader
% object associated with the Lua interface options (see Section
% <#sec:luaoptions>) `options` and with a writer object `writer`. When
@@ -20498,7 +20814,7 @@ end
% \end{markdown}
% \begin{macrocode}
M.reader = {}
-function M.reader.new(writer, options, extensions)
+function M.reader.new(writer, options)
local self = {}
% \end{macrocode}
% \par
@@ -21147,150 +21463,174 @@ function M.reader.new(writer, options, extensions)
% \begin{markdown}
%
%#### Syntax Specification
+% Define \luamdef{reader->finalize_grammar} as a function that constructs the
+% \acro{peg} grammar of markdown, applies syntax extensions `extensions` and
+% returns a conversion function that takes a markdown string and turns it into
+% a plain \TeX{} output.
%
-% Create a \luamdef{reader->syntax} hash table that stores the \acro{peg}
-% grammar.
-%
-% \end{markdown}
-% \begin{macrocode}
- self.syntax =
- { "Blocks",
-
- Blocks = ( V("ExpectedJekyllData")
- * (V("Blank")^0 / writer.interblocksep)
- )^-1
- * V("Blank")^0
- * V("Block")^-1
- * (V("Blank")^0 / writer.interblocksep
- * V("Block"))^0
- * V("Blank")^0 * parsers.eof,
-
- Blank = parsers.Blank,
-
- UnexpectedJekyllData = parsers.fail,
- ExpectedJekyllData = parsers.fail,
-
- Block = V("ContentBlock")
- + V("UnexpectedJekyllData")
- + V("Blockquote")
- + V("PipeTable")
- + V("Verbatim")
- + V("FencedCode")
- + V("HorizontalRule")
- + V("BulletList")
- + V("OrderedList")
- + V("Heading")
- + V("DefinitionList")
- + V("DisplayHtml")
- + V("Paragraph")
- + V("Plain"),
-
- ContentBlock = parsers.fail,
- Blockquote = parsers.Blockquote,
- Verbatim = parsers.Verbatim,
- FencedCode = parsers.fail,
- HorizontalRule = parsers.HorizontalRule,
- BulletList = parsers.BulletList,
- OrderedList = parsers.OrderedList,
- Heading = parsers.Heading,
- DefinitionList = parsers.fail,
- DisplayHtml = parsers.DisplayHtml,
- Paragraph = parsers.Paragraph,
- PipeTable = parsers.fail,
- Plain = parsers.Plain,
- EndlineExceptions = parsers.EndlineExceptions,
-
- Inline = V("Str")
- + V("Space")
- + V("Endline")
- + V("UlOrStarLine")
- + V("Strong")
- + V("Emph")
- + V("StrikeThrough")
- + V("Superscript")
- + V("Subscript")
- + V("InlineNote")
- + V("NoteRef")
- + V("Citations")
- + V("Link")
- + V("Image")
- + V("Code")
- + V("AutoLinkUrl")
- + V("AutoLinkEmail")
- + V("AutoLinkRelativeReference")
- + V("InlineHtml")
- + V("HtmlEntity")
- + V("EscapedChar")
- + V("Smart")
- + V("Symbol"),
-
- IndentedInline = V("Str")
- + V("OptionalIndent")
- + V("Endline")
- + V("UlOrStarLine")
- + V("Strong")
- + V("Emph")
- + V("StrikeThrough")
- + V("Superscript")
- + V("Subscript")
- + V("InlineNote")
- + V("NoteRef")
- + V("Citations")
- + V("Link")
- + V("Image")
- + V("Code")
- + V("AutoLinkUrl")
- + V("AutoLinkEmail")
- + V("AutoLinkRelativeReference")
- + V("InlineHtml")
- + V("HtmlEntity")
- + V("EscapedChar")
- + V("Smart")
- + V("Symbol"),
-
- Str = parsers.Str,
- Space = parsers.Space,
- OptionalIndent = parsers.OptionalIndent,
- Endline = parsers.Endline,
- UlOrStarLine = parsers.UlOrStarLine,
- Strong = parsers.Strong,
- Emph = parsers.Emph,
- StrikeThrough = parsers.fail,
- Superscript = parsers.fail,
- Subscript = parsers.fail,
- InlineNote = parsers.fail,
- NoteRef = parsers.fail,
- Citations = parsers.fail,
- Link = parsers.Link,
- Image = parsers.Image,
- Code = parsers.Code,
- AutoLinkUrl = parsers.AutoLinkUrl,
- AutoLinkEmail = parsers.AutoLinkEmail,
- AutoLinkRelativeReference
- = parsers.AutoLinkRelativeReference,
- InlineHtml = parsers.InlineHtml,
- HtmlEntity = parsers.HtmlEntity,
- EscapedChar = parsers.EscapedChar,
- Smart = parsers.Smart,
- Symbol = parsers.Symbol,
- SpecialChar = parsers.fail,
- }
+% \end{markdown}
+% \begin{macrocode}
+ function self.finalize_grammar(extensions)
+% \end{macrocode}
+% \begin{markdown}
+%
+% Create a local writable copy of the global read-only
+% \luamdef{walkable_syntax} hash table. This table can be used by user-defined
+% syntax extensions to insert new \acro{peg} patterns into existing rules
+% of the \acro{peg} grammar of markdown using
+% the \luamref{reader->insert_pattern} method. Furthermore, built-in syntax
+% extensions can use this table to override existing rules using the
+% \luamref{reader->update_rule} method.
+%
+% \end{markdown}
+% \begin{macrocode}
+ 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)
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamref{reader->insert_pattern} as a function that receives two
+% arguments: a selector string in the form `"`\meta{left-hand side terminal
+% symbol} \meta{`before`, `after`, or `instead of`} \meta{right-hand side
+% terminal symbol}`"` and a \acro{peg} pattarn to insert. The function adds
+% the pattern to \luamref{walkable_syntax}`[`*left-hand side terminal
+% symbol*`]` before or after *right-hand side terminal symbol*.
+%
+% \end{markdown}
+% \begin{macrocode}
+ self.insert_pattern = function(selector, pattern)
+ 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]])
+ if pos == "instead of" then
+ rule[index] = pattern
+ else
+ table.insert(rule, index, pattern)
+ end
+ end
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Create a local \luamdef{syntax} hash table that stores those rules of the
+% \acro{peg} grammar of markdown that can't be represented as an ordered choice
+% of terminal symbols.
+%
+% \end{markdown}
+% \begin{macrocode}
+ local syntax =
+ { "Blocks",
+
+ Blocks = ( V("ExpectedJekyllData")
+ * (V("Blank")^0 / writer.interblocksep)
+ )^-1
+ * V("Blank")^0
+ * V("Block")^-1
+ * (V("Blank")^0 / writer.interblocksep
+ * V("Block"))^0
+ * V("Blank")^0 * parsers.eof,
+
+ ExpectedJekyllData = parsers.fail,
+
+ Blank = parsers.Blank,
+
+ Blockquote = parsers.Blockquote,
+ Verbatim = parsers.Verbatim,
+ HorizontalRule = parsers.HorizontalRule,
+ BulletList = parsers.BulletList,
+ OrderedList = parsers.OrderedList,
+ Heading = parsers.Heading,
+ DisplayHtml = parsers.DisplayHtml,
+ Paragraph = parsers.Paragraph,
+ Plain = parsers.Plain,
+ EndlineExceptions = parsers.EndlineExceptions,
+
+ Str = parsers.Str,
+ Space = parsers.Space,
+ OptionalIndent = parsers.OptionalIndent,
+ Endline = parsers.Endline,
+ UlOrStarLine = parsers.UlOrStarLine,
+ Strong = parsers.Strong,
+ Emph = parsers.Emph,
+ Link = parsers.Link,
+ Image = parsers.Image,
+ 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,
+ }
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Define \luamref{reader->update_rule} as a function that receives two arguments:
+% a left-hand side terminal symbol and a \acro{peg} pattarn. The function
+% (re)defines \luamref{walkable_syntax}`[`left-hand side terminal symbol`]` to
+% be equal to pattern.
+%
+% \end{markdown}
+% \begin{macrocode}
+ self.update_rule = function(rule_name, pattern)
+ assert(syntax[rule_name] ~= nil,
+ [[Rule ]] .. rule_name .. [[ -> ... does not exist in markdown grammar]])
+ walkable_syntax[rule_name] = { pattern }
+ end
% \end{macrocode}
% \par
% \begin{markdown}
%
% Define a hash table of all characters with special meaning and add method
-% `reader->add_special_character` that extends the hash table and updates the
-% \acro{peg} grammar.
+% \luamref{reader->add_special_character} that extends the hash table and
+% updates the \acro{peg} grammar of markdown.
%
% \end{markdown}
% \begin{macrocode}
- local special_characters = {"*", "`", "[", "]", "<", "!", "\\"}
- self.add_special_character = function(c)
- table.insert(special_characters, c)
- self.syntax.SpecialChar = S(table.concat(special_characters, ""))
- end
- self.syntax.SpecialChar = S(table.concat(special_characters, ""))
+ 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.add_special_character("\\")
% \end{macrocode}
% \par
% \begin{markdown}
@@ -21299,83 +21639,128 @@ function M.reader.new(writer, options, extensions)
%
% \end{markdown}
% \begin{macrocode}
- for _, extension in ipairs(extensions) do
- extension.extend_writer(writer)
- extension.extend_reader(self)
- end
-
- if options.underscores then
- self.add_special_character("_")
- end
+ for _, extension in ipairs(extensions) do
+ extension.extend_writer(writer)
+ extension.extend_reader(self)
+ end
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Duplicate the `Inline` rule as `IndentedInline` with the right-hand-side
+% terminal symbol `Space` replaced with `OptionalIndent`.
+%
+% \end{markdown}
+% \begin{macrocode}
+ walkable_syntax["IndentedInline"] = util.table_copy(
+ walkable_syntax["Inline"])
+ self.insert_pattern(
+ "IndentedInline instead of Space",
+ "OptionalIndent")
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Materialize \luamref{walkable_syntax} and merge it into \luamref{syntax} to
+% produce the complete \acro{peg} grammar of markdown. Whenever a rule exists
+% in both \luamref{walkable_syntax} and \luamref{syntax}, the rule from
+% \luamref{walkable_syntax} overrides the rule from \luamref{syntax}.
+%
+% \end{markdown}
+% \begin{macrocode}
+ 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
+ end
+ syntax[lhs] = syntax[lhs] + pattern
+ end
+ end
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Finalize the parser by enabling built-in syntax extensions and producing
+% special parsers for difficult edge cases such as blocks nested in definition
+% lists or inline content nested in link, note, and image labels.
+%
+% \end{markdown}
+% \begin{macrocode}
+ if options.underscores then
+ self.add_special_character("_")
+ end
- if not options.codeSpans then
- self.syntax.Code = parsers.fail
- end
+ if not options.codeSpans then
+ syntax.Code = parsers.fail
+ end
- if not options.html then
- self.syntax.DisplayHtml = parsers.fail
- self.syntax.InlineHtml = parsers.fail
- self.syntax.HtmlEntity = 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 options.preserveTabs then
+ options.stripIndent = false
+ end
- if not options.smartEllipses then
- self.syntax.Smart = parsers.fail
- else
- self.add_special_character(".")
- end
+ if not options.smartEllipses then
+ syntax.Smart = parsers.fail
+ else
+ self.add_special_character(".")
+ end
- if not options.relativeReferences then
- self.syntax.AutoLinkRelativeReference = parsers.fail
- end
+ if not options.relativeReferences then
+ syntax.AutoLinkRelativeReference = parsers.fail
+ end
- local blocks_nested_t = util.table_copy(self.syntax)
- blocks_nested_t.ExpectedJekyllData = parsers.fail
- parsers.blocks_nested = Ct(blocks_nested_t)
+ local blocks_nested_t = util.table_copy(syntax)
+ blocks_nested_t.ExpectedJekyllData = parsers.fail
+ parsers.blocks_nested = Ct(blocks_nested_t)
- parsers.blocks = Ct(self.syntax)
+ parsers.blocks = Ct(syntax)
- local inlines_t = util.table_copy(self.syntax)
- inlines_t[1] = "Inlines"
- inlines_t.Inlines = parsers.Inline^0 * (parsers.spacing^0 * parsers.eof / "")
- parsers.inlines = Ct(inlines_t)
+ local inlines_t = util.table_copy(syntax)
+ inlines_t[1] = "Inlines"
+ inlines_t.Inlines = parsers.Inline^0 * (parsers.spacing^0 * parsers.eof / "")
+ parsers.inlines = Ct(inlines_t)
- local inlines_no_link_t = util.table_copy(inlines_t)
- inlines_no_link_t.Link = parsers.fail
- parsers.inlines_no_link = Ct(inlines_no_link_t)
+ local inlines_no_link_t = util.table_copy(inlines_t)
+ inlines_no_link_t.Link = parsers.fail
+ parsers.inlines_no_link = Ct(inlines_no_link_t)
- local inlines_no_inline_note_t = util.table_copy(inlines_t)
- inlines_no_inline_note_t.InlineNote = parsers.fail
- parsers.inlines_no_inline_note = Ct(inlines_no_inline_note_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_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_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)
% \end{macrocode}
% \par
% \begin{markdown}
%
-%#### Exported Conversion Function
-% Define \luamdef{reader->convert} as a function that converts markdown string
-% `input` into a plain \TeX{} output and returns it. Note that the converter
-% assumes that the input has \acro{unix} line endings.
+% Return a function that converts markdown string `input` into a plain \TeX{}
+% output and returns it. Note that the converter assumes that the input has
+% \acro{unix} line endings.
%
% \end{markdown}
% \begin{macrocode}
- function self.convert(input)
- references = {}
+ return function(input)
+ references = {}
% \end{macrocode}
% \begin{markdown}
% When determining the name of the cache file, create salt for the hashing
@@ -21384,37 +21769,42 @@ function M.reader.new(writer, options, extensions)
% is disregarded.
% \end{markdown}
% \begin{macrocode}
- local opt_string = {}
- for k,_ in pairs(defaultOptions) do
- local v = options[k]
- if k ~= "cacheDir" then
- opt_string[#opt_string+1] = k .. "=" .. tostring(v)
+ 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
- end
- table.sort(opt_string)
- local salt = table.concat(opt_string, ",") .. "," .. metadata.version
- local output
+ table.sort(opt_string)
+ local salt = table.concat(opt_string, ",") .. "," .. metadata.version
+ local output
% \end{macrocode}
% \begin{markdown}
% If we cache markdown documents, produce the cache file and transform its
% filename to plain \TeX{} output via the \luamref{writer->pack} method.
% \end{markdown}
% \begin{macrocode}
- local function convert(input)
- local document = self.parser_functions.parse_blocks(input)
- return util.rope_to_string(writer.document(document))
- end
- if options.eagerCache or options.finalizeCache then
- local name = util.cache(options.cacheDir, input, salt, convert, ".md" .. writer.suffix)
- output = writer.pack(name)
+ local function convert(input)
+ local document = self.parser_functions.parse_blocks(input)
+ return util.rope_to_string(writer.document(document))
+ end
+ if options.eagerCache or options.finalizeCache then
+ local name = util.cache(options.cacheDir, input, salt, convert,
+ ".md" .. writer.suffix)
+ output = writer.pack(name)
% \end{macrocode}
% \begin{markdown}
% Otherwise, return the result of the conversion directly.
% \end{markdown}
% \begin{macrocode}
- else
- output = convert(input)
- end
+ else
+ output = convert(input)
+ end
% \end{macrocode}
% \begin{markdown}
% If the \Opt{finalizeCache} option is enabled, populate the frozen cache in
@@ -21422,36 +21812,37 @@ function M.reader.new(writer, options, extensions)
% number \Opt{frozenCacheCounter}.
% \end{markdown}
% \begin{macrocode}
- if options.finalizeCache then
- local file, mode
- if options.frozenCacheCounter > 0 then
- mode = "a"
- else
- mode = "w"
+ 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
- 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())
+ return output
end
- return output
end
return self
end
% \end{macrocode}
% \begin{markdown}
%
-%### Syntax Extensions for Markdown
+%### Built-In Syntax Extensions {#luabuiltinextensions}
%
-% Create \luamdef{extensions} hash table that contains syntax extensions.
-% Syntax extensions are functions that produce objects with two methods:
-% `extend_writer` and `extend_reader`. The `extend_writer` object takes a
-% \luamref{writer} object as the only parameter and mutates it. Similarly,
-% `extend_reader` takes a \luamref{reader} object as the only parameter and
-% mutates it.
+% Create \luamdef{extensions} hash table that contains built-in syntax
+% extensions. Syntax extensions are functions that produce objects with two
+% methods: `extend_writer` and `extend_reader`. The `extend_writer` object
+% takes a \luamref{writer} object as the only parameter and mutates it.
+% Similarly, `extend_reader` takes a \luamref{reader} object as the only
+% parameter and mutates it.
%
% \end{markdown}
% \begin{macrocode}
@@ -21548,7 +21939,6 @@ M.extensions.citations = function(citation_nbsps)
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local citation_chars
@@ -21656,7 +22046,7 @@ M.extensions.citations = function(citation_nbsps)
local Citations = TextCitations + ParenthesizedCitations
- syntax.Citations = Citations
+ self.insert_pattern("Inline after Emph", Citations)
self.add_special_character("@")
self.add_special_character("-")
@@ -21682,37 +22072,21 @@ M.extensions.content_blocks = function(language_map)
%
% The \luamdef{languages_json} table maps programming language filename
% extensions to fence infostrings. All `language_map` files located by the
-% KPathSea library are loaded into a chain of tables. \luamref{languages_json}
-% corresponds to the first table and is chained with the rest via Lua
-% metatables.
+% \pkg{kpathsea} library are loaded into a chain of tables.
+% \luamref{languages_json} corresponds to the first table and is chained with
+% the rest via Lua metatables.
%
% \end{markdown}
% \begin{macrocode}
local languages_json = (function()
- local ran_ok, kpse = pcall(require, "kpse")
- if ran_ok then
- kpse.set_program_name("luatex")
-% \end{macrocode}
-% \begin{markdown}
-%
-% If the KPathSea library is unavailable, perhaps because we are using
-% LuaMeta\TeX, we will only locate the `options.`\luamref{contentBlocksLanguageMap}
-% in the current working directory:
-%
-% \end{markdown}
-% \begin{macrocode}
- else
- kpse = {lookup=function(filename, _) return filename end}
- end
local base, prev, curr
- for _, filename in ipairs{kpse.lookup(language_map, { all=true })} do
- local file = io.open(filename, "r")
+ for _, pathname in ipairs{util.lookup_files(language_map, { all=true })} do
+ local file = io.open(pathname, "r")
if not file then goto continue end
- local json = file:read("*all"):gsub('("[^\n]-"):','[%1]=')
- curr = (function()
- local _ENV={ json=json, load=load } -- luacheck: ignore _ENV
- return load("return "..json)()
- 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
@@ -21764,7 +22138,6 @@ M.extensions.content_blocks = function(language_map)
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local contentblock_tail
@@ -21838,7 +22211,7 @@ M.extensions.content_blocks = function(language_map)
* contentblock_tail
/ writer.contentblock
- syntax.ContentBlock = ContentBlock
+ self.insert_pattern("Block before Blockquote", ContentBlock)
end
}
end
@@ -21894,7 +22267,6 @@ M.extensions.definition_lists = function(tight_lists)
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local defstartchar = S("~:")
@@ -21937,7 +22309,7 @@ M.extensions.definition_lists = function(tight_lists)
* -DefinitionListItemLoose * Cc(true))
) / writer.definitionlist
- syntax.DefinitionList = DefinitionList
+ self.insert_pattern("Block after Heading", DefinitionList)
end
}
end
@@ -21976,7 +22348,6 @@ M.extensions.fenced_code = function(blank_before_code_fence)
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local function captures_geq_length(_,i,a,b)
@@ -22039,7 +22410,8 @@ M.extensions.fenced_code = function(blank_before_code_fence)
self.expandtabs(code))
end
- syntax.FencedCode = FencedCode
+ self.insert_pattern("Block after Verbatim",
+ FencedCode)
local fencestart
if blank_before_code_fence then
@@ -22049,8 +22421,8 @@ M.extensions.fenced_code = function(blank_before_code_fence)
+ fencehead(parsers.tilde)
end
- parsers.EndlineExceptions = parsers.EndlineExceptions + fencestart
- syntax.EndlineExceptions = parsers.EndlineExceptions
+ local EndlineExceptions = parsers.EndlineExceptions + fencestart
+ self.update_rule("EndlineExceptions", EndlineExceptions)
self.add_special_character("~")
end
@@ -22087,9 +22459,16 @@ M.extensions.footnotes = function(footnotes, inline_footnotes)
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
+ if inline_footnotes then
+ local InlineNote
+ = parsers.circumflex
+ * (parsers.tag / self.parser_functions.parse_inlines_no_inline_note)
+ / writer.note
+
+ self.insert_pattern("Inline after Emph", InlineNote)
+ end
if footnotes then
local function strip_first_char(s)
return s:sub(2)
@@ -22127,17 +22506,10 @@ M.extensions.footnotes = function(footnotes, inline_footnotes)
* parsers.spnl * parsers.indented_blocks(parsers.chunk)
/ register_note
- parsers.Blank = NoteBlock + parsers.Blank
- syntax.Blank = parsers.Blank
+ local Blank = NoteBlock + parsers.Blank
+ self.update_rule("Blank", Blank)
- syntax.NoteRef = NoteRef
- end
- if inline_footnotes then
- local InlineNote
- = parsers.circumflex
- * (parsers.tag / self.parser_functions.parse_inlines_no_inline_note)
- / writer.note
- syntax.InlineNote = InlineNote
+ self.insert_pattern("Inline after Emph", NoteRef)
end
self.add_special_character("^")
@@ -22159,7 +22531,6 @@ M.extensions.header_attributes = function()
extend_writer = function()
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
parsers.AtxHeading = Cg(parsers.HeadingStart,"level")
@@ -22204,8 +22575,8 @@ M.extensions.header_attributes = function()
* parsers.newline
/ writer.heading
- parsers.Heading = parsers.AtxHeading + parsers.SetextHeading
- syntax.Heading = parsers.Heading
+ local Heading = parsers.AtxHeading + parsers.SetextHeading
+ self.update_rule("Heading", Heading)
end
}
end
@@ -22322,7 +22693,6 @@ M.extensions.jekyll_data = function(expect_jekyll_data)
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local JekyllData
@@ -22358,9 +22728,9 @@ M.extensions.jekyll_data = function(expect_jekyll_data)
* JekyllData
* (P("---") + P("..."))^-1
- syntax.UnexpectedJekyllData = UnexpectedJekyllData
+ self.insert_pattern("Block before Blockquote", UnexpectedJekyllData)
if expect_jekyll_data then
- syntax.ExpectedJekyllData = ExpectedJekyllData
+ self.update_rule("ExpectedJekyllData", ExpectedJekyllData)
end
end
}
@@ -22468,7 +22838,6 @@ M.extensions.pipe_tables = function(table_captions)
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local table_hline_separator = parsers.pipe + parsers.plus
@@ -22523,7 +22892,7 @@ M.extensions.pipe_tables = function(table_captions)
* table_caption^-1
/ writer.table
- syntax.PipeTable = PipeTable
+ self.insert_pattern("Block after Blockquote", PipeTable)
end
}
end
@@ -22554,7 +22923,6 @@ M.extensions.strike_through = function()
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local StrikeThrough = (
@@ -22562,7 +22930,7 @@ M.extensions.strike_through = function()
parsers.doubletildes)
) / writer.strike_through
- syntax.StrikeThrough = StrikeThrough
+ self.insert_pattern("Inline after Emph", StrikeThrough)
self.add_special_character("~")
end
@@ -22595,14 +22963,13 @@ M.extensions.superscripts = function()
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local Superscript = (
parsers.between(parsers.Str, parsers.circumflex, parsers.circumflex)
) / writer.superscript
- syntax.Superscript = Superscript
+ self.insert_pattern("Inline after Emph", Superscript)
self.add_special_character("^")
end
@@ -22635,14 +23002,13 @@ M.extensions.subscripts = function()
end
end, extend_reader = function(self)
local parsers = self.parsers
- local syntax = self.syntax
local writer = self.writer
local Subscript = (
parsers.between(parsers.Str, parsers.tilde, parsers.tilde)
) / writer.subscript
- syntax.Subscript = Subscript
+ self.insert_pattern("Inline after Emph", Subscript)
self.add_special_character("~")
end
@@ -22729,7 +23095,6 @@ M.extensions.fancy_lists = function()
end, extend_reader = function(self)
local parsers = self.parsers
local options = self.options
- local syntax = self.syntax
local writer = self.writer
local label = parsers.dig + parsers.letter
@@ -22845,8 +23210,7 @@ M.extensions.fancy_lists = function()
* Cc(false) * parsers.skipblanklines
) * Cb("listtype") / fancylist
- syntax.OrderedList = FancyList
-
+ self.update_rule("OrderedList", FancyList)
end
}
end
@@ -22855,10 +23219,9 @@ end
%
%### Conversion from Markdown to Plain \TeX{}
%
-% The \luamref{new} method returns the \luamref{reader->convert} function of a reader
-% object associated with the Lua interface options (see Section
-% <#sec:luaoptions>) `options` and with a writer object associated with
-% `options`.
+% The \luamref{new} function returns a conversion function that takes a
+% markdown string and turns it into a plain \TeX{} output. See Section
+% <#luaconversion>.
%
% \end{markdown}
% \begin{macrocode}
@@ -22874,20 +23237,16 @@ function M.new(options)
options = options or {}
setmetatable(options, { __index = function (_, key)
return defaultOptions[key] end })
+% \end{macrocode}
% \par
% \begin{markdown}
%
-% Apply syntax extensions based on `options`.
+% Apply built-in syntax extensions based on `options`.
%
% \end{markdown}
% \begin{macrocode}
local extensions = {}
- if options.citations then
- local citations_extension = M.extensions.citations(options.citationNbsps)
- table.insert(extensions, citations_extension)
- end
-
if options.contentBlocks then
local content_blocks_extension = M.extensions.content_blocks(
options.contentBlocksLanguageMap)
@@ -22906,12 +23265,6 @@ function M.new(options)
table.insert(extensions, fenced_code_extension)
end
- if options.footnotes or options.inlineFootnotes then
- local footnotes_extension = M.extensions.footnotes(
- options.footnotes, options.inlineFootnotes)
- table.insert(extensions, footnotes_extension)
- end
-
if options.headerAttributes then
local header_attributes_extension = M.extensions.header_attributes()
table.insert(extensions, header_attributes_extension)
@@ -22944,15 +23297,128 @@ function M.new(options)
table.insert(extensions, superscript_extension)
end
+ if options.footnotes or options.inlineFootnotes then
+ local footnotes_extension = M.extensions.footnotes(
+ options.footnotes, options.inlineFootnotes)
+ table.insert(extensions, footnotes_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
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Apply user-defined syntax extensions based on `options.extensions`.
+%
+% \end{markdown}
+% \begin{macrocode}
+ for _, user_extension_filename in ipairs(options.extensions) do
+ local user_extension = (function(filename)
+% \end{macrocode}
+% \begin{markdown}
+%
+% First, load and compile the contents of the user-defined syntax extension.
+%
+% \end{markdown}
+% \begin{macrocode}
+ local pathname = util.lookup_files(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 [[]]))
+% \end{macrocode}
+% \begin{markdown}
+%
+% Then, validate the user-defined syntax extension.
+%
+% \end{markdown}
+% \begin{macrocode}
+ 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 == 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]])
+% \end{macrocode}
+% \begin{markdown}
+%
+% Finally, cast the user-defined syntax extension to the internal format
+% of user extensions used by the Markdown package (see Section
+% <#luabuiltinextensions>.)
+%
+% \end{markdown}
+% \begin{macrocode}
+ local extension = {
+ extend_reader = user_extension.finalize_grammar,
+ extend_writer = function() end,
+ }
+ return extension
+ end)(user_extension_filename)
+ table.insert(extensions, user_extension)
+ end
+% \end{macrocode}
+% \par
+% \begin{markdown}
+%
+% Produce and return a conversion function from markdown to plain \TeX.
+%
+% \end{markdown}
+% \begin{macrocode}
local writer = M.writer.new(options)
- local reader = M.reader.new(writer, options, extensions)
+ local reader = M.reader.new(writer, options)
+ local convert = reader.finalize_grammar(extensions)
- return reader.convert
+ return convert
end
return M
@@ -22975,7 +23441,7 @@ return M
local input
if input_filename then
local input_file = assert(io.open(input_filename, "r"),
- [[could not open file "]] .. input_filename .. [[" for reading]])
+ [[Could not open file "]] .. input_filename .. [[" for reading]])
input = assert(input_file:read("*a"))
assert(input_file:close())
else
@@ -23018,7 +23484,7 @@ local output = convert(input:gsub("\r\n?", "\n") .. "\n")
if output_filename then
local output_file = assert(io.open(output_filename, "w"),
- [[could not open file "]] .. output_filename .. [[" for writing]])
+ [[Could not open file "]] .. output_filename .. [[" for writing]])
assert(output_file:write(output))
assert(output_file:close())
else
@@ -23409,25 +23875,50 @@ end
\@@_get_option_type:nN
{ #1 }
\l_tmpa_tl
- \bool_if:nTF
+ \bool_case_true:nF
{
- \str_if_eq_p:VV
- \l_tmpa_tl
- \c_@@_option_type_boolean_tl ||
- \str_if_eq_p:VV
- \l_tmpa_tl
- \c_@@_option_type_number_tl ||
- \str_if_eq_p:VV
- \l_tmpa_tl
- \c_@@_option_type_counter_tl
- }
- {
- \@@_get_option_value:nN
- { #1 }
- \l_tmpa_tl
- \tl_gput_right:Nx
- \g_@@_formatted_lua_options_tl
- { #1~=~ \l_tmpa_tl ,~ }
+ {
+ \str_if_eq_p:VV
+ \l_tmpa_tl
+ \c_@@_option_type_boolean_tl ||
+ \str_if_eq_p:VV
+ \l_tmpa_tl
+ \c_@@_option_type_number_tl ||
+ \str_if_eq_p:VV
+ \l_tmpa_tl
+ \c_@@_option_type_counter_tl
+ }
+ {
+ \@@_get_option_value:nN
+ { #1 }
+ \l_tmpa_tl
+ \tl_gput_right:Nx
+ \g_@@_formatted_lua_options_tl
+ { #1~=~ \l_tmpa_tl ,~ }
+ }
+ {
+ \str_if_eq_p:VV
+ \l_tmpa_tl
+ \c_@@_option_type_clist_tl
+ }
+ {
+ \@@_get_option_value:nN
+ { #1 }
+ \l_tmpa_tl
+ \tl_gput_right:Nx
+ \g_@@_formatted_lua_options_tl
+ { #1~=~\c_left_brace_str }
+ \clist_map_inline:Vn
+ \l_tmpa_tl
+ {
+ \tl_gput_right:Nx
+ \g_@@_formatted_lua_options_tl
+ { "##1" ,~ }
+ }
+ \tl_gput_right:Nx
+ \g_@@_formatted_lua_options_tl
+ { \c_right_brace_str ,~ }
+ }
}
{
\@@_get_option_value:nN
@@ -23438,6 +23929,9 @@ end
{ #1~=~ " \l_tmpa_tl " ,~ }
}
}
+\cs_generate_variant:Nn
+ \clist_map_inline:nn
+ { Vn }
\let\markdownPrepareLuaOptions=\@@_format_lua_options:
\def\markdownLuaOptions{{ \g_@@_formatted_lua_options_tl }}
\ExplSyntaxOff
@@ -23737,8 +24231,8 @@ end
%
% The package assumes that although the user is not using the Lua\TeX{} engine,
% their \TeX{} distribution contains it, and uses shell access to produce and
-% execute Lua scripts using the \TeX{}Lua interpreter~[@luatex17, Section
-% 3.1.1].
+% execute Lua scripts using the \TeX{}Lua interpreter~[@luatex21, Section
+% 4.1.1].
%
% \end{markdown}
% \begin{macrocode}
@@ -24020,7 +24514,7 @@ end
|markdownLuaExecute{%
|markdownPrepare
local file = assert(io.open("&1", "r"),
- [[could not open file "&1" for reading]])
+ [[Could not open file "&1" for reading]])
local input = assert(file:read("*a"))
assert(file:close())
% \end{macrocode}