Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline Options for Roles and Directives #28

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
5 changes: 5 additions & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@ project:
orcid: 0000-0002-0760-5497
affiliation: Curvenote Inc.
email: [email protected]
- id: agoose77
name: Angus Hollands
github: agoose77
orcid: 0000-0003-0788-3814
affiliation: 2i2c
278 changes: 278 additions & 0 deletions meps/mep-0003.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
---
label: MEP0003
tags:
- Active
title: Inline Options for Roles and Directives
date: 2025/01/27
authors:
- rowanc1
- agoose77
short_title: Inline Options
description: An inline syntax for setting options on roles and directives.
data:
discussion: "https://github.com/jupyter-book/myst-enhancement-proposals/pull/28"
abbreviations:
DSL: Domain Specific Language
---

## 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 parametrising role definitions[^def] 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.

[^def]: 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.

## Proposal

We propose extending the MyST Markdown syntax to support parametrisation 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.
```
````

````{code}
:label: new-syntax-example

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

where [](#new-syntax-example) is the new inline attribute syntax.

### Inline Attribute Syntax

The internal inline syntax for specifying content takes inspiration from the prior art in [Pandoc](https://pandoc.org/chunkedhtml-demo/8.18-divs-and-spans.html)/[djot](https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#inline-attributes).

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

```{code} ebnf
:name: ebnf-grammar

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 intepretations 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-value).

(key-value)=
`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 [](#ebnf-grammar)).
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 .classname #uniqueid 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 this mechanisms for providing options, i.e.

````
```{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](https://sparontologies.github.io/cito/current/cito.html)):\
`` {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 stricly 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 extensibilty 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

#### 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
```
````

(bool-attrs)=

#### Short-hand Boolean Attibutes

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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flag values are not part of pandoc/djot: jgm/djot#257

Numbered code directive

Another line
```
````

sets the `linenos` option to `true`.
rowanc1 marked this conversation as resolved.
Show resolved Hide resolved

#### Class Coercion

Similarly to [](#bool-attrs), 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`.

## 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](https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#inline-attributes)
- Sphinx allows you to set role data for an entire page using the [`role` directive](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#inline-code-highlighting).
- Pandoc [bracketed spans](https://pandoc.org/MANUAL.html#extension-bracketed_spans)
- MyST Python implementation [myst-parser](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#attributes)
- https://michelf.ca/projects/php-markdown/extra/