Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Inline Options for Roles and Directives

Authors
Affiliations
Curvenote Inc.
2i2c

Summary

We propose an extension to the MyST Markdown syntax for roles and directives that facilitates inline definition of configuration options.

Context

There is currently no syntax support for parameterizing role definitions[1] in MyST Markdown. This limitation has led to workarounds such as postfix role name conventions that conceptually group together related roles (e.g. cite:ps, cite:t), or the use of DSLs in the role body for parsing multiple values (e.g. {doc}`title <document.md>`). Given that these approaches are both unstandardized and limited in their extensibility, the lack of proper syntax support imposes strong contraints on the utility of roles.

Proposal

We propose extending the MyST Markdown syntax to support parametrization of roles and directives. The existing syntax for defining a role and directive of the form:

{ROLE-NAME}`ROLE-BODY`

and

```{DIRECTIVE-NAME}
DIRECTIVE-BODY
```

will be extended by replacing the {NAME} syntax of specifying role/directive names with a more general named inline attribute syntax of the form {NAME .CLASS #ID KEY=VALUE}. With this new syntax, it will be possible to directly define directive options in an inline manner, e.g. the following are equivalent:

```{tip}
:label: my-tip
:class: dropdown

Content of the tip directive.
```
```{tip #my-tip .dropdown}
Content of the tip directive.
```

Program 1:Proposal for inline options in directives; in this case showing a tip directive with a label and class.

where Program 1 is the new inline attribute syntax.

Inline Attribute Syntax

The internal inline syntax for specifying content takes inspiration from the prior art in Pandoc/djot.

In pseudo-grammar, the new named inline attribute syntax may be expressed as named-attribute-set below

named-attribute-set = "{" { whitespace } name [ var-whitespace attribute { var-whitespace attribute } ] { whitespace } "}";

attribute = identifier | class | key-value;

identifier = "#" attr-name;
class = "." attr-name;
key-value = key "=" value;

name = name-token { name-token };
name-token = letter |  "-" | "_" | ":" | "+" ;

attr-name = attr-name-token { attr-name-token };
attr-name-token = letter | digit | "-" | "_" | ":";

key = key-token { key-token };
key-token = letter | digit | ':' | '-' | '_';

value = safe-value | quoted-value;

safe-value = safe-value-token { safe-value-token };
safe-value-token = key;

quoted-value = '"' quoted-value-token { quoted-value-token } '"';
quoted-value-token = letter | digit | quote-safe-punctuation | escaped-quote | non-escaping-slash;
escaped-quote = "\\" '"';
non-escaping-slash = "\\" ( "\\" | digit | letter | quote-safe-punctuation);

var-whitespace = whitespace { whitespace };
whitespace = " ";

letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z";
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
quote-safe-punctuation = "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | ":" | ";" | "<" | "=" | ">" | "?" | "@" | "[" | "]" | "^" | "_" | "`" | "{" | "|" | "}" | "~";

in the following example of a named attribute set

{NAME .CLASS #ID KEY=VALUE}

the interpretations of NAME, .CLASS, #ID, and KEY=VALUE are thus:

NAME
The name of the role or directive. This must be defined once at the beginning of the attributes definition.
.CLASS
The name of a class to annotate the AST with. This may be (repeatedly) defined. Each defined class will be combined into a single whitespace-separated string and passed to the role or directive as the class option
#ID
A unique label which will be passed to the role or directive as the label option. This may be defined once. If it is repeated, only the final label will be used and a warning will be raised. Users wishing to specify complex identifiers should pass in the label option explicitly as a key-value pair.
KEY=LITERAL-VALUE or KEY="RAW VALUE"
A key-value pair to set as named options. A KEY must be defined once if the role definition marks the it as required, otherwise these may be defined once. Quotes are not needed when a literal is given (see Program 2). Backslash escapes may be used inside quoted values. These values are parsed according to the directive options (e.g. string, markup, number, etc.)

Below is an example showing syntax that includes all extensions proposed here (this could apply to either a role or a directive):

{name #uniqueid .classname key="value"}`some body markup`

Directive Parsing Rules

Directives already support two mutually exclusive mechanisms of defining directive options. The syntax proposed by this PR is intended to complement each mechanism for providing additional inline options, i.e.

```{figure #fig-1} https://foo.com/image
:alt: Simple alt-text

I'm a caption!
```

or for YAML-based options

```{figure #fig-1} https://foo.com/image
---
alt: |
  A long inline alt-text,
  in a figure far, far away ...
---

I'm a caption!
```

Examples

Roles

The new inline attribute syntax is helpful in specifying classes/IDs on elements:
{highlight .red #important-point}`Inline _content_`
or to give additional attributes to complex roles like reasons for a citation (e.g. CiTO):
{cite cito="disputes"}`controversial-ref`.
Other applications include buttons (primary, secondary classes, etc.), inline widgets (sliders, text-boxes, etc.), and evaluation controls (setting number format).

Directives

Directives with only class and label options can be defined more concisely. For example, an example of a tip directive with an identifier and a dropdown class is written as:

```{tip #my-tip .dropdown}
Content of the tip directive.
```

This is a much more concise version of the same syntax for specifying the directive options across multiple lines using the :key: value pairs:

```{tip}
:class: dropdown
:label: my-tip
Content of the tip directive.
```

or using the --- syntax to write the options in YAML:

```{tip}
---
class: dropdown
label: my-tip
---
Content of the tip directive.
```

Each of the above is useful and will continue to be supported; the new inline attribute syntax will complement the existing block-level option specification. For example, the multi-line syntax is helpful for long descriptions such as a caption. The YAML is useful when making those descriptions run over multiple lines themselves and take advantage of other YAML syntax for lists, etc. The single line syntax is much more concise for specifying identifiers and classes.

UX implications & migration

The proposed syntax is an addition to existing role and directive patterns. It is a strictly non-breaking enhancement; existing MyST documents that use this syntax are currently considered invalid and should not successfully parse. It does not invalidate or change the behavior of any existing workflows.

The inline attribute syntax adopts the same inline attribute syntax used by Pandoc, Quarto, and djot, with the exception that the inline attribute must start with the role/directive name. This change follows from adapting the reference syntax to MyST Markdown, which already implements extensibility through nominal types (directives and roles) rather than compositional types (i.e. class names).

Alternatives Considered

We considered other approaches in this proposal and in discussion over several years. Including:

  1. extending the patterns used in Sphinx internal to the role content ({name}`content <argument .class1 key="value">`)

  2. attributes specified after a role in addition to the name ({name}`content`{.class1 key="value"}); and

We chose the current approach to (a) separate the options and content clearly (i.e. not 1), and (b) to keep role names and options close together (i.e. not 2).

Future Syntax Possibilities

These syntax possibilities are not suggested in this MEP, however, they came up in discussions for possible future syntax directions.

Anonymous Roles and Directives

We could provide a short-hand for the div and span roles, where

```{div .red}
Some markup
```

becomes

```{.red}
Some markup
```

Short-hand Boolean Attributes

In certain markup languages, such as React JSX, it is possible to pass boolean true values simply by name, e.g.

<FooComponent myProp />

where props.myProp will be true. We might consider extending this syntax such that:

```{code linenos}
Numbered code directive

Another line
```

sets the linenos option to true.

Class Coercion

Similarly to Short-hand Boolean Attributes, it may be helpful to use the .CLASS attribute to set boolean options. Consider the tip admonition:

```{tip .dropdown .open}
```

Where dropdown is a valid directive boolean option, setting .dropdown could preferentially set dropdown=true, and leave the class option untouched. This is not a syntax-level enhancement, but rather a proposal that would modify the consumer of the markup e.g. mystmd. Currently this decision is up to the implementation of each role or directive.

Questions or Objections Considered

Adopting the proposed syntax opens up a currently impossible way to specify options for roles. The same syntax can also be applied to directives, however, it introduces a third way to specify options for directives. We find that acceptable, as this is for short-hand properties like classes and IDs, which ideally should not take up an extra line.

References

We drew on prior art for inspiration and alignment, including:

Footnotes
  1. Where a role definition is the appearance of the role in a MyST document, rather than the role declaration which specifies its behaviour and interface.