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
classoption #ID- A unique label which will be passed to the role or directive as the
labeloption. 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 thelabeloption explicitly as a key-value pair.
KEY=LITERAL-VALUEorKEY="RAW VALUE"- A key-value pair to set as named options. A
KEYmust 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:
extending the patterns used in Sphinx internal to the role content (
{name}`content <argument .class1 key="value">`)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:
djot inline-attributes
Sphinx allows you to set role data for an entire page using the
roledirective.Pandoc bracketed spans
MyST Python implementation myst-parser
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.