diff --git a/.changeset/afraid-weeks-glow.md b/.changeset/afraid-weeks-glow.md new file mode 100644 index 000000000..fe24749f0 --- /dev/null +++ b/.changeset/afraid-weeks-glow.md @@ -0,0 +1,5 @@ +--- +'myst-common': patch +--- + +Export `BodyDefinition` and `OptionDefinition`. diff --git a/.changeset/brave-seahorses-help.md b/.changeset/brave-seahorses-help.md new file mode 100644 index 000000000..743e69455 --- /dev/null +++ b/.changeset/brave-seahorses-help.md @@ -0,0 +1,10 @@ +--- +'myst-ext-exercise': patch +'myst-ext-reactive': patch +'myst-ext-proof': patch +'myst-ext-card': patch +'myst-ext-grid': patch +'myst-ext-tabs': patch +--- + +Change to use String/Number/Boolean instead of ParsedEnumType diff --git a/.changeset/curly-comics-tell.md b/.changeset/curly-comics-tell.md new file mode 100644 index 000000000..74155793c --- /dev/null +++ b/.changeset/curly-comics-tell.md @@ -0,0 +1,5 @@ +--- +'myst-spec-ext': patch +--- + +Improve the citation node diff --git a/.changeset/empty-feet-hammer.md b/.changeset/empty-feet-hammer.md new file mode 100644 index 000000000..b3636a9a1 --- /dev/null +++ b/.changeset/empty-feet-hammer.md @@ -0,0 +1,5 @@ +--- +'myst-directives': patch +--- + +Move from ParseTypesEnum to String/Number/Boolean in many cases. diff --git a/.changeset/fifty-kangaroos-think.md b/.changeset/fifty-kangaroos-think.md new file mode 100644 index 000000000..803ae9437 --- /dev/null +++ b/.changeset/fifty-kangaroos-think.md @@ -0,0 +1,7 @@ +--- +'myst-ext-card': patch +'myst-ext-tabs': patch +'myst-roles': patch +--- + +Change from alias as string to alias as a string-list. diff --git a/.changeset/great-keys-end.md b/.changeset/great-keys-end.md new file mode 100644 index 000000000..dc4601399 --- /dev/null +++ b/.changeset/great-keys-end.md @@ -0,0 +1,5 @@ +--- +'myst-common': patch +--- + +Add `pluginLoads` ruleId diff --git a/.changeset/odd-pots-peel.md b/.changeset/odd-pots-peel.md new file mode 100644 index 000000000..e42625952 --- /dev/null +++ b/.changeset/odd-pots-peel.md @@ -0,0 +1,6 @@ +--- +'myst-common': patch +'myst-cli': patch +--- + +Add MySTPlugin to common exported types diff --git a/.changeset/perfect-peas-ring.md b/.changeset/perfect-peas-ring.md new file mode 100644 index 000000000..1264f461f --- /dev/null +++ b/.changeset/perfect-peas-ring.md @@ -0,0 +1,5 @@ +--- +'myst-parser': patch +--- + +Support ParseTypesEnum as String/Number/Boolean or `"myst"` diff --git a/.changeset/pretty-olives-fly.md b/.changeset/pretty-olives-fly.md new file mode 100644 index 000000000..d53b6f7c9 --- /dev/null +++ b/.changeset/pretty-olives-fly.md @@ -0,0 +1,5 @@ +--- +'myst-directives': patch +--- + +Document images and figure and iframe directives diff --git a/.changeset/purple-snails-return.md b/.changeset/purple-snails-return.md new file mode 100644 index 000000000..401b3f847 --- /dev/null +++ b/.changeset/purple-snails-return.md @@ -0,0 +1,5 @@ +--- +'myst-roles': patch +--- + +Support additional citation roles diff --git a/.changeset/real-ducks-appear.md b/.changeset/real-ducks-appear.md new file mode 100644 index 000000000..5dda1b245 --- /dev/null +++ b/.changeset/real-ducks-appear.md @@ -0,0 +1,5 @@ +--- +'myst-config': patch +--- + +Allow for plugins in the ProjectConfig diff --git a/.changeset/silver-suits-rush.md b/.changeset/silver-suits-rush.md new file mode 100644 index 000000000..3f7183215 --- /dev/null +++ b/.changeset/silver-suits-rush.md @@ -0,0 +1,7 @@ +--- +'myst-directives': patch +'myst-transforms': patch +'myst-spec-ext': patch +--- + +Add filename to codeblock and include directives diff --git a/.changeset/soft-cups-melt.md b/.changeset/soft-cups-melt.md new file mode 100644 index 000000000..f04abcf27 --- /dev/null +++ b/.changeset/soft-cups-melt.md @@ -0,0 +1,5 @@ +--- +'myst-common': patch +--- + +Only allow `alias` to be a string list, which simplifies the downstream implementations diff --git a/.changeset/soft-singers-warn.md b/.changeset/soft-singers-warn.md new file mode 100644 index 000000000..4480fc988 --- /dev/null +++ b/.changeset/soft-singers-warn.md @@ -0,0 +1,5 @@ +--- +'myst-cli': patch +--- + +Support for loading plugins in the session diff --git a/.changeset/tidy-points-bow.md b/.changeset/tidy-points-bow.md new file mode 100644 index 000000000..7442f4cf0 --- /dev/null +++ b/.changeset/tidy-points-bow.md @@ -0,0 +1,5 @@ +--- +'myst-directives': patch +--- + +Improve documentation for admonition and include diff --git a/.changeset/wild-mayflies-lay.md b/.changeset/wild-mayflies-lay.md new file mode 100644 index 000000000..aa5694534 --- /dev/null +++ b/.changeset/wild-mayflies-lay.md @@ -0,0 +1,5 @@ +--- +'myst-common': patch +--- + +Allow for `ParseTypesEnum` to also be a `Number`, `String` or `Boolean` object or `"myst"` for parsed content. diff --git a/docs/_toc.yml b/docs/_toc.yml index 5db8ac545..46b4da82a 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -45,6 +45,9 @@ parts: - file: creating-pdf-documents - file: creating-word-documents - file: creating-jats-xml + - caption: Extensions + chapters: + - file: plugins - caption: Reference chapters: - file: background @@ -52,5 +55,6 @@ parts: - file: installing-prerequisites - file: commonmark - file: syntax-overview + - file: directives - file: frontmatter - file: glossary diff --git a/docs/admonitions.md b/docs/admonitions.md index 19676f8a1..e1f06f589 100644 --- a/docs/admonitions.md +++ b/docs/admonitions.md @@ -13,7 +13,7 @@ Try changing `tip` to `warning`! ::: ``` -In MyST we call these kind of directives admonitions, however, they are almost always used through their _named_ directives, like `{note}` or `{danger}`. Admonitions can be styled as `simple` or as a `dropdown`, and can optionally hide the icon. There are ten kinds[^docutils-admonitions] of admonitions available: +In MyST we call these kind of directives {myst:directive}`admonitions `, however, they are almost always used through their _named_ directives, like `{note}` or `{danger}`. Admonitions can be styled as `simple` or as a `dropdown`, and can optionally hide the icon using the {myst:directive}`admonition.class` option. There are ten kinds[^docutils-admonitions] of admonitions available: ```{list-table} Named admonitions that can be used as directives :name: admonitions-list @@ -99,8 +99,8 @@ This is an error admonition ## Admonition Titles -All admonitions have a single argument, which is the admonition title and can use markdown. -If a title argument is not supplied the first node is used if it is a `heading` or a paragraph with fully bold text; otherwise the name of the directive is used (e.g. `seealso` becomes `See Also`; `note` becomes `Note`). +All admonitions have a single argument ({myst:directive}`docs `), which is the admonition title and can use markdown. +If a title argument is not supplied the first node of the {myst:directive}`admonition.body` is used if it is a `heading` or a paragraph with fully bold text; otherwise the name of the directive is used (e.g. `seealso` becomes `See Also`; `note` becomes `Note`). ```{myst} :::{tip} Admonition _title_ @@ -110,7 +110,7 @@ Here is an admonition! :::::::{tip} Compatibility with GitHub :class: dropdown -GitHub markdown transforms blockquotes that start with a bold `Note` or text with `[!NOTE]` into a simple admonition (see [GitHub](https://github.com/community/community/discussions/16925)). This syntax only works for `note`, `important` or `warning`. MyST transforms these blockquotes into the appropriate admonitions with a `simple` class. +GitHub markdown transforms blockquotes that start with a bold `Note` or text with `[!NOTE]` into a simple admonition (see [GitHub](https://github.com/community/community/discussions/16925)). This syntax only works for `note`, `important` or `warning`. MyST transforms these blockquotes into the appropriate admonitions with a `simple` {myst:directive}`admonition.class`. ```{myst} > [!NOTE] @@ -164,7 +164,7 @@ This is the body. ## Admonition Dropdown -To turn an admonition into a dropdown, add the `dropdown` class to them. +To turn an admonition into a dropdown, add the `dropdown` {myst:directive}`admonition.class` to them. Dropdown admonitions use the `
` HTML element (meaning they also will work without Javascript!), and they can be helpful when including text that shouldn't immediately visible to your readers. @@ -177,23 +177,5 @@ and they can be helpful when including text that shouldn't immediately visible t :::{seealso} You can also use a `{dropdown}` :class: dropdown -You can also use a `{dropdown}` directive, which provides a more compact writing experience and is simpler in the displayed style. See [](#dropdowns) for more information. +You can also use a {myst:directive}`dropdown` directive, which provides a more compact writing experience and is simpler in the displayed style. See [](#dropdowns) for more information. ::: - -### Reference - -**Arguments** _(markdown)_ -: The `admonition` requires a single argument that is the title, parsed as markdown. - -**Options** -: No options are required - - class _(optional, string)_ - : CSS classes to add to your admonition. Special classes include: - - `dropdown`: turns the admonition into a `
` html element - - `simple`: an admonition with "simple" styles - - the name of an admonition, the first admonition name encountered will be used - : Note that if you provide conflicting class names, the first one in the {ref}`list above ` will be used. - - icon _(optional, boolean)_ - : setting icon to false will hide the icon diff --git a/docs/code.md b/docs/code.md index 735459810..4eaf683bd 100644 --- a/docs/code.md +++ b/docs/code.md @@ -9,7 +9,7 @@ numbering: ```{warning} The code blocks on this page are for **presentation** of code only, they are not executed. -For code execution, see the `{code-cell}` directive in the execution section of the documentation. +For code execution, see the {myst:directive}`code-cell` directive in the execution section of the documentation. ``` You can include code in your documents using the standard markup syntax of ` ```language `, @@ -31,10 +31,10 @@ A list of language names supported by the `myst-react` package is here: [HLJS l ## Code blocks -The above code is not a directive, it is just standard markdown syntax, which cannot add a caption or label. To caption or label blocks of code use the `code-block` directive. +The above code is not a directive, it is just standard markdown syntax, which cannot add a {myst:directive}`code.caption` or {myst:directive}`code.label`. To caption or label blocks of code use the {myst:directive}`code` directive. ````{myst} -```{code-block} python +```{code} python :name: my-program :caption: Creating a TensorMesh using SimPEG from discretize import TensorMesh @@ -50,52 +50,42 @@ In the [](#my-program), we create a mesh for simulation using [SimPEG](https://d ## Numbering and Highlighting -To add numbers and emphasis to lines, we are following the [sphinx](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block) `code-block` directive. You can use `linenos` which is a flag, with no value, and `emphasize-lines` with a comma-separated list of line numbers to emphasize. +To add numbers and emphasis to lines use the {myst:directive}`code` directive. You can use {myst:directive}`code.linenos` which is a flag, with no value, and {myst:directive}`code.emphasize-lines` with a comma-separated list of line numbers to emphasize. -````{code-block} md +````{code} md :linenos: :emphasize-lines: 2,3 :caption: Emphasize lines inside of a `code` block. -```{code-block} +```{code} :linenos: :emphasize-lines: 2,3 ... ```` -You can also set the start number using the `lineno-start` directive, and all emphasized lines will be relative to that number. +You can also set the start number using the {myst:directive}`code.lineno-start` directive, and all emphasized lines will be relative to that number. -## `code-block` reference - -linenos (no value) -: Show line numbers for the code block - -lineno-start (number) -: Set the first line number of the code block. If present, `linenos` option is also automatically activated. -: Default line numbering starts at `1`. - -emphasize-lines (string) -: Emphasize lines of the code block, for example, `1, 2` highlights the first and second lines. -: The line number counting starts at `lineno-start`, which is by default `1`. - -caption (string) -: Add a caption to the code block. +```{tip} Docutils and Sphinx Compatibility +:class: dropdown -name (string) -: The target label for the code-block, can be used by `ref` and `numref` roles. +For full compatibility with Sphinx we suggest using `{code-block}` directive, which is an alias of the {myst:directive}`code` directive. The MyST implementation supports both the Sphinx [`{code-block} directive`](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block) as well as the `docutils` [{code} directive](https://docutils.sourceforge.io/docs/ref/rst/directives.html#code) implementation, which only supports the `number-lines` option. -```{note} Alternative implementations -:class: dropdown +You can use either `code` or `code-block` directive documented above or even a normal markdown code block. +All implementations in MyST are resolved to the same `code` type in the abstract syntax tree. +``` -The parser also supports the `docutils` implementation (see [docutils documentation](https://docutils.sourceforge.io/docs/ref/rst/directives.html#code)) of a `{code}` directive, which only supports the `number-lines` option. +## Showing a Filename -It is recommended to use the more fully featured `code-block` directive documented above, or a simple markdown code block. +Adding a {myst:directive}`code.filename` option will show the name of the file at the top of the code block. For example, `myst.yml` in the following example: -All implementations are resolved to the same `code` type in the abstract syntax tree. +```{code} yaml +:filename: myst.yml +project: + title: Showing Filenames in code-blocks ``` ## Including Files -If your code is in a separate file you can use the `literalinclude` directive (or the `include` directive with the `literal` flag). +If your code is in a separate file you can use the {myst:directive}`literalinclude` directive (or the {myst:directive}`include` directive with the {myst:directive}`include.literal` flag). This directive is helpful for showing code snippets without duplicating your content. For example, a `literalinclude` of a snippet of the `myst.yml` such as: @@ -117,45 +107,15 @@ creates a snippet that has matching line numbers, and starts at a line including ``` :::{note} Auto Reload -If you are working with the auto-reload (e.g. `myst start`), currently you will need to save the file with the `literalinclude` directive for the contents to update.code for the contents to update. +If you are working with the auto-reload (e.g. `myst start`), currently you will need to save the file with the {myst:directive}`literalinclude` directive for the contents to update.code for the contents to update. ::: -## `include` Reference - -The argument of an include directive is the file path, relative to the file from which it was referenced. -By default the file will be parsed using MyST, you can also set the file to be `literal`, which will show as a code-block; this is the same as using the `literalinclude` directive. -If in literal mode, the directive also accepts all of the options from the `code-block` (e.g. `:linenos:`). - -To select a portion of the file to be shown using the `start-at`/`start-after` selectors with the `end-before`/`end-at`, which use a snippet of included text. -Alternatively, you can explicitly select the lines (e.g. `1,3,5-10,20-`) or the `start-line`/`end-line` (which is zero based for compatibility with Sphinx). - -literal (boolean) -: Flag the include block as literal, and show the contents as a code block. This can also be set automatically by setting the `language` or using the `literalinclude` directive. - -lang (string) -: The language of the code to be highlighted as. If set, this automatically changes an `include` into a `literalinclude`. -: You can alias this as `language` or `code` - -start-line (number) -: Only the content starting from this line will be included. The first line has index 0 and negative values count from the end. - -start-at (string) -: Only the content after and including the first occurrence of the specified text in the external data file will be included. - -start-after (string) -: Only the content after the first occurrence of the specified text in the external data file will be included. - -end-line (number) -: Only the content up to (but excluding) this line will be included. - -end-at (string) -: Only the content up to and including the first occurrence of the specified text in the external data file (but after any start-after text) will be included. +The argument of an include directive is the file path ({myst:directive}`docs `), which is relative to the file from which it was referenced. +By default the file will be parsed using MyST, you can also set the file to be {myst:directive}`include.literal`, which will show as a code-block; this is the same as using the {myst:directive}`literalinclude` directive. -end-before (string) -: Only the content before the first occurrence of the specified text in the external data file (but after any start-after text) will be included. +If in {myst:directive}`include.literal` mode, the directive also accepts all of the options from the `code-block` (e.g. {myst:directive}`include.linenos`). +To select a portion of the file to be shown using the {myst:directive}`include.start-at`/{myst:directive}`include.start-after` selectors with the {myst:directive}`include.end-before`/{myst:directive}`include.end-at`, which use a snippet of included text. -lines (string) -: Specify exactly which lines to include from the original file, starting at 1. For example, `1,3,5-10,20-` includes the lines 1, 3, 5 to 10 and lines 20 to the last line of the original file. +Alternatively, you can explicitly select the lines (e.g. `1,3,5-10,20-`) or the {myst:directive}`include.start-line`/{myst:directive}`include.end-line` (which is zero based for compatibility with Sphinx). -lineno-match (boolean) -: Display the original line numbers, correct only when the selection consists of contiguous lines. +The include directive is based on [RST](https://docutils.sourceforge.io/docs/ref/rst/directives.html#including-an-external-document-fragment) and [Sphinx](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude). diff --git a/docs/directives.md b/docs/directives.md new file mode 100644 index 000000000..5812e1d52 --- /dev/null +++ b/docs/directives.md @@ -0,0 +1,58 @@ +--- +title: Directives +description: Code and code-blocks can be used to show programming languages. +--- + +:::{myst:directive} admonition +::: + +:::{myst:directive} bibliography +::: + +:::{myst:directive} code +::: + +:::{myst:directive} code-cell +::: + +:::{myst:directive} dropdown +::: + +:::{myst:directive} embed +::: + +:::{myst:directive} figure +::: + +:::{myst:directive} glossary +::: + +:::{myst:directive} iframe +::: + +:::{myst:directive} image +::: + +:::{myst:directive} include +::: + +:::{myst:directive} list-table +::: + +:::{myst:directive} margin +::: + +:::{myst:directive} math +::: + +:::{myst:directive} mdast +::: + +:::{myst:directive} mermaid +::: + +:::{myst:directive} myst +::: + +:::{myst:directive} output +::: diff --git a/docs/directives.mjs b/docs/directives.mjs new file mode 100644 index 000000000..f6c37b9fc --- /dev/null +++ b/docs/directives.mjs @@ -0,0 +1,154 @@ +import { u } from 'unist-builder'; +import { mystParse } from 'myst-parser'; +import { defaultDirectives } from 'myst-directives'; +import { fileError } from 'myst-common'; + +export const plugin = { + name: 'MyST Documentation Plugins', + author: 'Rowan Cockett', + license: 'MIT', +}; + +/** + * @param {import('myst-common').OptionDefinition} option + */ +function type2string(option) { + if (option.type === 'string' || option.type === String) return 'string'; + if (option.type === 'number' || option.type === Number) return 'number'; + if (option.type === 'boolean' || option.type === Boolean) return 'boolean'; + if (option.type === 'parsed' || option.type === 'myst') return 'parsed'; + return ''; +} + +function createOption(directive, optName, option) { + if (!option) return []; + const optType = type2string(option); + const def = [ + u('definitionTerm', { identifier: `directive-${directive.name}-${optName}` }, [ + u('strong', [ + u( + 'text', + optName === 'arg' + ? 'Directive Argument' + : optName === 'body' + ? 'Directive Body' + : optName, + ), + ]), + ...(optType + ? [ + u('text', ' ('), + u('emphasis', [u('text', `${optType}${option.required ? ', required' : ''}`)]), + u('text', ')'), + ] + : []), + ]), + u( + 'definitionDescription', + option.doc ? mystParse(option.doc).children : [u('text', 'No description')], + ), + ]; + if (option.alias && option.alias.length > 0) { + def.push( + u('definitionDescription', [ + u('strong', [u('text', 'Alias')]), + u('text', ': '), + ...option.alias + .map((a, i) => { + const c = [u('inlineCode', a)]; + if (i < option.alias.length - 1) c.push(u('text', ', ')); + return c; + }) + .flat(), + ]), + ); + } + return def; +} + +/** + * Create a documentation section for a directive + * + * @type {import('myst-common').DirectiveSpec} + */ +const mystDirective = { + name: 'myst:directive', + arg: { + type: String, + required: true, + }, + run(data, vfile) { + const name = data.arg; + const directive = defaultDirectives.find((d) => d.name === name); + if (!directive) { + fileError(vfile, `myst:directive: Unknown myst directive "${name}"`); + return []; + } + const heading = u('heading', { depth: 2, identifier: `directive-${name}` }, [ + u('inlineCode', name), + u('text', ' directive'), + ]); + const doc = directive.doc ? mystParse(directive.doc).children : []; + let alias = []; + if (directive.alias && directive.alias.length > 0) { + alias = [ + u('paragraph', [ + u('strong', [u('text', 'Alias')]), + u('text', ': '), + ...directive.alias + .map((a, i) => { + const c = [u('inlineCode', a)]; + if (i < directive.alias.length - 1) c.push(u('text', ', ')); + return c; + }) + .flat(), + ]), + ]; + } + const options = Object.entries(directive.options ?? {}) + .map(([optName, option]) => createOption(directive, optName, option)) + .flat(); + const list = u('definitionList', [ + ...createOption(directive, 'arg', directive.arg), + ...createOption(directive, 'body', directive.body), + u('definitionTerm', { identifier: `directive-${directive.name}-opts` }, [ + u('strong', [u('text', 'Options')]), + ]), + options.length > 0 + ? u('definitionDescription', [u('definitionList', options)]) + : u('definitionDescription', [u('text', 'No options')]), + ]); + return [heading, u('div', [...doc, ...alias]), list]; + }, +}; + +const REF_PATTERN = /^(.+?)<([^<>]+)>$/; // e.g. 'Labeled Reference ' + +/** + * Create a documentation section for a directive + * + * @type {import('myst-common').RoleSpec} + */ +const mystDirectiveRole = { + name: 'myst:directive', + body: { + type: String, + required: true, + }, + run(data) { + const match = REF_PATTERN.exec(data.body); + const [, modified, rawLabel] = match ?? []; + const label = rawLabel ?? data.body; + const [name, opt] = label?.split('.') ?? []; + const directive = defaultDirectives.find((d) => d.name === name || d.alias?.includes(name)); + const identifier = opt + ? `directive-${directive?.name ?? name}-${opt}` + : `directive-${directive?.name ?? name}`; + return [ + u('crossReference', { identifier }, [u('inlineCode', modified?.trim() || opt || name)]), + ]; + }, +}; + +export const directives = [mystDirective]; +export const roles = [mystDirectiveRole]; diff --git a/docs/figures.md b/docs/figures.md index 00447c4b3..3160cb6a1 100644 --- a/docs/figures.md +++ b/docs/figures.md @@ -17,15 +17,15 @@ The simplest way to create an image is to use the standard Markdown syntax: You can explore a [demo of images](#md:image) in the discussion of [](./commonmark.md) features of MyST. -Using standard markdown to create an image will render across all output formats (HTML, TeX, Word, PDF, etc). However, this markdown syntax is limited in the configuration that can be applied beyond `alt` text and an optional `title`. For example, the image width, alignment or a figure caption cannot be set with this syntax. +Using standard markdown to create an image will render across all output formats (HTML, TeX, Word, PDF, etc). However, this markdown syntax is limited in the configuration that can be applied beyond `alt` text and an optional `title`. -There are two directives that can be used to add additional information about the layout and metadata associated with an image. +There are two directives that can be used to add additional information about the layout and metadata associated with an image. For example, {myst:directive}`image.width`, {myst:directive}`alignment ` or a {myst:directive}`figure caption `. **image** -: The `image` directive allows you to customize width, alignment, and other classes to add to the image +: The {myst:directive}`image` directive allows you to customize {myst:directive}`image.width`, {myst:directive}`alignment `, and other {myst:directive}`classes ` to add to the image **figure** -: The `figure` directive can contain a figure caption and allows you to cross-reference this in other parts of your document. +: The {myst:directive}`figure` directive can contain a {myst:directive}`figure caption ` and allows you to cross-reference this in other parts of your document. (image-directive)= @@ -167,7 +167,7 @@ If you want to help out with this feature, please get in touch! ## YouTube Videos -If your video is on a platform like YouTube or Vimeo, you can use the `{iframe}` directive that takes the URL of the video. +If your video is on a platform like YouTube or Vimeo, you can use the {myst:directive}`iframe` directive that takes the URL of the video. ```{myst} :::{iframe} https://www.youtube.com/embed/F3st8X0L1Ys @@ -176,4 +176,4 @@ Get up and running with MyST in Jupyter! ::: ``` -You can find this URL when clicking share > embed on various platforms. You can also give the `{iframe}` directive `width` and a `caption`. +You can find this URL when clicking share > embed on various platforms. You can also give the {myst:directive}`iframe` directive {myst:directive}`iframe.width` and a {myst:directive}`caption `. diff --git a/docs/myst.yml b/docs/myst.yml index 807b67c29..23ce3261e 100644 --- a/docs/myst.yml +++ b/docs/myst.yml @@ -44,6 +44,9 @@ project: JSON: JavaScript Object Notation TLA: Three Letter Acronym OA: Open Access + plugins: + - directives.mjs + - unsplash.mjs site: title: MyST Markdown Guide domains: diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 000000000..e5049b868 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,80 @@ +--- +title: Plugins +description: Plugins provide powerful ways to extend and customize MyST +--- + +Plugins provide powerful ways to extend and customize MyST by adding directives and roles or specific transformations on the AST that can download or augment your articles and documentation. These plugins also support custom rendering and transformation pipelines for various export targets including React, HTML, LaTeX, and Microsoft Word. + +:::{danger} Plugins are incomplete and in Beta +The interfaces and packaging for the plugins may change substantially in the future.\ +**Expect changes!!** + +If you are implementing a plugin, please let us know on [GitHub](https://github.com/executablebooks/mystmd) or [Discord](https://discord.mystmd.org/). +::: + +## Overview of a Plugin + +Plugins are executable javascript code that can modify a document source. The supported plugin types are: + +directives +: Add or overwrite directives, which provide a "block-level" extension point. +: For example, create a `:::{proof}` extension that allows for numbered proofs. + +roles +: Add or overwrite roles, which provide an inline extension point. +: For example, create a role for showing units, `` {si}`4 kg per meter squared` ``. + +:::{warning} Planned - Not Yet Implemented +:class: dropdown +transforms +: These plugins transform the document source while it is rendered. +: For example, add metadata or transform a link to a DOI. + +renderers +: These plugins add handlers for various nodes when exporting to a specific format. +: For example, do something special for node in HTML, React, Microsoft Word, or LaTeX. +::: + +## Creating a Plugin + +To create a plugin, you will need a single Javascript file[^esm] that exports one or more of the objects above. For example, a simple directive that pulls a random image from [Unsplash](https://unsplash.com/) can be created with a single file that exports an `unsplash` directive. + +[^esm]: The format of the Javascript should be an ECMAScript modules, not CommonJS. This means it uses `import` statements rather than `require()` and is the most modern style of Javascript. + +:::{literalinclude} unsplash.mjs +:filename: unsplash.mjs +:caption: A plugin to add an `unsplash` directive that includes a beautiful, random picture based on a query string. +::: + +This code should be referenced from your `myst.yml` under the `projects.plugins`: + +```{code} yaml +:filename: myst.yml +project: + plugins: + - unsplash.mjs +``` + +Then start or build your document using `myst start` or `myst build`, and you will see that the plugin is loaded. + +```text +myst start +... +🔌 Unnamed Plugin (unsplash.mjs) loaded: 1 directive(s), 0 role(s) +... +``` + +You can now use the directive, for example: + +```markdown +:::{unsplash} misty,mountains +::: +``` + +:::{unsplash} misty,mountains +:size: 600x250 +::: + +If you change the source code you will have to stop and re-start the server to see the results. + +The types are defined in `myst-common` ([npm](https://www.npmjs.com/package/myst-common), [github](https://github.com/executablebooks/mystmd/tree/main/packages/myst-common)) with the [`DirectiveSpec`](https://github.com/executablebooks/mystmd/blob/9965925030c3fab6f34c20d11eeee7ffdafa73df/packages/myst-common/src/types.ts#L68-L77) and [`RoleSpec`](https://github.com/executablebooks/mystmd/blob/9965925030c3fab6f34c20d11eeee7ffdafa73df/packages/myst-common/src/types.ts#L79-L85) being the main types to implement. diff --git a/docs/unsplash.mjs b/docs/unsplash.mjs new file mode 100644 index 000000000..6b33aae8b --- /dev/null +++ b/docs/unsplash.mjs @@ -0,0 +1,18 @@ +const unsplashDirective = { + name: 'unsplash', + doc: 'An example directive for showing a nice random image at a custom size.', + alias: ['random-pic'], + arg: { type: String, doc: 'The kinds of images to search for, e.g., `fruit`' }, + options: { + size: { type: String, doc: 'Size of the image, for example, `500x200`.' }, + }, + run(data) { + const query = data.arg; + const size = data.options.size || '500x200'; + const url = `https://source.unsplash.com/random/${size}/?${query}`; + const img = { type: 'image', url }; + return [img]; + }, +}; + +export const directives = [unsplashDirective]; diff --git a/package-lock.json b/package-lock.json index 92227516c..24028fce0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7732,11 +7732,35 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/markdown-it-amsmath": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-amsmath/-/markdown-it-amsmath-0.4.0.tgz", + "integrity": "sha512-wWGX5bpHu9iq4cqi/U58srCH0js5ws7X3BrQZcN2/anQ9S4P8MpvTxHLCZC2rmGHA6mSZf4PKWF6caBc+nxH2g==", + "engines": { + "node": ">=16", + "npm": ">=6" + }, + "peerDependencies": { + "markdown-it": "^12 || ^13" + } + }, "node_modules/markdown-it-deflist": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz", "integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==" }, + "node_modules/markdown-it-dollarmath": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/markdown-it-dollarmath/-/markdown-it-dollarmath-0.5.0.tgz", + "integrity": "sha512-W+8se6cx6vowjzRAfDbHDPBQ+Y9G/8M/JZSFiaYvT0HqfwyFK1hIA1Xj360Nc3ymknKgdDVoL5fI8XjAqZF3tg==", + "engines": { + "node": ">=16", + "npm": ">=6" + }, + "peerDependencies": { + "markdown-it": "^12 || ^13" + } + }, "node_modules/markdown-it-footnote": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz", @@ -7751,6 +7775,18 @@ "resolved": "packages/markdown-it-myst", "link": true }, + "node_modules/markdown-it-myst-extras": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/markdown-it-myst-extras/-/markdown-it-myst-extras-0.3.0.tgz", + "integrity": "sha512-678qviK97MEzSM9Hr0jlX5nBPzMcKZo6Ixgh4nEf/WYpii8LXQ72FametoXkzyDy77qNKDE3vlqYhqfbbCGHrw==", + "engines": { + "node": ">=16", + "npm": ">=6" + }, + "peerDependencies": { + "markdown-it": "^12 || ^13" + } + }, "node_modules/markdown-it-task-lists": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", @@ -13372,6 +13408,7 @@ "commander": "^10.0.1", "js-yaml": "^4.1.0", "myst-cli-utils": "^2.0.3", + "myst-common": "^1.1.5", "myst-frontmatter": "^1.1.5", "myst-templates": "^1.0.8", "node-fetch": "^3.3.1", @@ -13690,13 +13727,13 @@ "dependencies": { "he": "^1.2.0", "markdown-it": "^12.3.2", - "markdown-it-amsmath": "^0.3.1", + "markdown-it-amsmath": "^0.4.0", "markdown-it-deflist": "^2.1.0", - "markdown-it-dollarmath": "^0.4.2", + "markdown-it-dollarmath": "^0.5.0", "markdown-it-footnote": "^3.0.3", "markdown-it-front-matter": "^0.2.3", "markdown-it-myst": "1.0.2", - "markdown-it-myst-extras": "0.2.0", + "markdown-it-myst-extras": "0.3.0", "markdown-it-task-lists": "^2.1.1", "myst-common": "^1.1.6", "myst-directives": "^1.0.7", @@ -13752,42 +13789,6 @@ "markdown-it": "bin/markdown-it.js" } }, - "packages/myst-parser/node_modules/markdown-it-amsmath": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/markdown-it-amsmath/-/markdown-it-amsmath-0.3.1.tgz", - "integrity": "sha512-QMYopYAQHzSfOSCUIS8g84piGvmQ+GXkcfzR1J41UgxvIKPrKTcI7oRRLO/vD390JUokR7V04+jN1djeMhqBHg==", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "markdown-it": "^12.3.2" - } - }, - "packages/myst-parser/node_modules/markdown-it-dollarmath": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/markdown-it-dollarmath/-/markdown-it-dollarmath-0.4.2.tgz", - "integrity": "sha512-2qgZEyMDgW+ysfsit41N4Xcenc71RIiwNNhhJPMUQG8KX0RO+lt8PHibkD4gDrjbU/ExRbZIN112l3nQPFlr7A==", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "markdown-it": "^12.3.2" - } - }, - "packages/myst-parser/node_modules/markdown-it-myst-extras": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/markdown-it-myst-extras/-/markdown-it-myst-extras-0.2.0.tgz", - "integrity": "sha512-DKVCFDsSvT/AjlIEtWuOXjQmlgT5K7CP5XFa8nbcdGxsbmRKuHczEs+vo7u53L2Ae1tBVPZmDArK8jqlsMNbfw==", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "markdown-it": "^12.3.2" - } - }, "packages/myst-roles": { "version": "1.0.7", "license": "MIT", diff --git a/packages/myst-cli/package.json b/packages/myst-cli/package.json index ec2198158..1f5723d19 100644 --- a/packages/myst-cli/package.json +++ b/packages/myst-cli/package.json @@ -34,7 +34,7 @@ "lint:format": "npm run copy:version; prettier --check \"src/**/*.ts\"", "test": "npm run copy:version; vitest run", "test:watch": "npm run copy:version; vitest watch", - "build:esm": "tsc --project ./tsconfig.json --module es2015 --outDir dist --declaration", + "build:esm": "tsc --project ./tsconfig.json --module esnext --outDir dist --declaration", "build": "npm-run-all -l clean copy:version -p build:esm" }, "engines": { diff --git a/packages/myst-cli/src/config.ts b/packages/myst-cli/src/config.ts index 8ef2f3321..722b6ce72 100644 --- a/packages/myst-cli/src/config.ts +++ b/packages/myst-cli/src/config.ts @@ -172,6 +172,13 @@ function resolveProjectConfigPaths( return resolutionFn(session, path, file); }); } + if (projectConfig.plugins) { + resolvedFields.plugins = projectConfig.plugins.map((file) => { + const resolved = resolutionFn(session, path, file); + if (fs.existsSync(resolved)) return resolved; + return file; + }); + } return { ...projectConfig, ...resolvedFields }; } diff --git a/packages/myst-cli/src/plugins.ts b/packages/myst-cli/src/plugins.ts new file mode 100644 index 000000000..14894cab3 --- /dev/null +++ b/packages/myst-cli/src/plugins.ts @@ -0,0 +1,64 @@ +import fs from 'node:fs'; +import type { ISession } from './session/types.js'; +import { selectCurrentProjectConfig } from './store/selectors.js'; +import { RuleId, type MystPlugin } from 'myst-common'; +import { addWarningForFile } from './utils/addWarningForFile.js'; + +export async function loadPlugins(session: ISession): Promise { + const config = selectCurrentProjectConfig(session.store.getState()); + + const plugins: MystPlugin = { + directives: [], + roles: [], + }; + if (!config?.plugins || config.plugins.length === 0) { + return plugins; + } + session.log.debug(`Loading plugins: "${config?.plugins?.join('", "')}"`); + const modules = await Promise.all( + config?.plugins?.map(async (filename) => { + if (!fs.statSync(filename).isFile || !filename.endsWith('.mjs')) { + addWarningForFile( + session, + filename, + `Unknown plugin "${filename}", it must be an mjs file`, + 'error', + { + ruleId: RuleId.pluginLoads, + }, + ); + return null; + } + let module: any; + try { + module = await import(filename); + } catch (error) { + session.log.debug(`\n\n${(error as Error)?.stack}\n\n`); + addWarningForFile(session, filename, `Error reading plugin: ${error}`, 'error', { + ruleId: RuleId.pluginLoads, + }); + return null; + } + return { filename, module }; + }), + ); + modules.forEach((plugin) => { + if (!plugin) return; + const pluginConfig = plugin.module.plugin; + session.log.info( + `🔌 ${pluginConfig?.name ?? 'Unnamed Plugin'} (${plugin.filename}) loaded: ${ + plugin.module.directives?.length ?? 0 + } directive(s), ${plugin.module.roles?.length ?? 0} role(s)`, + ); + if (plugin.module.directives) { + // TODO: validate each directive + plugins.directives.push(...plugin.module.directives); + } + if (plugin.module.roles) { + // TODO: validate each role + plugins.roles.push(...plugin.module.roles); + } + }); + session.log.debug('Plugins loaded'); + return plugins; +} diff --git a/packages/myst-cli/src/process/file.ts b/packages/myst-cli/src/process/file.ts index 5bb7836ae..84851d4da 100644 --- a/packages/myst-cli/src/process/file.ts +++ b/packages/myst-cli/src/process/file.ts @@ -30,6 +30,7 @@ export async function loadFile( extension?: '.md' | '.ipynb' | '.bib', opts?: { minifyMaxCharacters?: number }, ) { + await session.loadPlugins(); const toc = tic(); session.store.dispatch(warnings.actions.clearWarnings({ file })); const cache = castSession(session); diff --git a/packages/myst-cli/src/process/myst.ts b/packages/myst-cli/src/process/myst.ts index dd759041e..b16c3ae14 100644 --- a/packages/myst-cli/src/process/myst.ts +++ b/packages/myst-cli/src/process/myst.ts @@ -22,8 +22,9 @@ export function parseMyst(session: ISession, content: string, file: string): Gen proofDirective, ...exerciseDirectives, ...tabDirectives, + ...(session.plugins?.directives ?? []), ], - roles: [reactiveRole], + roles: [reactiveRole, ...(session.plugins?.roles ?? [])], vfile, }); logMessagesFromVFile(session, vfile); diff --git a/packages/myst-cli/src/session/session.ts b/packages/myst-cli/src/session/session.ts index 3175068f1..56f353de0 100644 --- a/packages/myst-cli/src/session/session.ts +++ b/packages/myst-cli/src/session/session.ts @@ -15,6 +15,8 @@ import latestVersion from 'latest-version'; import boxen from 'boxen'; import chalk from 'chalk'; import version from '../version.js'; +import { loadPlugins } from '../plugins.js'; +import type { MystPlugin } from 'myst-common'; const CONFIG_FILES = ['myst.yml']; const API_URL = 'https://api.mystmd.org'; @@ -100,6 +102,18 @@ export class Session implements ISession { return this; } + plugins: MystPlugin | undefined; + + _pluginPromise: Promise | undefined; + + async loadPlugins() { + // Early return if a promise has already been initiated + if (this._pluginPromise) return this._pluginPromise; + this._pluginPromise = loadPlugins(this); + this.plugins = await this._pluginPromise; + return this.plugins; + } + buildPath(): string { const state = this.store.getState(); const sitePath = selectors.selectCurrentSitePath(state); diff --git a/packages/myst-cli/src/session/types.ts b/packages/myst-cli/src/session/types.ts index 0635fc218..bbff6f518 100644 --- a/packages/myst-cli/src/session/types.ts +++ b/packages/myst-cli/src/session/types.ts @@ -7,6 +7,7 @@ import type { Store } from 'redux'; import type { RootState } from '../store/index.js'; import type { PreRendererData, RendererData, SingleCitationRenderer } from '../transforms/types.js'; +import type { MystPlugin } from 'myst-common'; export type ISession = { API_URL: string; @@ -20,6 +21,8 @@ export type ISession = { contentPath(): string; publicPath(): string; showUpgradeNotice(): void; + plugins: MystPlugin | undefined; + loadPlugins(): Promise; }; export type ISessionWithCache = ISession & { diff --git a/packages/myst-cli/src/transforms/citations.ts b/packages/myst-cli/src/transforms/citations.ts index e04f85a59..80161379f 100644 --- a/packages/myst-cli/src/transforms/citations.ts +++ b/packages/myst-cli/src/transforms/citations.ts @@ -1,7 +1,7 @@ import type { CitationRenderer } from 'citation-js-utils'; import { InlineCite } from 'citation-js-utils'; -import type { GenericParent, References } from 'myst-common'; -import type { StaticPhrasingContent, Parent } from 'myst-spec'; +import type { GenericNode, GenericParent, References } from 'myst-common'; +import type { StaticPhrasingContent } from 'myst-spec'; import type { Cite } from 'myst-spec-ext'; import { selectAll } from 'unist-util-select'; @@ -46,7 +46,7 @@ function addCitationChildren(cite: Cite, renderer: CitationRenderer): boolean { return true; } -function hasChildren(node: Parent) { +function hasChildren(node: GenericNode) { return node.children && node.children.length > 0; } diff --git a/packages/myst-common/src/index.ts b/packages/myst-common/src/index.ts index 1ba0c2f3b..30b1a55ae 100644 --- a/packages/myst-common/src/index.ts +++ b/packages/myst-common/src/index.ts @@ -25,9 +25,12 @@ export type { Citations, References, ArgDefinition, + BodyDefinition, + OptionDefinition, DirectiveData, RoleData, DirectiveSpec, RoleSpec, ParseTypes, + MystPlugin, } from './types.js'; diff --git a/packages/myst-common/src/ruleids.ts b/packages/myst-common/src/ruleids.ts index 60c8bae0e..60515e6dc 100644 --- a/packages/myst-common/src/ruleids.ts +++ b/packages/myst-common/src/ruleids.ts @@ -92,4 +92,6 @@ export enum RuleId { sourceFileCopied = 'source-file-copied', templateFileCopied = 'template-file-copied', staticActionFileCopied = 'static-action-file-copied', + // Plugins + pluginLoads = 'plugin-loads', } diff --git a/packages/myst-common/src/types.ts b/packages/myst-common/src/types.ts index 8373d576a..8361cf195 100644 --- a/packages/myst-common/src/types.ts +++ b/packages/myst-common/src/types.ts @@ -42,14 +42,14 @@ export enum ParseTypesEnum { export type ParseTypes = string | number | boolean | GenericNode[]; export type ArgDefinition = { - type: ParseTypesEnum; + type: ParseTypesEnum | typeof Boolean | typeof String | typeof Number | 'myst'; required?: boolean; doc?: string; }; -type BodyDefinition = ArgDefinition; +export type BodyDefinition = ArgDefinition; -type OptionDefinition = ArgDefinition & { +export type OptionDefinition = ArgDefinition & { alias?: string[]; }; @@ -67,7 +67,7 @@ export type RoleData = { export type DirectiveSpec = { name: string; - alias?: string | string[]; + alias?: string[]; doc?: string; arg?: ArgDefinition; options?: Record; @@ -78,12 +78,20 @@ export type DirectiveSpec = { export type RoleSpec = { name: string; - alias?: string | string[]; + alias?: string[]; body?: BodyDefinition; validate?: (data: RoleData, vfile: VFile) => RoleData; run: (data: RoleData, vfile: VFile) => GenericNode[]; }; +/** + * Create MyST plugins that export this from a file, + * or combine multiple plugins to a single object. */ +export type MystPlugin = { + directives: DirectiveSpec[]; + roles: RoleSpec[]; +}; + export enum TargetKind { heading = 'heading', equation = 'equation', diff --git a/packages/myst-config/src/project/types.ts b/packages/myst-config/src/project/types.ts index 55aa59498..a9b573a44 100644 --- a/packages/myst-config/src/project/types.ts +++ b/packages/myst-config/src/project/types.ts @@ -6,4 +6,5 @@ export type ProjectConfig = ProjectFrontmatter & { remote?: string; index?: string; exclude?: string[]; + plugins?: string[]; }; diff --git a/packages/myst-config/src/project/validators.ts b/packages/myst-config/src/project/validators.ts index 06411f54f..a2f4133f5 100644 --- a/packages/myst-config/src/project/validators.ts +++ b/packages/myst-config/src/project/validators.ts @@ -10,7 +10,7 @@ import { import type { ProjectConfig } from './types.js'; const PROJECT_CONFIG_KEYS = { - optional: ['remote', 'index', 'exclude'].concat(PROJECT_FRONTMATTER_KEYS), + optional: ['remote', 'index', 'exclude', 'plugins'].concat(PROJECT_FRONTMATTER_KEYS), alias: { jupyter: 'thebe', author: 'authors', @@ -37,6 +37,15 @@ function validateProjectConfigKeys(value: Record, opts: ValidationO }, ); } + if (defined(value.plugins)) { + output.plugins = validateList( + value.plugins, + incrementOptions('plugins', opts), + (file, index: number) => { + return validateString(file, incrementOptions(`plugins.${index}`, opts)); + }, + ); + } return output; } diff --git a/packages/myst-directives/src/admonition.ts b/packages/myst-directives/src/admonition.ts index f4a2f5aff..6b8464fc5 100644 --- a/packages/myst-directives/src/admonition.ts +++ b/packages/myst-directives/src/admonition.ts @@ -1,9 +1,9 @@ import type { Admonition } from 'myst-spec-ext'; import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const admonitionDirective: DirectiveSpec = { name: 'admonition', + doc: 'Callouts, or "admonitions", highlight a particular block of text that exists slightly apart from the narrative of your page, such as a note or a warning. \n\n The admonition kind can be determined by the directive name used (e.g. `:::{tip}` or `:::{note}`).', alias: [ 'attention', 'caution', @@ -22,24 +22,31 @@ export const admonitionDirective: DirectiveSpec = { '.callout-caution', ], arg: { - type: ParseTypesEnum.parsed, + type: 'myst', + doc: 'The optional title of the admonition, if not supplied the admonition kind will be used.\n\nNote that the argument parsing is different from Sphinx, which does not allow named admonitions to have custom titles.', }, options: { // label: { - // type: ParseTypesEnum.string, + // type: String, // alias: ['name'], // }, class: { - type: ParseTypesEnum.string, - // class_option: list of strings? + type: String, + doc: `CSS classes to add to your admonition. Special classes include: + +- \`dropdown\`: turns the admonition into a \`
\` html element +- \`simple\`: an admonition with "simple" styles +- the name of an admonition, the first valid admonition name encountered will be used (e.g. \`tip\`). Note that if you provide conflicting class names, the first valid admonition name will be used.`, }, icon: { - type: ParseTypesEnum.boolean, + type: Boolean, + doc: 'Setting icon to false will hide the icon.', // class_option: list of strings? }, }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', + doc: 'The body of the admonition. If there is no title and the body starts with bold text or a heading, that content will be used as the admonition title.', }, run(data: DirectiveData): GenericNode[] { const children: GenericNode[] = []; diff --git a/packages/myst-directives/src/bibliography.ts b/packages/myst-directives/src/bibliography.ts index 96944f433..5ea3c393c 100644 --- a/packages/myst-directives/src/bibliography.ts +++ b/packages/myst-directives/src/bibliography.ts @@ -1,11 +1,10 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const bibliographyDirective: DirectiveSpec = { name: 'bibliography', options: { filter: { - type: ParseTypesEnum.string, + type: String, }, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/code.ts b/packages/myst-directives/src/code.ts index 9ce33ee7b..20012d150 100644 --- a/packages/myst-directives/src/code.ts +++ b/packages/myst-directives/src/code.ts @@ -2,7 +2,7 @@ import type { Caption, Container } from 'myst-spec'; import type { Code } from 'myst-spec-ext'; import yaml from 'js-yaml'; import type { DirectiveData, DirectiveSpec, GenericNode } from 'myst-common'; -import { fileError, fileWarn, normalizeLabel, ParseTypesEnum, RuleId } from 'myst-common'; +import { fileError, fileWarn, normalizeLabel, RuleId } from 'myst-common'; import type { VFile } from 'vfile'; function parseEmphasizeLines(emphasizeLinesString?: string | undefined): number[] | undefined { @@ -18,7 +18,8 @@ function parseEmphasizeLines(emphasizeLinesString?: string | undefined): number[ export function getCodeBlockOptions( options: DirectiveData['options'], vfile: VFile, -): Pick { + defaultFilename?: string, +): Pick { if (options?.['lineno-start'] != null && options?.['number-lines'] != null) { fileWarn(vfile, 'Cannot use both "lineno-start" and "number-lines"', { source: 'code-block:options', @@ -39,62 +40,77 @@ export function getCodeBlockOptions( } else if (startingLineNumber == null || startingLineNumber <= 1) { startingLineNumber = undefined; } + let filename = options?.['filename'] as string | undefined; + if (filename?.toLowerCase() === 'false') { + filename = undefined; + } else if (!filename && defaultFilename) { + filename = defaultFilename; + } return { emphasizeLines, showLineNumbers, startingLineNumber, + filename, }; } export const CODE_DIRECTIVE_OPTIONS: DirectiveSpec['options'] = { caption: { - type: ParseTypesEnum.parsed, + type: 'myst', + doc: 'A parsed caption for the code block.', }, linenos: { - type: ParseTypesEnum.boolean, + type: Boolean, doc: 'Show line numbers', }, 'lineno-start': { - type: ParseTypesEnum.number, + type: Number, doc: 'Start line numbering from a particular value, default is 1. If present, line numbering is activated.', }, 'number-lines': { - type: ParseTypesEnum.number, + type: Number, doc: 'Alternative for "lineno-start", turns on line numbering and can be an integer that is the start of the line numbering.', }, 'emphasize-lines': { - type: ParseTypesEnum.string, + type: String, doc: 'Emphasize particular lines (comma-separated numbers), e.g. "3,5"', }, + filename: { + type: String, + doc: 'Show the filename in addition to the rendered code. The `include` directive will use the filename by default, to turn off this default set the filename to `false`.', + }, // dedent: { - // type: ParseTypesEnum.number, + // type: Number, // doc: 'Strip indentation characters from the code block', // }, // force: { - // type: ParseTypesEnum.boolean, + // type: Boolean, // doc: 'Ignore minor errors on highlighting', // }, }; export const codeDirective: DirectiveSpec = { name: 'code', + doc: 'A code-block environment with a language as the argument, and options for highlighting, showing line numbers, and an optional filename.', alias: ['code-block', 'sourcecode'], arg: { - type: ParseTypesEnum.string, + type: String, + doc: 'Code language, for example `python` or `typescript`', }, options: { label: { - type: ParseTypesEnum.string, + type: String, alias: ['name'], }, class: { - type: ParseTypesEnum.string, + type: String, // class_option: list of strings? }, ...CODE_DIRECTIVE_OPTIONS, }, body: { - type: ParseTypesEnum.string, + type: String, + doc: 'The raw code to display for the code block.', }, run(data, vfile): GenericNode[] { const { label, identifier } = normalizeLabel(data.options?.label as string | undefined) || {}; @@ -134,16 +150,16 @@ export const codeDirective: DirectiveSpec = { export const codeCellDirective: DirectiveSpec = { name: 'code-cell', arg: { - type: ParseTypesEnum.string, + type: String, required: true, }, options: { tags: { - type: ParseTypesEnum.string, + type: String, }, }, body: { - type: ParseTypesEnum.string, + type: String, }, run(data, vfile): GenericNode[] { const code: Code = { diff --git a/packages/myst-directives/src/dropdown.ts b/packages/myst-directives/src/dropdown.ts index cd39dde59..893060f5a 100644 --- a/packages/myst-directives/src/dropdown.ts +++ b/packages/myst-directives/src/dropdown.ts @@ -1,14 +1,13 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const dropdownDirective: DirectiveSpec = { name: 'dropdown', arg: { - type: ParseTypesEnum.parsed, + type: 'myst', }, options: { open: { - type: ParseTypesEnum.boolean, + type: Boolean, }, // Legacy options we may want to implement: // color @@ -21,7 +20,7 @@ export const dropdownDirective: DirectiveSpec = { // 'class-body' }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/embed.ts b/packages/myst-directives/src/embed.ts index 6baae2ed5..4c187b122 100644 --- a/packages/myst-directives/src/embed.ts +++ b/packages/myst-directives/src/embed.ts @@ -1,19 +1,19 @@ import type { DirectiveSpec, DirectiveData } from 'myst-common'; -import { normalizeLabel, ParseTypesEnum } from 'myst-common'; +import { normalizeLabel } from 'myst-common'; import type { Embed } from 'myst-spec-ext'; export const embedDirective: DirectiveSpec = { name: 'embed', arg: { - type: ParseTypesEnum.string, + type: String, required: true, }, options: { 'remove-input': { - type: ParseTypesEnum.boolean, + type: Boolean, }, 'remove-output': { - type: ParseTypesEnum.boolean, + type: Boolean, }, }, run(data: DirectiveData): Embed[] { diff --git a/packages/myst-directives/src/figure.ts b/packages/myst-directives/src/figure.ts index 3caaab0a8..d1e79831e 100644 --- a/packages/myst-directives/src/figure.ts +++ b/packages/myst-directives/src/figure.ts @@ -1,65 +1,66 @@ import type { Image } from 'myst-spec-ext'; import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { normalizeLabel, ParseTypesEnum } from 'myst-common'; +import { normalizeLabel } from 'myst-common'; export const figureDirective: DirectiveSpec = { name: 'figure', arg: { - type: ParseTypesEnum.string, + type: String, + doc: 'The filename of an image (e.g. `my-fig.png`), or an ID of a Jupyter Notebook cell (e.g. `#my-cell`).', required: true, }, options: { label: { - type: ParseTypesEnum.string, + type: String, alias: ['name'], }, class: { - type: ParseTypesEnum.string, - // class_option: list of strings? + type: String, + alias: ['figclass'], }, height: { - type: ParseTypesEnum.string, - // length_or_unitless, + type: String, + doc: 'The figure height, in CSS units, for example `4em` or `300px`.', + alias: ['h'], }, width: { - type: ParseTypesEnum.string, + type: String, // TODO: validate that this is a CSS width - // length_or_percentage_or_unitless, + alias: ['w', 'figwidth'], + doc: 'The figure width, in CSS units, for example `50%` or `300px`.', }, alt: { - type: ParseTypesEnum.string, + type: String, + doc: 'Alternative text for the image', }, // scale: { - // type: ParseTypesEnum.number, + // type: Number, // }, // target: { - // type: ParseTypesEnum.string, + // type: String, // }, align: { - type: ParseTypesEnum.string, + type: String, + doc: 'The alignment of the image in the figure. Choose one of `left`, `center` or `right`', // TODO: this is not implemented below // choice(["left", "center", "right"]) }, - // figwidth: { - // type: ParseTypesEnum.string, - // // length_or_percentage_or_unitless_figure - // }, - // figclass: { - // type: ParseTypesEnum.string, - // // class_option: list of strings? - // }, 'remove-input': { - type: ParseTypesEnum.boolean, + type: Boolean, + doc: 'If the argument is a notebook cell, use this flag to remove the input code from the cell.', }, 'remove-output': { - type: ParseTypesEnum.boolean, + type: Boolean, + doc: 'If the argument is a notebook cell, use this flag to remove the output from the cell.', }, placeholder: { - type: ParseTypesEnum.string, + type: String, + doc: 'A placeholder image when using a notebook cell as the figure contents. This will be shown in place of the Jupyter output until an execution environment is attached. It will also be used in static outputs, such as a PDF output.', }, }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', + doc: 'If provided, this will be the figure caption.', }, run(data: DirectiveData): GenericNode[] { const children: GenericNode[] = []; diff --git a/packages/myst-directives/src/glossary.ts b/packages/myst-directives/src/glossary.ts index fedac2b00..6764e3526 100644 --- a/packages/myst-directives/src/glossary.ts +++ b/packages/myst-directives/src/glossary.ts @@ -1,10 +1,9 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const glossaryDirective: DirectiveSpec = { name: 'glossary', body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/iframe.ts b/packages/myst-directives/src/iframe.ts index 3294f39d6..56708565d 100644 --- a/packages/myst-directives/src/iframe.ts +++ b/packages/myst-directives/src/iframe.ts @@ -1,32 +1,33 @@ import type { Iframe } from 'myst-spec-ext'; import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum, normalizeLabel } from 'myst-common'; +import { normalizeLabel } from 'myst-common'; export const iframeDirective: DirectiveSpec = { name: 'iframe', arg: { - type: ParseTypesEnum.string, + type: String, + doc: 'The URL source (`src`) of the HTML iframe element.', required: true, }, options: { label: { - type: ParseTypesEnum.string, + type: String, alias: ['name'], }, class: { - type: ParseTypesEnum.string, + type: String, // class_option: list of strings? }, width: { - type: ParseTypesEnum.string, - // length_or_percentage_or_unitless, + type: String, + doc: 'The iframe width, in CSS units, for example `50%` or `300px`.', }, align: { - type: ParseTypesEnum.string, - // choice(["left", "center", "right"]) + type: String, + doc: 'The alignment of the iframe in the page. Choose one of `left`, `center` or `right`', }, }, - body: { type: ParseTypesEnum.parsed }, + body: { type: 'myst', doc: 'If provided, this will be the iframe caption.' }, run(data: DirectiveData): GenericNode[] { const { label, identifier } = normalizeLabel(data.options?.label as string | undefined) || {}; const iframe: Iframe = { diff --git a/packages/myst-directives/src/image.ts b/packages/myst-directives/src/image.ts index 8c53f849c..13a2df30f 100644 --- a/packages/myst-directives/src/image.ts +++ b/packages/myst-directives/src/image.ts @@ -1,44 +1,50 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { toText, ParseTypesEnum } from 'myst-common'; +import { toText } from 'myst-common'; export const imageDirective: DirectiveSpec = { name: 'image', arg: { - type: ParseTypesEnum.string, + type: String, + doc: 'The filename of an image (e.g. `my-fig.png`).', required: true, }, options: { // label: { - // type: ParseTypesEnum.string, + // type: String, // alias: ['name'], // }, class: { - type: ParseTypesEnum.string, + type: String, // class_option: list of strings? }, height: { - type: ParseTypesEnum.string, - // length_or_unitless, + type: String, + doc: 'The image height, in CSS units, for example `4em` or `300px`.', + alias: ['h'], }, width: { - type: ParseTypesEnum.string, - // length_or_percentage_or_unitless, + type: String, + alias: ['w'], + doc: 'The image width, in CSS units, for example `50%` or `300px`.', }, alt: { - type: ParseTypesEnum.string, + type: String, + doc: 'Alternative text for the image', }, // scale: { - // type: ParseTypesEnum.number, + // type: Number, // }, // target: { - // type: ParseTypesEnum.string, + // type: String, // }, align: { - type: ParseTypesEnum.string, + type: String, // choice(["left", "center", "right", "top", "middle", "bottom"]) + doc: 'The alignment of the image. Choose one of `left`, `center` or `right`', }, title: { - type: ParseTypesEnum.string, + type: String, + doc: 'Title text for the image', }, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/include.ts b/packages/myst-directives/src/include.ts index 5ff811b20..47bbc652d 100644 --- a/packages/myst-directives/src/include.ts +++ b/packages/myst-directives/src/include.ts @@ -1,5 +1,5 @@ import type { DirectiveSpec } from 'myst-common'; -import { ParseTypesEnum, RuleId, fileWarn, normalizeLabel } from 'myst-common'; +import { RuleId, fileWarn, normalizeLabel } from 'myst-common'; import { CODE_DIRECTIVE_OPTIONS, getCodeBlockOptions } from './code.js'; import type { Include } from 'myst-spec-ext'; import type { VFile } from 'vfile'; @@ -14,55 +14,57 @@ import type { VFile } from 'vfile'; export const includeDirective: DirectiveSpec = { name: 'include', alias: ['literalinclude'], + doc: 'Allows you to include the source or parsed version of a separate file into your document tree.', arg: { - type: ParseTypesEnum.string, + type: String, + doc: 'The file path, which is relative to the file from which it was referenced.', required: true, }, options: { label: { - type: ParseTypesEnum.string, + type: String, alias: ['name'], }, literal: { - type: ParseTypesEnum.boolean, + type: Boolean, doc: 'Flag the include block as literal, and show the contents as a code block. This can also be set automatically by setting the `language` or using the `literalinclude` directive.', }, lang: { - type: ParseTypesEnum.string, + type: String, doc: 'The language of the code to be highlighted as. If set, this automatically changes an `include` into a `literalinclude`.', alias: ['language', 'code'], }, ...CODE_DIRECTIVE_OPTIONS, 'start-line': { - type: ParseTypesEnum.number, + type: Number, doc: 'Only the content starting from this line will be included. The first line has index 0 and negative values count from the end.', }, 'start-at': { - type: ParseTypesEnum.string, + type: String, doc: 'Only the content after and including the first occurrence of the specified text in the external data file will be included.', }, 'start-after': { - type: ParseTypesEnum.string, + type: String, doc: 'Only the content after the first occurrence of the specified text in the external data file will be included.', }, 'end-line': { - type: ParseTypesEnum.number, + type: Number, doc: 'Only the content up to (but excluding) this line will be included.', }, 'end-at': { - type: ParseTypesEnum.string, + type: String, doc: 'Only the content up to and including the first occurrence of the specified text in the external data file (but after any start-after text) will be included.', }, 'end-before': { - type: ParseTypesEnum.string, + type: String, doc: 'Only the content before the first occurrence of the specified text in the external data file (but after any start-after text) will be included.', }, lines: { - type: ParseTypesEnum.string, + type: String, doc: 'Specify exactly which lines to include from the original file, starting at 1. For example, `1,3,5-10,20-` includes the lines 1, 3, 5 to 10 and lines 20 to the last line of the original file.', }, 'lineno-match': { - type: ParseTypesEnum.boolean, + type: Boolean, doc: 'Display the original line numbers, correct only when the selection consists of contiguous lines.', }, }, @@ -84,7 +86,12 @@ export const includeDirective: DirectiveSpec = { ]; } const lang = (data.options?.lang as string) ?? extToLanguage(file.split('.').pop()); - const opts = getCodeBlockOptions(data.options, vfile); + const opts = getCodeBlockOptions( + data.options, + vfile, + // Set the filename in the literal include by default + file.split(/\/|\\/).pop(), + ); const filter: Include['filter'] = {}; ensureOnlyOneOf(vfile, data.options, ['start-at', 'start-line', 'start-after', 'lines']); ensureOnlyOneOf(vfile, data.options, ['end-at', 'end-line', 'end-before', 'lines']); @@ -175,6 +182,7 @@ function extToLanguage(ext?: string): string | undefined { { ts: 'typescript', js: 'javascript', + mjs: 'javascript', tex: 'latex', py: 'python', md: 'markdown', diff --git a/packages/myst-directives/src/margin.ts b/packages/myst-directives/src/margin.ts index d35007501..d4f7f9ecb 100644 --- a/packages/myst-directives/src/margin.ts +++ b/packages/myst-directives/src/margin.ts @@ -1,10 +1,9 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const marginDirective: DirectiveSpec = { name: 'margin', body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/math.ts b/packages/myst-directives/src/math.ts index ff6425656..2c70e0a9a 100644 --- a/packages/myst-directives/src/math.ts +++ b/packages/myst-directives/src/math.ts @@ -1,16 +1,16 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { normalizeLabel, ParseTypesEnum } from 'myst-common'; +import { normalizeLabel } from 'myst-common'; export const mathDirective: DirectiveSpec = { name: 'math', options: { label: { - type: ParseTypesEnum.string, + type: String, alias: ['name'], }, }, body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/mdast.ts b/packages/myst-directives/src/mdast.ts index c72b4187a..87ff3c732 100644 --- a/packages/myst-directives/src/mdast.ts +++ b/packages/myst-directives/src/mdast.ts @@ -1,10 +1,9 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const mdastDirective: DirectiveSpec = { name: 'mdast', arg: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/mermaid.ts b/packages/myst-directives/src/mermaid.ts index 3f68c5bdf..29089bdfd 100644 --- a/packages/myst-directives/src/mermaid.ts +++ b/packages/myst-directives/src/mermaid.ts @@ -1,10 +1,9 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const mermaidDirective: DirectiveSpec = { name: 'mermaid', body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/mystdemo.ts b/packages/myst-directives/src/mystdemo.ts index 1fb607493..bb3eded9a 100644 --- a/packages/myst-directives/src/mystdemo.ts +++ b/packages/myst-directives/src/mystdemo.ts @@ -1,16 +1,15 @@ import yaml from 'js-yaml'; import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const mystdemoDirective: DirectiveSpec = { name: 'myst', options: { numbering: { - type: ParseTypesEnum.string, + type: String, }, }, body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-directives/src/output.ts b/packages/myst-directives/src/output.ts index 8cbf7087b..ffd203cb8 100644 --- a/packages/myst-directives/src/output.ts +++ b/packages/myst-directives/src/output.ts @@ -1,11 +1,10 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const outputDirective: DirectiveSpec = { name: 'output', options: { id: { - type: ParseTypesEnum.string, + type: String, required: true, }, }, diff --git a/packages/myst-directives/src/table.ts b/packages/myst-directives/src/table.ts index bcd902603..ec8f9a2fe 100644 --- a/packages/myst-directives/src/table.ts +++ b/packages/myst-directives/src/table.ts @@ -1,44 +1,44 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { fileError, normalizeLabel, ParseTypesEnum, RuleId } from 'myst-common'; +import { fileError, normalizeLabel, RuleId } from 'myst-common'; import type { VFile } from 'vfile'; export const listTableDirective: DirectiveSpec = { name: 'list-table', arg: { - type: ParseTypesEnum.parsed, + type: 'myst', }, options: { label: { - type: ParseTypesEnum.string, + type: String, alias: ['name'], }, 'header-rows': { - type: ParseTypesEnum.number, + type: Number, // nonnegative int }, // 'stub-columns': { - // type: ParseTypesEnum.number, + // type: Number, // // nonnegative int // }, // width: { - // type: ParseTypesEnum.string, + // type: String, // // length_or_percentage_or_unitless, // }, // widths: { - // type: ParseTypesEnum.string, + // type: String, // // TODO use correct widths option validator // }, class: { - type: ParseTypesEnum.string, + type: String, // class_option: list of strings? }, align: { - type: ParseTypesEnum.string, + type: String, // choice(['left', 'center', 'right']) }, }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, validate(data: DirectiveData, vfile: VFile) { diff --git a/packages/myst-ext-card/src/index.ts b/packages/myst-ext-card/src/index.ts index 488bd74a9..8e483662a 100644 --- a/packages/myst-ext-card/src/index.ts +++ b/packages/myst-ext-card/src/index.ts @@ -1,23 +1,22 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; const HEADER_REGEX = /((?[\s\S]*?)\s+){0,1}\^\^\^(\s+(?[\s\S]*)){0,1}/; export const cardDirective: DirectiveSpec = { name: 'card', - alias: 'grid-item-card', + alias: ['grid-item-card'], arg: { - type: ParseTypesEnum.parsed, + type: 'myst', }, options: { link: { - type: ParseTypesEnum.string, + type: String, }, header: { - type: ParseTypesEnum.parsed, + type: 'myst', }, footer: { - type: ParseTypesEnum.parsed, + type: 'myst', }, // // https://sphinx-design.readthedocs.io/en/furo-theme/cards.html#card-options // width @@ -47,7 +46,7 @@ export const cardDirective: DirectiveSpec = { // 'class-item' // This seems the same as `class-card`? }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-ext-exercise/src/exercise.ts b/packages/myst-ext-exercise/src/exercise.ts index 278d3776a..30eb6b2e4 100644 --- a/packages/myst-ext-exercise/src/exercise.ts +++ b/packages/myst-ext-exercise/src/exercise.ts @@ -1,28 +1,28 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { createId, normalizeLabel, ParseTypesEnum } from 'myst-common'; +import { createId, normalizeLabel } from 'myst-common'; export const exerciseDirective: DirectiveSpec = { name: 'exercise', alias: ['exercise-start'], arg: { - type: ParseTypesEnum.parsed, + type: 'myst', }, options: { label: { - type: ParseTypesEnum.string, + type: String, }, class: { - type: ParseTypesEnum.string, + type: String, }, nonumber: { - type: ParseTypesEnum.boolean, + type: Boolean, }, hidden: { - type: ParseTypesEnum.boolean, + type: Boolean, }, }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', }, run(data: DirectiveData): GenericNode[] { const children: GenericNode[] = []; @@ -60,22 +60,22 @@ export const solutionDirective: DirectiveSpec = { name: 'solution', alias: ['solution-start'], arg: { - type: ParseTypesEnum.string, + type: String, required: true, }, options: { label: { - type: ParseTypesEnum.string, + type: String, }, class: { - type: ParseTypesEnum.string, + type: String, }, hidden: { - type: ParseTypesEnum.boolean, + type: Boolean, }, }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', }, run(data: DirectiveData): GenericNode[] { const children: GenericNode[] = []; diff --git a/packages/myst-ext-grid/src/index.ts b/packages/myst-ext-grid/src/index.ts index 2f3fb71cd..1eecac0e0 100644 --- a/packages/myst-ext-grid/src/index.ts +++ b/packages/myst-ext-grid/src/index.ts @@ -1,10 +1,9 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const gridDirective: DirectiveSpec = { name: 'grid', arg: { - type: ParseTypesEnum.string, + type: String, }, // options: // // https://sphinx-design.readthedocs.io/en/furo-theme/grids.html#grid-options @@ -16,7 +15,7 @@ export const gridDirective: DirectiveSpec = { // reverse // outline body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-ext-proof/src/proof.ts b/packages/myst-ext-proof/src/proof.ts index 4e674c501..6aee237d1 100644 --- a/packages/myst-ext-proof/src/proof.ts +++ b/packages/myst-ext-proof/src/proof.ts @@ -1,5 +1,5 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { normalizeLabel, ParseTypesEnum } from 'myst-common'; +import { normalizeLabel } from 'myst-common'; export const proofDirective: DirectiveSpec = { name: 'proof', @@ -21,21 +21,21 @@ export const proofDirective: DirectiveSpec = { 'prf:assumption', ], arg: { - type: ParseTypesEnum.parsed, + type: 'myst', }, options: { label: { - type: ParseTypesEnum.string, + type: String, }, class: { - type: ParseTypesEnum.string, + type: String, }, nonumber: { - type: ParseTypesEnum.boolean, + type: Boolean, }, }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-ext-reactive/src/index.ts b/packages/myst-ext-reactive/src/index.ts index 4774083a6..e0e2cb863 100644 --- a/packages/myst-ext-reactive/src/index.ts +++ b/packages/myst-ext-reactive/src/index.ts @@ -1,11 +1,10 @@ import type { RoleSpec, RoleData, GenericNode, DirectiveSpec, DirectiveData } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const reactiveRole: RoleSpec = { name: 'r:range', alias: ['r:dynamic', 'r:display'], body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: RoleData): GenericNode[] { @@ -32,17 +31,17 @@ export const reactiveDirective: DirectiveSpec = { name: 'r:var', options: { name: { - type: ParseTypesEnum.string, + type: String, required: true, }, value: { - type: ParseTypesEnum.string, + type: String, }, rValue: { - type: ParseTypesEnum.string, + type: String, }, format: { - type: ParseTypesEnum.string, + type: String, }, }, run(data: DirectiveData): GenericNode[] { diff --git a/packages/myst-ext-tabs/src/index.ts b/packages/myst-ext-tabs/src/index.ts index 0d1e27b47..70cf6199c 100644 --- a/packages/myst-ext-tabs/src/index.ts +++ b/packages/myst-ext-tabs/src/index.ts @@ -1,16 +1,15 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const tabSetDirective: DirectiveSpec = { name: 'tab-set', - alias: 'tabSet', + alias: ['tabSet'], options: { class: { - type: ParseTypesEnum.string, + type: String, }, }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', }, run(data: DirectiveData): GenericNode[] { return [ @@ -25,20 +24,20 @@ export const tabSetDirective: DirectiveSpec = { export const tabItemDirective: DirectiveSpec = { name: 'tab-item', - alias: 'tabItem', + alias: ['tabItem', 'tab'], // TODO: A transform is necessary for stray `tab`s arg: { - type: ParseTypesEnum.string, + type: String, }, options: { sync: { - type: ParseTypesEnum.string, + type: String, }, selected: { - type: ParseTypesEnum.boolean, + type: Boolean, }, }, body: { - type: ParseTypesEnum.parsed, + type: 'myst', }, run(data: DirectiveData): GenericNode[] { return [ diff --git a/packages/myst-parser/package.json b/packages/myst-parser/package.json index 6a9bef1e1..9ff92d562 100644 --- a/packages/myst-parser/package.json +++ b/packages/myst-parser/package.json @@ -43,13 +43,13 @@ "dependencies": { "he": "^1.2.0", "markdown-it": "^12.3.2", - "markdown-it-amsmath": "^0.3.1", + "markdown-it-amsmath": "^0.4.0", "markdown-it-deflist": "^2.1.0", - "markdown-it-dollarmath": "^0.4.2", + "markdown-it-dollarmath": "^0.5.0", "markdown-it-footnote": "^3.0.3", "markdown-it-front-matter": "^0.2.3", "markdown-it-myst": "1.0.2", - "markdown-it-myst-extras": "0.2.0", + "markdown-it-myst-extras": "0.3.0", "markdown-it-task-lists": "^2.1.1", "myst-common": "^1.1.6", "myst-directives": "^1.0.7", diff --git a/packages/myst-parser/src/roles.ts b/packages/myst-parser/src/roles.ts index cfa3b747a..1eb63cfaf 100644 --- a/packages/myst-parser/src/roles.ts +++ b/packages/myst-parser/src/roles.ts @@ -15,7 +15,7 @@ export function contentFromNode( ruleId: RuleId, ) { const { children, value } = node; - if (spec.type === ParseTypesEnum.parsed) { + if (spec.type === ParseTypesEnum.parsed || spec.type === 'myst') { if (typeof value !== 'string') { fileWarn(vfile, `content is parsed from non-string value for ${description}`, { node, @@ -36,14 +36,14 @@ export function contentFromNode( } return undefined; } - if (spec.type === ParseTypesEnum.string) { + if (spec.type === ParseTypesEnum.string || spec.type === String) { // silently transform numbers into strings here if (typeof value !== 'string' && !(value && typeof value === 'number' && !isNaN(value))) { fileWarn(vfile, `value is not a string for ${description}`, { node, ruleId }); } return String(value); } - if (spec.type === ParseTypesEnum.number) { + if (spec.type === ParseTypesEnum.number || spec.type === Number) { const valueAsNumber = Number(value); if (isNaN(valueAsNumber)) { const fileFn = spec.required ? fileError : fileWarn; @@ -52,7 +52,7 @@ export function contentFromNode( } return valueAsNumber; } - if (spec.type === ParseTypesEnum.boolean) { + if (spec.type === ParseTypesEnum.boolean || spec.type === Boolean) { if (typeof value === 'string') { if (value.toLowerCase() === 'false') return false; if (value.toLowerCase() === 'true') return true; diff --git a/packages/myst-parser/tests/directives/ext.directive.spec.ts b/packages/myst-parser/tests/directives/ext.directive.spec.ts index f431b3a21..6a01529e2 100644 --- a/packages/myst-parser/tests/directives/ext.directive.spec.ts +++ b/packages/myst-parser/tests/directives/ext.directive.spec.ts @@ -352,7 +352,7 @@ describe('custom directive extensions', () => { test('test directive alias string', () => { const TestDirective: DirectiveSpec = { name: 'test', - alias: 'abc', + alias: ['abc'], body: { type: 'string' as any, }, diff --git a/packages/myst-parser/tests/roles/cite.yml b/packages/myst-parser/tests/roles/cite.yml index c127bb564..9a893af77 100644 --- a/packages/myst-parser/tests/roles/cite.yml +++ b/packages/myst-parser/tests/roles/cite.yml @@ -12,6 +12,7 @@ cases: value: Smith_1990 children: - type: cite + kind: narrative label: Smith_1990 identifier: smith_1990 - title: cite role parses with multiple citations @@ -30,8 +31,10 @@ cases: children: - type: cite label: Smith_1990 + kind: narrative identifier: smith_1990 - type: cite + kind: narrative label: Brown_2000 identifier: brown_2000 - title: cite:p role parses @@ -49,6 +52,7 @@ cases: kind: parenthetical children: - type: cite + kind: parenthetical label: smith_1990 identifier: smith_1990 - title: cite:t role parses @@ -66,8 +70,10 @@ cases: kind: narrative children: - type: cite + kind: narrative label: smith_1990 identifier: smith_1990 - type: cite + kind: narrative label: brown_2000 identifier: brown_2000 diff --git a/packages/myst-parser/tests/roles/ext.role.spec.ts b/packages/myst-parser/tests/roles/ext.role.spec.ts index 58ad8bd4d..9c3e12e58 100644 --- a/packages/myst-parser/tests/roles/ext.role.spec.ts +++ b/packages/myst-parser/tests/roles/ext.role.spec.ts @@ -90,7 +90,7 @@ describe('custom role extensions', () => { test('test role alias string', () => { const TestRole: RoleSpec = { name: 'test', - alias: 'abc', + alias: ['abc'], body: { type: 'string' as any, }, diff --git a/packages/myst-roles/src/abbreviation.ts b/packages/myst-roles/src/abbreviation.ts index c595801d0..05a5575c8 100644 --- a/packages/myst-roles/src/abbreviation.ts +++ b/packages/myst-roles/src/abbreviation.ts @@ -1,13 +1,12 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; const ABBR_PATTERN = /^(.+?)\(([^()]+)\)$/; // e.g. 'CSS (Cascading Style Sheets)' export const abbreviationRole: RoleSpec = { name: 'abbreviation', - alias: 'abbr', + alias: ['abbr'], body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/chem.ts b/packages/myst-roles/src/chem.ts index 0d1ad3c3a..99486a5d9 100644 --- a/packages/myst-roles/src/chem.ts +++ b/packages/myst-roles/src/chem.ts @@ -1,11 +1,10 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const chemRole: RoleSpec = { name: 'chemicalFormula', - alias: 'chem', + alias: ['chem'], body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/cite.ts b/packages/myst-roles/src/cite.ts index b512b0bcc..74ec98c4d 100644 --- a/packages/myst-roles/src/cite.ts +++ b/packages/myst-roles/src/cite.ts @@ -1,29 +1,59 @@ -import type { CiteKind } from 'myst-spec-ext'; -import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { normalizeLabel, ParseTypesEnum } from 'myst-common'; +import type { Cite, CiteGroup, CiteKind } from 'myst-spec-ext'; +import type { RoleSpec, RoleData } from 'myst-common'; +import { normalizeLabel } from 'myst-common'; export const citeRole: RoleSpec = { - name: 'cite:p', - alias: ['cite:t', 'cite'], + name: 'cite', + alias: [ + 'cite:p', + 'cite:t', + // https://sphinxcontrib-bibtex.readthedocs.io/en/latest/usage.html + 'cite:ps', + 'cite:ts', + 'cite:ct', + 'cite:cts', + 'cite:alp', + 'cite:alps', + 'cite:label', + 'cite:labelpar', + 'cite:year', + 'cite:yearpar', + 'cite:author', + 'cite:authors', + 'cite:authorpar', + 'cite:authorpars', + 'cite:cauthor', + 'cite:cauthors', + // 'cite:empty', + ], body: { - type: ParseTypesEnum.string, + type: String, required: true, }, - run(data: RoleData): GenericNode[] { + run(data: RoleData): (Cite | CiteGroup)[] { const content = data.body as string; const labels = content.split(/[,;]/).map((s) => s.trim()); + const kind: CiteKind = + data.name.startsWith('cite:p') || data.name.includes('par') ? 'parenthetical' : 'narrative'; const children = labels.map((l) => { const { label, identifier } = normalizeLabel(l) ?? {}; - return { + const cite: Cite = { type: 'cite', + kind, label: label ?? l, identifier, }; + if (data.name.startsWith('cite:year')) { + cite.partial = 'year'; + } + if (data.name.startsWith('cite:author') || data.name.startsWith('cite:cauthor')) { + cite.partial = 'author'; + } + return cite; }); if (data.name === 'cite' && children.length === 1) { return children; } - const kind: CiteKind = data.name === 'cite:p' ? 'parenthetical' : 'narrative'; return [ { type: 'citeGroup', diff --git a/packages/myst-roles/src/delete.ts b/packages/myst-roles/src/delete.ts index d0b26d537..327583ac0 100644 --- a/packages/myst-roles/src/delete.ts +++ b/packages/myst-roles/src/delete.ts @@ -1,11 +1,10 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const deleteRole: RoleSpec = { name: 'delete', alias: ['del', 'strike'], body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/doc.ts b/packages/myst-roles/src/doc.ts index af7d67640..b4d4561d7 100644 --- a/packages/myst-roles/src/doc.ts +++ b/packages/myst-roles/src/doc.ts @@ -1,12 +1,12 @@ import type { RoleSpec } from 'myst-common'; -import { fileWarn, ParseTypesEnum, RuleId } from 'myst-common'; +import { fileWarn, RuleId } from 'myst-common'; const REF_PATTERN = /^(.+?)<([^<>]+)>$/; // e.g. 'Labeled Document ' export const docRole: RoleSpec = { name: 'doc', body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data, vfile) { diff --git a/packages/myst-roles/src/download.ts b/packages/myst-roles/src/download.ts index f7fbad246..94ce5e5a8 100644 --- a/packages/myst-roles/src/download.ts +++ b/packages/myst-roles/src/download.ts @@ -1,12 +1,11 @@ import type { RoleSpec } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; const REF_PATTERN = /^(.+?)<([^<>]+)>$/; // e.g. 'Labeled Download ' export const downloadRole: RoleSpec = { name: 'download', body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data) { diff --git a/packages/myst-roles/src/inlineExpression.ts b/packages/myst-roles/src/inlineExpression.ts index 285181ecf..18f02618e 100644 --- a/packages/myst-roles/src/inlineExpression.ts +++ b/packages/myst-roles/src/inlineExpression.ts @@ -1,11 +1,10 @@ import type { InlineExpression } from 'myst-spec-ext'; import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const evalRole: RoleSpec = { name: 'eval', body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/math.ts b/packages/myst-roles/src/math.ts index c7fb66b4f..baa53a7ca 100644 --- a/packages/myst-roles/src/math.ts +++ b/packages/myst-roles/src/math.ts @@ -1,10 +1,9 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const mathRole: RoleSpec = { name: 'math', body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/reference.ts b/packages/myst-roles/src/reference.ts index 5fa6e0927..b46667344 100644 --- a/packages/myst-roles/src/reference.ts +++ b/packages/myst-roles/src/reference.ts @@ -1,5 +1,5 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { normalizeLabel, ParseTypesEnum } from 'myst-common'; +import { normalizeLabel } from 'myst-common'; const REF_PATTERN = /^(.+?)<([^<>]+)>$/; // e.g. 'Labeled Reference ' @@ -7,7 +7,7 @@ export const refRole: RoleSpec = { name: 'ref', alias: ['eq', 'numref', 'prf:ref'], body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/si.ts b/packages/myst-roles/src/si.ts index 6db431ef1..44ae6d0bc 100644 --- a/packages/myst-roles/src/si.ts +++ b/packages/myst-roles/src/si.ts @@ -1,11 +1,10 @@ import type { SiUnit } from 'myst-spec-ext'; import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const siRole: RoleSpec = { name: 'si', body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/smallcaps.ts b/packages/myst-roles/src/smallcaps.ts index 927621f47..0f2de760d 100644 --- a/packages/myst-roles/src/smallcaps.ts +++ b/packages/myst-roles/src/smallcaps.ts @@ -1,11 +1,10 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const smallcapsRole: RoleSpec = { name: 'smallcaps', - alias: 'sc', + alias: ['sc'], body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/subscript.ts b/packages/myst-roles/src/subscript.ts index ff51a8870..bcb2f34d9 100644 --- a/packages/myst-roles/src/subscript.ts +++ b/packages/myst-roles/src/subscript.ts @@ -1,11 +1,10 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const subscriptRole: RoleSpec = { name: 'subscript', - alias: 'sub', + alias: ['sub'], body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/superscript.ts b/packages/myst-roles/src/superscript.ts index 8b6e0c554..0f0a20bc0 100644 --- a/packages/myst-roles/src/superscript.ts +++ b/packages/myst-roles/src/superscript.ts @@ -1,11 +1,10 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const superscriptRole: RoleSpec = { name: 'superscript', - alias: 'sup', + alias: ['sup'], body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-roles/src/term.ts b/packages/myst-roles/src/term.ts index cfdf8e5be..2a38d1908 100644 --- a/packages/myst-roles/src/term.ts +++ b/packages/myst-roles/src/term.ts @@ -1,12 +1,12 @@ import type { RoleSpec } from 'myst-common'; -import { fileWarn, normalizeLabel, ParseTypesEnum, RuleId } from 'myst-common'; +import { fileWarn, normalizeLabel, RuleId } from 'myst-common'; const REF_PATTERN = /^(.+?)<([^<>]+)>$/; // e.g. 'Labeled Term ' export const termRole: RoleSpec = { name: 'term', body: { - type: ParseTypesEnum.string, + type: String, required: true, }, run(data, vfile) { diff --git a/packages/myst-roles/src/underline.ts b/packages/myst-roles/src/underline.ts index f513d1027..49a2a5903 100644 --- a/packages/myst-roles/src/underline.ts +++ b/packages/myst-roles/src/underline.ts @@ -1,11 +1,10 @@ import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; -import { ParseTypesEnum } from 'myst-common'; export const underlineRole: RoleSpec = { name: 'underline', - alias: 'u', + alias: ['u'], body: { - type: ParseTypesEnum.parsed, + type: 'myst', required: true, }, run(data: RoleData): GenericNode[] { diff --git a/packages/myst-spec-ext/src/types.ts b/packages/myst-spec-ext/src/types.ts index d07d66da2..94c7d24e0 100644 --- a/packages/myst-spec-ext/src/types.ts +++ b/packages/myst-spec-ext/src/types.ts @@ -85,6 +85,7 @@ export type Admonition = SpecAdmonition & { export type Code = SpecCode & { executable?: boolean; + filename?: string; visibility?: 'show' | 'hide' | 'remove'; }; @@ -98,7 +99,8 @@ export type Cite = { type: 'cite'; kind: CiteKind; label: string; - children: StaticPhrasingContent[]; + identifier?: string; + children?: StaticPhrasingContent[]; error?: boolean | 'not found' | 'rendering error'; prefix?: string; suffix?: string; @@ -170,6 +172,7 @@ export type Include = { /** The `match` will be removed in a transform */ startingLineNumber?: number | 'match'; emphasizeLines?: number[]; + filename?: string; identifier?: string; label?: string; children?: (FlowContent | ListContent | PhrasingContent)[]; diff --git a/packages/myst-transforms/src/include.ts b/packages/myst-transforms/src/include.ts index 3f31c9332..df4dd7936 100644 --- a/packages/myst-transforms/src/include.ts +++ b/packages/myst-transforms/src/include.ts @@ -43,6 +43,7 @@ export async function includeDirectiveTransform(tree: GenericParent, file: VFile 'startingLineNumber', 'label', 'identifier', + 'filename', ] as const ).forEach((attr) => { if (!node[attr]) return;