Skip to main content

lmd: Markdown and Lua in the same file

Write Lua cells whose results are markdown, and markdown fragments that are Lua values: read metadata and compute on it, repeat markup over a list, share state between cells, and build reusable fragments, all inside the document.

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”:

  1. 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 the pandoc.Blocks or pandoc.Inlines you 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.
  2. 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:

---
title: "A first lmd cell"
stuff: [a, b, c, d]
---

The front matter of this document has `stuff: [a, b, c, d]`. The cell below
produces one bold paragraph per item:

```{lmd}
return meta.stuff:map(function(v) return {=: **`{lmd} v`** :=} end)
```

Demo 1: One cell, four paragraphs. meta.stuff is the front-matter list; :map is a pandoc.List method; {=: … :=} turns markdown into a value; the inner `{lmd} v` puts each item back into the markdown; the list of blocks the cell returns replaces the cell. View source

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 tostring prints it: 12.1, 3, true. The same holds wherever an Inline is expected, so pandoc.Strong(meta.version) works, and so does an inline cell inside a fragment. Pandoc itself rejects pandoc.Strong(3); Quarto accepts it because its metadata hands cells real numbers.
  • Metadata strings are Inlines, not Lua strings, so table.concat and :upper() need pandoc.utils.stringify first. Leave the Inlines alone 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:

---
title: "Reading metadata"
version: 12.1
team: [ada, grace, linus]
author:
  name: Ada
  affil: Analytical Engines
---

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()`.

This is version `{lmd} meta.version` (a number in the front matter; an
inline cell writes it the way `tostring` would).

`{lmd} meta.author.name` works at `{lmd} quarto.metadata.get("author.affil")`.
A key that may be absent is read with `quarto.metadata.get`, which returns
`nil` instead of raising: `{lmd} quarto.metadata.get("author.phone") or "no phone on file"`.

Demo 2: Reading and computing on metadata: a count, a joined list, a string method, a number written as tostring would print it, a nested key through meta and through quarto.metadata.get, and a fallback for an absent key. View source

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:

---
title: "Repeating markdown over a list"
team: [ada, grace, linus]
authors:
  - name: Ada
    role: lead
  - name: Grace
    role: reviewer
---

## A tight list

Each item of a bullet list is a list of blocks, so an inline fragment is
wrapped in `pandoc.Plain`:

```{lmd}
return pandoc.BulletList(meta.authors:map(function(a)
  return pandoc.Plain({=[ `{lmd} a.name` (`{lmd} a.role`) ]=})
end))
```

## Items with several blocks

A block fragment can hold any number of blocks, so an item can be more
than one paragraph:

```{lmd}
return pandoc.BulletList(meta.authors:map(function(a)
  return {=:
`{lmd} a.name` is the `{lmd} a.role`.

A second paragraph about `{lmd} a.name`.
:=}
end))
```

## An ordered list

```{lmd}
return pandoc.OrderedList(meta.authors:map(function(a)
  return pandoc.Plain({=[ `{lmd} a.name` ]=})
end))
```

## A section per item

```{lmd}
local out = pandoc.Blocks({})
for i, t in ipairs(meta.team) do
  out:extend({=:
### `{lmd} t`

Member number `{lmd} i`.
:=})
end
return out
```

Demo 3: A tight list from pandoc.Plain items, items with several blocks from block fragments, an ordered list, and a section per element from :extend. View source

Setup cells and shared state

A cell with no return leaves nothing in the document. That makes it the natural place for definitions: every cell of a document runs in the same Lua state, in document order, so a variable or function defined in one cell is there for every cell after it, block and inline alike.

---
title: "Setup cells and shared state"
---

A cell with no `return` leaves nothing behind. Everything it defines is
visible to the cells after it:

```{lmd}
greeting = "Hello"
function badge(text)
  return {=[ [`{lmd} text`]{.badge .text-bg-secondary} ]=}
end
```

## `{lmd} greeting`, world

A `{lmd} badge("new")` feature and a `{lmd} badge("beta")` one. The heading
above got its id from its final text.

Demo 4: A setup cell defines greeting and badge; the heading and the paragraph after it use them. The heading’s id is hello-world, derived from its final text. View source

Document order runs top to bottom through nested blocks: a cell inside a fenced div runs before a top-level cell that follows the div, and an inline cell in the paragraph above a setup cell sees nil for what that cell defines.

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:

---
title: "Fragments as functions"
---

A function that returns a fragment is a reusable piece of markup. The
fragment here contains a block cell of its own, which splices `body` in,
so the outer cell needs a four-backtick fence:

````{lmd}
local function card(title, body)
  return {=:
::: {.card .p-3 .mb-3}
### `{lmd} title`

```{lmd}
return body
```
:::
:=}
end
return card("Hello", {=: Some *body* text. :=}) .. card("Bye", {=: More. :=})
````

Demo 5: card(title, body) returns a fragment whose heading splices title inline and whose nested block cell splices body. The outer cell uses four backticks because it contains a three-backtick fence, the ordinary markdown rule. View source

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 M

Demo 6 loads it from two cells:

---
title: "Reusable modules"
---

The functions live in `components/cards.lmd`, a Lua file that may hold
markdown fragments. `require` loads it relative to this document, so any
cell on the page can use it:

```{lmd}
local cards = require("components/cards")
return cards.card("Hello", {=[ from a *module* ]=})
```

The module ran once; a second `require` returns the same table. Its
functions take ordinary Lua values, so a list of records becomes a deck
of cards:

```{lmd}
local cards = require("components/cards")
return cards.deck(pandoc.List({
  { title = "One", body = "first" },
  { title = "Two", body = "second" },
}))
```

Demo 6: The module is named without its extension, and a module may require its own neighbours the same way. View source

A few things to know about modules:

  • A module runs once per document. require caches by file, so every cell that requires components/cards gets 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) and FORMAT are all in scope.
  • .lmd wins over .lua. When cards.lmd and cards.lua sit side by side, the .lmd is 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 its require line with Q-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) and load the 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") finds components/cards.lmd at the root of the project however deep the document sits (the same convention components.src uses). 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’s require(quarto.utils.resolve_path(…)) keeps working.
  • An extension, by its name: require("badges/pill") finds _extensions/badges/pill.lmd, and a bare require("badges") is that extension’s init.lmd. A local badges/ directory beside the document wins over an extension of the same name. An extension that exists only to ship modules declares contributes: modules: true in 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 name

The 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:

---
title: Modules across a project
---

Autoloaded from `_quarto.yml`:

```{lmd}
return cards.card("From the project", {=[ a module at the project root ]=})
```

An extension's module, autoloaded by the extension's name:

```{lmd}
return pandoc.Para({ badges.badge("new") })
```

An explicit project-root `require`, whose module requires its neighbour:

```{lmd}
local deck = require("/components/deck")
return deck.deck({ "one", "two" })
```

A parameterised include:

```{lmd}
return quarto.include("partials/callout.lmd", { kind = "tip", body = {=[ from a *partial* ]=} })
```

A markdown partial from the project root:

```{lmd}
return quarto.include("/partials/footer.md")
```

Demo 7: A project with lmd: modules: [components/cards, badges] in _quarto.yml, a components/ directory at the root, a badges extension that ships an init.lmd, and two partials. View source

Conditional content

A cell that returns nil vanishes, so an if around the return is all it takes to include something conditionally:

---
title: "Conditional content"
draft: true
version: 0.4
---

A cell that returns `nil` vanishes, so a condition around `return` is all
it takes:

```{lmd}
if meta.draft then
  return {=:
::: {.callout-warning}
This is a draft of version `{lmd} meta.version`.
:::
:=}
end
```

The same works per format:

```{lmd}
if quarto.doc.is_format("html") then
  return {=: You are reading the HTML version. :=}
end
return {=: You are not reading the HTML version. :=}
```

Demo 8: A callout that appears only because draft: true is in the front matter, and a branch on quarto.doc.is_format. View source

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:

---
title: "A filter on a piece of markdown"
---

A fragment is a `pandoc.Blocks` value, so it has `walk`:

```{lmd}
local frag = {=: Some *text* to shout. :=}
return frag:walk({ Str = function(s) return pandoc.Str(s.text:upper()) end })
```

Demo 9: walk with a Str handler over a fragment: a one-off filter on one piece of markdown. View source

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:

---
title: "Seeing what ran"
---

With `--| echo: true` the cell shows the Lua it ran, fragments rewritten
into `pandoc` constructor calls, above its result:

```{lmd}
--| echo: true
return {=: A paragraph with *emphasis*, built from a fragment. :=}
```

Demo 10: The desugared Lua above the result. --| is the option syntax for lmd cells, as #| is for Python and R cells. View source

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.OrderedList needs explicit pandoc.ListAttributes for 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.