lmd: Markdown and Lua in the same file
An lmd cell (Lua and markdown) is an executable cell whose language is Lua and whose result is markdown. It needs no engine or runtime: Quarto runs it while rendering, and whatever the cell returns takes the cell’s place in the document. Two ideas, layered, make it more than “Lua in a code block”:
- Markdown in your Lua. Inside the cell, a piece of markdown is an
expression.
{=: … :=}is a block value and{=[ … ]=}an inline value, written as markdown and handed to you as thepandoc.Blocksorpandoc.Inlinesyou would otherwise assemble out of constructors. Inside such a fragment,`{lmd} expr`puts a Lua value back into the markdown, so the two languages nest in both directions. - Lua in your markdown. A
```{lmd}block is replaced by what it returns, where it sits;`{lmd} expr`in a paragraph is replaced by the value of the expression. A cell is a filter that acts in place, so it never has to name a class to match on.
A Lua filter is Lua for your markdown: a separate file, applied to the whole document from outside. lmd is markdown right in your Lua, and Lua right in your markdown, in the same file.
Demo 1 is the whole thing in one cell:
Everything on this page is a pattern of use. The mechanics are introduced as each pattern needs them and collected in The rules, in one place near the end.
Two facts shape all of it. All the cells of a document run in one Lua state, in document order, so a cell can define something a later cell uses. And cells run after the engines and before everything else: user filters, shortcodes, cross-references and callouts all see what a cell returned, exactly as if you had typed it.
Reaching for metadata
The most common reason to reach for a cell is a value in the front matter. This page’s own front matter has:
version: 12.1
team: [ada, grace, linus]and the cells on this page read it. meta is the document metadata as a
Lua value you navigate: meta.version, meta.team[1], meta.author.name
for nested keys. Each metadata value arrives in the shape a Lua filter would
see it:
| front-matter value | in Lua |
|---|---|
| a string (markdown by default) | Inlines, so emphasis and code in a title survive
|
| a number, a boolean | the number, the boolean |
| a list | a pandoc.List, with #, ipairs, :map, :filter
|
| a map | a value you keep navigating, meta.author.affil
|
A path that may be absent is read with quarto.metadata.get("a.b.c"),
which returns nil for a missing key where meta.a.b.c would raise. The
two agree everywhere else; numeric segments index lists, so
quarto.metadata.get("authors.2.name") is meta.authors[2].name.
Computing on it
The {{< meta version >}} shortcode inserts a value. A cell can also
compute on it. Live, from this page’s front matter:
The team has `{lmd} #meta.team` members:
`{lmd} table.concat(meta.team:map(pandoc.utils.stringify), ", ")`.
The lead is `{lmd} pandoc.utils.stringify(meta.team[1]):upper()`,
and this is version `{lmd} meta.version`.
The team has 3 members: ada, grace, linus. The lead is ADA, and this is version 12.1.
Two things to know, both visible above:
- A number or boolean reads the way
tostringprints it:12.1,3,true. The same holds wherever an Inline is expected, sopandoc.Strong(meta.version)works, and so does an inline cell inside a fragment. Pandoc itself rejectspandoc.Strong(3); Quarto accepts it because its metadata hands cells real numbers. - Metadata strings are
Inlines, not Lua strings, sotable.concatand:upper()needpandoc.utils.stringifyfirst. Leave theInlinesalone when the formatting should survive:`{lmd} meta.title`renders a title’s emphasis and code as written.
A value that holds markdown as a plain string (a !str metadata field, a
description read from a data file, a listing item’s description) is read
with quarto.markdown(str), which parses the string and returns a Nodes
value: blocks where blocks go, inlines where a one-paragraph string lands
inline. return quarto.markdown(item.description) is the run-time twin of a
{= … =} fragment, and a footnote the string defines is paired with its
reference just as in a fragment.
Demo 2 puts the two halves together with a nested key and a fallback for a key that is not there:
Repeating markdown over a list
The second pattern: the same markup for every element of a list. The
natural shape is :map from the list to a fragment per element, and then
the question is what to do with the pieces.
Items of a list. The item of a pandoc.BulletList is a list of blocks.
An inline fragment wrapped in pandoc.Plain gives a tight list; a block
fragment gives an item of one or more paragraphs:
```{lmd}
return pandoc.BulletList(meta.authors:map(function(a)
return pandoc.Plain({=[ `{lmd} a.name` (`{lmd} a.role`) ]=})
end))
```
Two shapes look right and are not. An inline fragment passed bare as an
item, return {=[ … ]=} inside the :map, is coerced one inline at a time,
and the item renders one word per line. And one {=: - item :=} fragment
per element gives a list of one-item lists, rendered as separate lists.
A section per element. Accumulate into a pandoc.Blocks with :extend,
and return it:
```{lmd}
local out = pandoc.Blocks({})
for i, t in ipairs(meta.team) do
out:extend({=:
### `{lmd} t`
Member number `{lmd} i`.
:=})
end
return out
```
Headings built this way get their ids from their final text, unique across the document, so they are linkable and show up in the table of contents like any other heading.
Two fragments. a .. b concatenates two Blocks values, for the case
where a loop is more than you need.
Demo 3 shows all of these, and an ordered list:
Fragments as functions
A function that returns a fragment is a reusable piece of markup, with the
arguments spliced in wherever an inline `{lmd} expr` appears. A fragment can also
hold a block cell of its own, which is how a function splices a whole body
of blocks into its markup:
Two markdown nesting rules come up here. A cell that contains a fenced block needs a longer fence than the one inside it. And an inline cell whose fragment holds a code span needs double-backtick delimiters, since single backticks pair up left to right.
A fragment as the argument
When a function takes one fragment, the parentheses can go: callout{=: … :=}
is a call with the fragment as its whole argument list, the way Lua already
reads f{…} for a table and f"…" for a string. A component then reads
like a tag around its body:
```{lmd}
local function note(body)
return pandoc.Div(body, pandoc.Attr("", {"callout-note"}))
end
return note{=:
Anything the fragment holds, *as blocks*.
:=}
```
Both spellings are the same call; note({=: … :=}) works too. A method
call takes the form as well: card:render{=[ title ]=}.
Quoting fragment syntax
Inside a fragment, every {= opens a nested fragment and every =} closes
one, so a fragment that talks about fragments needs one of two things.
For a literal here and there, escape the brace: \{= and =\}. Quarto
markdown wants a lone brace written \{ or \} in any case, and the
backslash also tells lmd not to read the pair as a delimiter. Balanced
fragment syntax inside a code span needs nothing: `{=: x :=}` opens
and closes, so the fragment around it ends where it should.
For a body with unbalanced delimiters, or a lot of them, raise the level
of the fragment the way a Lua long string does: {==: … :==}, {==[ … ]==},
{== … ==}, with as many = as it takes. Nothing inside a leveled fragment
is balanced or escaped; its content runs to the first closer of the same
level. Inline lmd code inside it is still code, since that is the markdown
reader’s business, not the delimiter scanner’s.
```{lmd}
return {==:
The closer of an inline fragment is `]=}`; on its own it would end this
fragment, so this one is written at level two.
:==}
```
({==} stays the empty unmarked fragment: an opener followed directly by
} is read at level one.)
Reusable modules
A function defined in a cell is visible to the later cells of the same
document and nowhere else. To share it between documents, put it in a
module: a .lmd file, which is a Lua chunk that may hold markdown
fragments, returning a table of functions. require loads it the way it
loads a .lua file, relative to the requiring file first and then
relative to the document, compiling the fragments on the way in. Here
components/cards.lmd holds the card function from the previous
section, plus a deck that maps it over a list:
-- components/cards.lmd
local M = {}
function M.card(title, body)
return {=:
::: {.card .p-3 .mb-3}
### `{lmd} title`
`{lmd} body`
:::
:=}
end
function M.deck(items)
return items:map(function(it) return M.card(it.title, it.body) end)
end
return MDemo 6 loads it from two cells:
A few things to know about modules:
- A module runs once per document.
requirecaches by file, so every cell that requirescomponents/cardsgets the same table, and a module that sets up state does so once. Two documents are two Lua states, so nothing is shared between pages. - A module sees what a cell sees.
pandoc,quarto,meta(the requiring document’s metadata) andFORMATare all in scope. .lmdwins over.lua. Whencards.lmdandcards.luasit side by side, the.lmdis the one that loads and a warning names both files, so a stale compiled copy can never shadow the source you are editing.- Errors name the module. A Lua error inside a module reports
cards.lmd:12:with the module’s own line, and the cell that required it is underlined. A module that does not compile, because of a Lua syntax error or a fragment the reader rejects, fails the requiring cell at itsrequireline withQ-26-8. - The compiler is a function.
quarto.lmd.compile(source, name)returns the plain Lua an lmd source compiles to, or raises with the position of the problem, so a cell can compile lmd text it holds (from a data file, say) andloadthe result itself.
Plain Lua filters and shortcode handlers can require an .lmd module
too; the loader is the same one.
Across a project
Beside the requiring file and beside the document, require knows two
more places:
- The project root, with a leading slash:
require("/components/cards")findscomponents/cards.lmdat the root of the project however deep the document sits (the same conventioncomponents.srcuses). For a single-file render the root is the document’s directory. A path that exists as an absolute path is still read as itself, so a Quarto 1 filter’srequire(quarto.utils.resolve_path(…))keeps working. - An extension, by its name:
require("badges/pill")finds_extensions/badges/pill.lmd, and a barerequire("badges")is that extension’sinit.lmd. A localbadges/directory beside the document wins over an extension of the same name. An extension that exists only to ship modules declarescontributes: modules: truein its_extension.yml.
A module every page needs can be bound once, for the whole project or one
document, so no cell has to require it. lmd: modules: lists modules to
load before the first cell; each is bound to a global named after its last
path segment, or after the key when the entry is a one-key map:
lmd:
modules:
- components/cards # cards.card(…) in every cell
- badges # the extension, as `badges`
- ui: components/widgets # a chosen nameThe modules load through the same require, so project-root and
extension names work here too. A module that fails to load is reported once
at its entry (Q-26-9) and the page’s cells
are left as written, instead of each failing on the missing name.
Where require returns a module value, cached once per document,
quarto.include(file, params) is a call: it evaluates a file every time
and returns its blocks. An .lmd partial receives params as its vararg,
and a .qmd or .md partial is read as markdown:
```{lmd}
-- partials/callout.lmd starts with `local p = ...`
return quarto.include("partials/callout.lmd", { kind = "tip", body = {=[ Read this. ]=} })
```
```{lmd}
return quarto.include("/partials/footer.md")
```
The path is relative to the file that calls quarto.include (a module’s
function includes beside its module), or to the project root with a leading
slash. Every file an lmd cell reaches, through require or
quarto.include, is read through Quarto’s own file layer, so the same
modules load in the Quarto Hub preview, and a sandboxed render can limit
what a document may read.
Demo 7 shows the four together in a small project:
Conditional content
A cell that returns nil vanishes, so an if around the return is all it
takes to include something conditionally:
The callout renders as a callout because whatever a cell returns is processed as if typed: fenced divs with classes, cross-reference labels and callouts inside a fragment all work.
A filter on a piece of markdown
A fragment is a pandoc.Blocks value, and Blocks has walk. So the
filter vocabulary you may know from Lua filters applies to a
value in hand rather than to the whole document:
Every pandoc.* constructor is available too, for whatever is easier to
build from data than to write out (a pandoc.Span with computed attributes,
for instance). The Lua filters page describes the API.
Seeing what ran, and when a cell fails
--| echo: true at the top of a block cell keeps the Lua the cell actually
ran visible above its result. Fragments appear rewritten into pandoc
constructor calls, which is also the clearest explanation of what a
fragment is:
A cell that fails produces an error located at the failing line, with the cell left displayed as a code block so the page still renders and the code can be read:
Error: [Q-26-4] lmd cell failed
╭─[ report.qmd:32:1 ]
│
32 │ error("boom")
│ ──────┬──────
│ ╰──────── runtime error: report.qmd:32: boom
stack traceback:
[C]: in function 'error'
report.qmd:32: in main chunk
────╯
The failures, each with a reference page: the Lua does not parse
(Q-26-1); a fragment holds markdown that
cannot be built, such as a footnote or a shortcode
(Q-26-2); an inline fragment parses to more
than one block (Q-26-3); the Lua raises at
run time (Q-26-4); a block cell returns
something that is not blocks (Q-26-5); an
inline cell produces something that is not inlines
(Q-26-6); the cell options do not parse
(Q-26-7). A markdown syntax error inside a
fragment is reported with the markdown reader’s own code and underlined
inside the fragment. The error reference lists
them all.
Writing about lmd
This page shows cells without running them, which is the ordinary markdown
rule described under Computations:
a cell inside a markdown fence with more backticks is displayed. In prose,
`{lmd}` on its own is a mention and stays as code; a tag followed by
an expression runs.
A displayed or echoed lmd block is syntax-highlighted as lmd: the Lua
with Lua’s colours, the fragment delimiters and their markdown marked as a
special string. A plain ```lmd fence gets the same treatment. In the
Quarto Hub editor an .lmd module opens with Lua highlighting.
The rules, in one place
What a cell may return. A block cell returns a Block, a list of Blocks
(a pandoc.Blocks, a plain table of blocks, or a list of such lists), or
nil. An inline cell evaluates to an Inline, a list of Inlines, a string, a
number, a boolean, or nil; a string is split into words and spaces as
pandoc does, and a number or boolean is written as tostring prints it.
The three fragment forms.
| form | value | notes |
|---|---|---|
{=: … :=}
|
pandoc.Blocks
|
any markdown; a top-level paragraph is a Para
|
{=[ … ]=}
|
pandoc.Inlines
|
one paragraph or nothing, else Q-26-3
|
{= … =}
|
pandoc.Nodes
|
decided by context: blocks where blocks go (a paragraph stays a paragraph), inlines where inlines go (a one-paragraph fragment unwraps) |
The two marked forms cover everything on this page. The unmarked form exists
for a value that is passed to both kinds of position; pandoc.Para({= x =})
and pandoc.Div({= x =}) both work.
Delimiters. f{=: … :=} is a call with the fragment as its argument.
A literal {= or =} in a fragment is \{= or =\}. A fragment at a
higher level, {==: … :==} and so on, is raw up to the closer of the same
level; see Quoting fragment syntax.
When cells run. After the engines, so a Python or R cell has already run and neither side sees the other’s variables; before user filters, shortcodes, cross-references, callouts and the table of contents. One Lua state per document; cells run in document order, pre-order through nested blocks.
Heading ids follow the final text of the heading, after every cell has
run, and are unique across the document. An explicit {#id} is kept.
What a cell can use. The pandoc and quarto tables a Lua filter gets
(constructors, pandoc.utils, quarto.doc.is_format, quarto.utils),
meta and quarto.metadata.get, quarto.markdown for markdown held in a
string, FORMAT, require for a .lua or .lmd module (beside the
document, at the project root with /, or in an extension by name) and
quarto.include for a partial (see Reusable modules
and Across a project).
Footnotes. A note defined inside a fragment is paired with the fragment’s references; a reference to a note defined in the document body is resolved with the document’s notes. Both are numbered with the rest of the page.
Current limitations
- Shortcodes inside a fragment fail the cell
(
Q-26-2). Read the value in Lua instead:`{lmd} meta.key`rather than{{< meta key >}}. - Inline cells in attribute values (a fenced div’s
data-x="…", a link title) are not evaluated. pandoc.OrderedListneeds explicitpandoc.ListAttributesfor output formats other than HTML.
Where this leads
A listing declared with type: metadata renders nothing of its own and
publishes its resolved items at meta.listings.<id>, for a cell to turn into
whatever markup the page wants; see Listings as data.
And a cell can hand what it builds to a React component with
quarto.component, so the markup it writes is the no-script version of an
interactive element; see Building the element from Lua.
The websites/09-interactive-listing example in the repository has both
together.