Creating Shortcodes

Overview

Shortcodes are special markdown directives that generate various types of content. Quarto shortcodes are similar in form and function to Hugo shortcodes and WordPress shortcodes.

For example, the following shortcode prints the title from document metadata:

{{< meta title >}}

Quarto supports several shortcodes natively:

Shortcode Description
var Print value from _variables.yml file
meta Print value from document metadata
env Print system environment variable
pagebreak Insert a native page-break
include Include contents of another qmd

This article describes how to create your own shortcodes.

Shortcode Extensions

Before diving into writing custom shortcodes, we’ll talk first about how to distribute your shortcode as a Quarto Extension. While it is possible to use a shortcode by just listing the Lua file in the shortcodes document option, bundling a shortcode as an an extension makes it much easier for others to install and use your shortcode.

Here is what the source code repository of an an extension named shorty might look like:

README.md
LICENSE
example.qmd
_extensions/
  shorty/
    _extension.yml
    shorty.lua

Note that the only thing strictly required is the _extensions directory (anything above that is for your own purposes and is ignored during installation). Even so, it’s good practice to include a README.md and LICENSE file, and the example.qmd will be useful for developing your extension.

Here’s what the contents of the files in _extensions/shorty/ might look like:

_extensions/shorty/_extension.yml
title: Shorty
author: Cooltools
version: 1.0.0
contributes:
  shortcodes:
    - shorty.lua
_extensions/shorty/shorty.lua
function shorty(args)
  return pandoc.Str("shorty!")
end

Finally, the example.qmd file would typically have code that exercises the extension. For example:

example.qmd
---
title: "Shorty Example"
---

{{< shorty >}}

To develop your extension, just make changes to shorty.lua and render example.qmd to test them out.

Distribution

if your extension source code it located within a GitHub repository, then it can be installed by referencing the GitHub organization and repository name. For example:

Terminal
# install the current HEAD of the extension
quarto install extension cooltools/shorty

# install a tagged release of the extension
quarto install extension cooltools/shorty@v1

Note that it is possible to bundle and distribute extensions as simple gzip archives (as opposed to using a GitHub repository as described above). See the article on Distributing Extensions for additional details.

Examples

You might also find it instructive to examine the source code of these shortcode extensions authored by the Quarto team:

Extension Description
fancy-text Output nicely formatted versions of fancy strings such as LaTeX and BibTeX in multiple formats.
fontawesome Use Font Awesome icons in HTML and PDF documents.

Shortcode Basics

You can create your own shortcodes using Lua (the same language used for filters). To get started with developing shortcodes:

  1. Be sure you have a very recent version of Quarto CLI (shortcode extensions require v1.0.15 or greater).
  2. If necessary, brush up on Developing with Lua (Lua is the language used to create shortcodes).
  3. Read the documentation on Pandoc Lua Filters, which describes the Lua extension API for Pandoc.

Custom shortcodes are implemented as Lua functions that take one or more arguments and return a Pandoc AST node (or list of nodes).

Here’s the implementation of the env shortcode that is built in to Quarto:

env.lua
function env(args)
  local var = pandoc.utils.stringify(args[1])
  local value = os.getenv(var)
  if value ~= nil then
    return pandoc.Str(value)
  else
    return pandoc.Null()
  end
end

Note that arguments to shortcodes are provided in args (a 1-based array), and that each argument is a list of Pandoc inlines (i.e. markdown AST parsed from the text).

We use the pandoc.utils.stringify() function to convert the inlines to an ordinary string, and then the os.getenv() function to get its value.

You would use this shortcode as follows:

{{< env HOME >}}

If this shortcode was distributed as an extension (i.e. installed into _extensions/env/ for the current project) then it is available for use without any further declaration. If it’s a plain Lua file not enclosed in an extension then you’ll need to explicitly list it in shortcodes:

shortcodes:
  - env.lua

Below we’ll provide a few a few more examples of custom shortcodes and their implementation. In these examples we’ll assume that the shortcode has been distributed as an extension so won’t use the explicit shortcodes: import shown above.

Example: Raw Output

Shortcodes can tailor their output to the format being rendered to. This is often useful when you want to conditionally generate rich HTML output but still have the same document render properly to PDF or MS Word.

The pagebreak shortcode generates “native” pagebreaks in a variety of formats. Here’s the implementation of pagebreak:

pagebreak.lua
function pagebreak()
 
  local raw = {
    epub = '<p style="page-break-after: always;"> </p>',
    html = '<div style="page-break-after: always;"></div>',
    latex = '\\newpage{}',
    ooxml = '<w:p><w:r><w:br w:type="page"/></w:r></w:p>',
    odt = '<text:p text:style-name="Pagebreak"/>',
    context = '\\page'
  }

  if quarto.doc.isFormat('docx') then
    return pandoc.RawBlock('openxml', raw.ooxml)
  elseif quarto.doc.isFormat('pdf')  then
    return pandoc.RawBlock('tex', raw.latex)
  elseif quarto.doc.isFormat('odt')  then
    return pandoc.RawBlock('opendocument', raw.odt)
  elseif quarto.doc.isFormat('epub') then
    return pandoc.RawBlock('html', raw.epub)
  elseif quarto.doc.isFormat('html') then
    return pandoc.RawBlock('html', raw.html)
  elseif quarto.doc.isFormat('context') then
    return pandoc.RawBlock('context', raw.context)
  else
    -- fall back to insert a form feed character
    return pandoc.Para{pandoc.Str '\f'}
  end

end

We use the pandoc.RawBlock() function to output the appropriate raw content for the target format. Note that raw blocks are passed straight through to the output file and are not processed as markdown.

You’d use this shortcode as follows:

{{< pagebreak >}}

Example: Named Arguments

The examples above use either a single argument (env) or no arguments at all (pagebreak). Here we demonstrate named argument handling by implementing a git-rev shortcode that prints the current git revision, providing a short option to determine whether a short or long SHA1 is displayed:

git.lua
-- run git and read its output
function git(command)
  local p = io.popen("git " .. command)
  local output = p:read('*all')
  p:close()
  return output
end

-- return a table containing shortcode definitions
-- defining shortcodes this way allows us to create helper 
-- functions that are not themselves considered shortcodes 
return {
  ["git-rev"] = function(args, kwargs)
    -- command line args
    local cmdArgs = ""
    local short = pandoc.utils.stringify(kwargs["short"])
    if short == "true" then
      cmdArgs = cmdArgs .. "--short "
    end
    
    -- run the command
    local cmd = "rev-parse " .. cmdArgs .. "HEAD"
    local rev = git(cmd)
    
    -- return as string
    return pandoc.Str(rev)
  end
}

There are some new things demonstrated here :

  1. Rather than defining our shortcode functions globally, we return a table with the shortcode definitions. This allows us to define helper functions that are not themselves registered as shortcodes. It also enables us to define a shortcode with a dash (-) in its name.

  2. There is a new argument to our shortcode handler: kwargs. This holds any named arguments to the shortcode. As with args, values in kwargs will always be a list of Pandoc inlines (allowing you to accept markdown as an argument). Since short is a simple boolean value we need to call pandoc.utils.stringify() to treat it as a string and then compare it to "true".

We’d use this shortcode as follows:

---
title: "My Document"
---

{{< git-rev >}}
{{< git-rev short=true >}}

Example: Metadata Options

In some cases you may want to provide options that affect how you shortcode behaves. There is a third argument to shortcode handlers (meta) that provides access to document and/or project level metadata.

Let’s implement a different version of the git-rev shortcode that emits the revision as a link to GitHub rather than plain text. To do this we make use of github.owner and github.repo metadata values:

git.lua
function git(command)
  local p = io.popen("git " .. command)
  local output = p:read('*all')
  p:close()
  return output
end

return {
  
  ["git-rev"] = function(args, kwargs, meta)
    -- run the command
    local rev = git("rev-parse HEAD")
    
    -- target repo
    local owner = pandoc.utils.stringify(meta["github.owner"])
    local repo = pandoc.utils.stringify(meta["github.repo"])
    local url = "https://github.com/" 
                .. owner .. "/" .. repo .. "/" .. rev 
    
    -- return as link
    return pandoc.Link(pandoc.Str(rev), url)
  end
}

As with args and kwargs, meta values are always provided as a list of Pandoc inlines so often need to be converted to string using pandoc.utils.stringify().

To use this shortcode in a document we provide the GitHub info as document options, then include the shortcode where we want the link to be:

---
title: "My Document"
github:
  owner: quarto-dev
  repo: quarto-cli
---

{{< git-rev >}}

The shortcode registration and GitHub metadata could just as well been provided in a project-level _quarto.yml file or a directory-level _metadata.yml file.

Escaping

If you are writing documentation about using variable shortcodes (for example, this article!) you might need to prevent them from being processed. You can do this in two ways:

  1. Escape the shortcode reference with extra braces like this:

    {{{< var version >}}}
  2. Add a shortcodes=false attribute to any code block you want to prevent processing of shortcodes within:

    ```{shortcodes=false}
    {{< var version >}}
    ```