diff --git a/.changeset/twenty-rocks-divide.md b/.changeset/twenty-rocks-divide.md new file mode 100644 index 000000000..cd155c344 --- /dev/null +++ b/.changeset/twenty-rocks-divide.md @@ -0,0 +1,9 @@ +--- +"myst-ext-exercise": patch +"myst-directives": patch +"myst-ext-proof": patch +"myst-ext-grid": patch +"myst-ext-tabs": patch +--- + +Add support for commonDirectiveOptions in all directives diff --git a/docs/directives.mjs b/docs/directives.mjs index 4c6923978..a6d4b7411 100644 --- a/docs/directives.mjs +++ b/docs/directives.mjs @@ -2,8 +2,25 @@ import { u } from 'unist-builder'; import { mystParse } from 'myst-parser'; import { defaultDirectives } from 'myst-directives'; import { defaultRoles } from 'myst-roles'; +import { cardDirective } from 'myst-ext-card'; +import { gridDirectives } from 'myst-ext-grid'; +import { proofDirective } from 'myst-ext-proof'; +import { exerciseDirectives } from 'myst-ext-exercise'; +import { reactiveDirective, reactiveRole } from 'myst-ext-reactive'; +import { tabDirectives } from 'myst-ext-tabs'; import { fileError } from 'myst-common'; +const allDirectives = [ + ...defaultDirectives, + ...gridDirectives, + ...exerciseDirectives, + ...tabDirectives, + reactiveDirective, + cardDirective, + proofDirective, +]; +const allRoles = [...defaultRoles, reactiveRole]; + /** * @param {import('myst-common').OptionDefinition} option */ @@ -74,7 +91,7 @@ const mystDirective = { }, run(data, vfile) { const name = data.arg; - const directive = defaultDirectives.find((d) => d.name === name); + const directive = allDirectives.find((d) => d.name === name); if (!directive) { fileError(vfile, `myst:directive: Unknown myst directive "${name}"`); return []; @@ -130,7 +147,7 @@ const mystRole = { }, run(data, vfile) { const name = data.arg; - const role = defaultRoles.find((d) => d.name === name); + const role = allRoles.find((d) => d.name === name); if (!role) { fileError(vfile, `myst:role: Unknown myst role "${name}"`); return []; @@ -178,7 +195,7 @@ const mystDirectiveRole = { 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 directive = allDirectives.find((d) => d.name === name || d.alias?.includes(name)); const identifier = opt ? `directive-${directive?.name ?? name}-${opt}` : `directive-${directive?.name ?? name}`; @@ -186,9 +203,7 @@ const mystDirectiveRole = { if (opt) { textToDisplay = `${textToDisplay}.${opt}`; } - return [ - u('crossReference', { identifier }, [u('inlineCode', `{${textToDisplay}}`)]), - ]; + return [u('crossReference', { identifier }, [u('inlineCode', `{${textToDisplay}}`)])]; }, }; @@ -208,15 +223,13 @@ const mystRoleRole = { const [, modified, rawLabel] = match ?? []; const label = rawLabel ?? data.body; const [name, opt] = label?.split('.') ?? []; - const role = defaultRoles.find((d) => d.name === name || d.alias?.includes(name)); + const role = allRoles.find((d) => d.name === name || d.alias?.includes(name)); const identifier = opt ? `role-${role?.name ?? name}-${opt}` : `role-${role?.name ?? name}`; var textToDisplay = modified?.trim() || name; if (opt) { textToDisplay = `${textToDisplay}.${opt}`; } - return [ - u('crossReference', { identifier }, [u('inlineCode', `{${textToDisplay}}`)]), - ]; + return [u('crossReference', { identifier }, [u('inlineCode', `{${textToDisplay}}`)])]; }, }; diff --git a/docs/exercises.md b/docs/exercises.md index 457c1f939..4a5b85cb3 100644 --- a/docs/exercises.md +++ b/docs/exercises.md @@ -7,19 +7,10 @@ thumbnail: ./thumbnails/exercise.png There are two directives available to add exercises and solutions to your documents: (1) an `exercise` directive; and (2) a `solution` directive. The exercises are enumerated by default and can take in an optional title argument as well as be "gated" around Jupyter Notebook cells. -:::{note} Same as Sphinx Exercise 🎉 -:class: dropdown - -The implementation and documentation for exercises and solutions is based on [Sphinx Exercise](https://ebp-sphinx-exercise.readthedocs.io), the syntax can be used interchangeably. We have reused the examples in that extension here to show off the various parts of the MyST extension. - -Changes to the original extension include being able to click on the exercise label (e.g. "Exercise 1"), and having a link to that exercise anchor. We have also updated the styles from both Sphinx and Jupyter Book to be more distinct from admonitions. - -You can also reference exercises with any cross-reference syntax (including the {myst:role}`ref` and {myst:role}`numref` roles). We recommend the markdown link syntax. -::: - ## Exercise Directive -**Example** +::::{tab-set} +:::{tab-item} Example ```{exercise} :label: my-exercise @@ -34,7 +25,8 @@ In particular, write a function `factorial` such that `factorial(n)` returns $n! for any positive integer $n$. ``` -**MyST Syntax** +::: +:::{tab-item} MyST Syntax ````markdown ```{exercise} @@ -51,31 +43,22 @@ for any positive integer $n$. ``` ```` -_Source:_ [QuantEcon](https://python-programming.quantecon.org/functions.html#Exercise-1) - -The following options for exercise and solution directives are supported: - -- `label`: text - - A unique identifier for your exercise that you can use to reference it with a Markdown link or {myst:role}`ref` and {myst:role}`numref` roles. Cannot contain spaces or special characters. - -- `class`: text - - Value of the exercise’s class attribute which can be used to add custom CSS or JavaScript. This can also be the optional `dropdown` class to initially hide the exercise. - -- `nonumber`: flag (empty) +::: +:::: - Turns off exercise auto numbering. +_Source:_ [QuantEcon](https://python-programming.quantecon.org/functions.html#Exercise-1) -- `hidden` : flag (empty) +The following options for exercise directives are supported: - Removes the directive from the final output. +:::{myst:directive} exercise +::: ## Solution Directive A solution directive can be included using the `solution` pattern. It takes in the label of the directive it wants to link to as a required argument. Unlike the `exercise` directive, the solution directive is not enumerable as it inherits numbering directly from the linked exercise. The argument for a solution is the label of the linked exercise, which is required. -**Example** +::::{tab-set} +:::{tab-item} Example ````{solution} my-exercise :label: my-solution @@ -93,7 +76,8 @@ factorial(4) ``` ```` -**MyST Syntax** +::: +:::{tab-item} MyST Syntax `````markdown ````{solution} my-exercise @@ -113,21 +97,15 @@ factorial(4) ```` ````` -_Source:_ [QuantEcon](https://python-programming.quantecon.org/functions.html#Exercise-1) - -The following options are also supported: - -- `label` : text - - A unique identifier for your solution that you can use to reference it with `{ref}`. Cannot contain spaces or special characters. - -- `class` : text +::: +:::: - Value of the solution’s class attribute which can be used to add custom CSS or JavaScript. +_Source:_ [QuantEcon](https://python-programming.quantecon.org/functions.html#Exercise-1) -- `hidden` : flag (empty) +The following options for solution directives are supported: - Removes the directive from the final output. +:::{myst:directive} solution +::: ## Referencing Exercises & Solutions @@ -170,7 +148,7 @@ In the event that the directive being referenced is unenumerable, the reference ```{exercise} $n!$ Factorial :label: nfactorial -:nonumber: +:enumerated: false Write a function `factorial` such that `factorial(int n)` returns $n!$ for any positive integer $n$. @@ -201,7 +179,7 @@ If the title of the linked directive being reference does not exist, it will def ```{exercise} :label: nfactorial-notitle -:nonumber: +:enumerated: false Write a function `factorial` such that `factorial(int n)` returns $n!$ for any positive integer $n$. @@ -238,44 +216,53 @@ to include in an exercise or solution admonition. ``` **Basic Syntax** +::::{tab-set} +:::{tab-item} Example -````markdown ```{exercise-start} :label: ex1 ``` ```python -# Some code to explain the figure +# Some setup code that needs executing ``` and maybe you wish to add a figure -```{figure} https://github.com/rowanc1/pics/blob/main/beach.png +```{figure} https://github.com/rowanc1/pics/blob/main/sunset.png ``` ```{exercise-end} ``` -```` +::: + +:::{tab-item} MyST Syntax + +````markdown ```{exercise-start} :label: ex1 ``` ```python -# Some setup code that needs executing +# Some code to explain the figure ``` and maybe you wish to add a figure -```{figure} https://github.com/rowanc1/pics/blob/main/sunset.png +```{figure} https://github.com/rowanc1/pics/blob/main/beach.png ``` ```{exercise-end} ``` +```` + +::: +:::: This can also be completed for solutions with `solution-start` and `solution-end` directives. The `solution-start` and `exercise-start` directives have the same options as original directive. @@ -289,7 +276,8 @@ alongside feedback to diagnose the issue in document structure. To visually hide the content, simply add `:class: dropdown` as a directive option, similar to an admonition. -**Example** +::::{tab-set} +:::{tab-item} Example ```{exercise} :class: dropdown @@ -304,7 +292,9 @@ In particular, write a function `factorial` such that `factorial(n)` returns $n! for any positive integer $n$. ``` -**MyST Syntax**: +::: + +:::{tab-item} MyST Syntax ````markdown ```{exercise} @@ -321,11 +311,14 @@ for any positive integer $n$. ``` ```` +::: +:::: + ### Remove Directives Any specific directive can be hidden by introducing the `:hidden:` option. For example, the following example will not be displayed -````markdown +````{myst} ```{exercise} :hidden: @@ -333,11 +326,5 @@ This is a hidden exercise directive. ``` ```` -```{exercise} -:hidden: - -This is a hidden exercise directive. -``` - % TODO: Remove All Solutions % TODO: Custom CSS diff --git a/packages/myst-directives/src/admonition.ts b/packages/myst-directives/src/admonition.ts index dd36e6ff2..61e3b022c 100644 --- a/packages/myst-directives/src/admonition.ts +++ b/packages/myst-directives/src/admonition.ts @@ -69,7 +69,6 @@ export const admonitionDirective: DirectiveSpec = { data.name !== 'admonition' ? (data.name.replace('.callout-', '') as Admonition['kind']) : undefined, - class: data.options?.class as string, children: children as any[], }; if (data.options?.icon === false) { diff --git a/packages/myst-directives/src/aside.ts b/packages/myst-directives/src/aside.ts index 6def25d46..9f29c6b98 100644 --- a/packages/myst-directives/src/aside.ts +++ b/packages/myst-directives/src/aside.ts @@ -1,7 +1,7 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; import type { Aside } from 'myst-spec-ext'; import type { FlowContent, ListContent, PhrasingContent } from 'myst-spec'; -import { addCommonDirectiveOptions, labelDirectiveOption } from './utils.js'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const asideDirective: DirectiveSpec = { name: 'aside', @@ -11,11 +11,7 @@ export const asideDirective: DirectiveSpec = { doc: 'An optional title', }, options: { - ...labelDirectiveOption('aside'), - // TODO: Add enumeration in future - class: { - type: String, - }, + ...commonDirectiveOptions('aside'), }, body: { type: 'myst', @@ -34,7 +30,6 @@ export const asideDirective: DirectiveSpec = { kind: data.name == 'aside' || data.name == 'margin' ? undefined : (data.name as Aside['kind']), children, - class: data.options?.class as string | undefined, }; addCommonDirectiveOptions(data, aside); return [aside]; diff --git a/packages/myst-directives/src/bibliography.ts b/packages/myst-directives/src/bibliography.ts index 5ea3c393c..2062bb491 100644 --- a/packages/myst-directives/src/bibliography.ts +++ b/packages/myst-directives/src/bibliography.ts @@ -1,18 +1,20 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const bibliographyDirective: DirectiveSpec = { name: 'bibliography', options: { + ...commonDirectiveOptions('bibliography'), filter: { type: String, }, }, run(data: DirectiveData): GenericNode[] { - return [ - { - type: 'bibliography', - filter: data.options?.filter, - }, - ]; + const bibliography = { + type: 'bibliography', + filter: data.options?.filter, + }; + addCommonDirectiveOptions(data, bibliography); + return [bibliography]; }, }; diff --git a/packages/myst-directives/src/blockquote.ts b/packages/myst-directives/src/blockquote.ts index 0a2fcb38a..a0d0cd9e3 100644 --- a/packages/myst-directives/src/blockquote.ts +++ b/packages/myst-directives/src/blockquote.ts @@ -1,14 +1,14 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; import type { Container } from 'myst-spec-ext'; import classNames from 'classnames'; -import { addCommonDirectiveOptions, labelDirectiveOption } from './utils.js'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const blockquoteDirective: DirectiveSpec = { name: 'blockquote', alias: ['epigraph', 'pull-quote'], doc: 'Block quotes are used to indicate that the enclosed content forms an extended quotation. They may be followed by an inscription or attribution formed of a paragraph beginning with `--`, `---`, or an em-dash.', options: { - ...labelDirectiveOption('blockquote'), + ...commonDirectiveOptions('blockquote'), // TODO: Add enumeration in future class: { @@ -28,11 +28,9 @@ export const blockquoteDirective: DirectiveSpec = { if (data.body) { children.push(...(data.body as GenericNode[])); } - const className = data.options?.class as string; const container: Container = { type: 'container', kind: 'quote', - class: classNames({ [className]: className, [data.name]: data.name !== 'blockquote' }), children: [ { // @ts-expect-error: myst-spec needs updating to support blockquote @@ -42,6 +40,12 @@ export const blockquoteDirective: DirectiveSpec = { ], }; addCommonDirectiveOptions(data, container); + // Override class to include name of container + const className = data.options?.class as string; + container.class = classNames({ + [className]: className, + [data.name]: data.name !== 'blockquote', + }); return [container]; }, }; diff --git a/packages/myst-directives/src/code.ts b/packages/myst-directives/src/code.ts index b7da9b0f6..a418b013d 100644 --- a/packages/myst-directives/src/code.ts +++ b/packages/myst-directives/src/code.ts @@ -142,10 +142,6 @@ export const codeDirective: DirectiveSpec = { }, options: { ...commonDirectiveOptions('code'), - class: { - type: String, - // class_option: list of strings? - }, ...CODE_DIRECTIVE_OPTIONS, }, body: { @@ -157,7 +153,6 @@ export const codeDirective: DirectiveSpec = { const code: Code = { type: 'code', lang: data.arg as string, - class: data.options?.class as string, ...opts, value: data.body as string, }; diff --git a/packages/myst-directives/src/div.ts b/packages/myst-directives/src/div.ts index 188b9f7bc..6590fa1ba 100644 --- a/packages/myst-directives/src/div.ts +++ b/packages/myst-directives/src/div.ts @@ -1,15 +1,11 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; import type { FlowContent, ListContent, PhrasingContent } from 'myst-spec'; -import { addCommonDirectiveOptions, labelDirectiveOption } from './utils.js'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const divDirective: DirectiveSpec = { name: 'div', options: { - ...labelDirectiveOption('div'), - // TODO: Add enumeration in future - class: { - type: String, - }, + ...commonDirectiveOptions('div'), }, body: { type: 'myst', @@ -18,7 +14,6 @@ export const divDirective: DirectiveSpec = { run(data: DirectiveData): GenericNode[] { const div: GenericNode = { type: 'div', - class: data.options?.class as string | undefined, children: data.body as unknown as (FlowContent | ListContent | PhrasingContent)[], }; addCommonDirectiveOptions(data, div); diff --git a/packages/myst-directives/src/dropdown.ts b/packages/myst-directives/src/dropdown.ts index c78dd9363..fd7b7f40b 100644 --- a/packages/myst-directives/src/dropdown.ts +++ b/packages/myst-directives/src/dropdown.ts @@ -1,5 +1,5 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { addCommonDirectiveOptions, labelDirectiveOption } from './utils.js'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const dropdownDirective: DirectiveSpec = { name: 'dropdown', @@ -7,7 +7,7 @@ export const dropdownDirective: DirectiveSpec = { type: 'myst', }, options: { - ...labelDirectiveOption('dropdown'), + ...commonDirectiveOptions('dropdown'), // TODO: Add enumeration in future open: { type: Boolean, diff --git a/packages/myst-directives/src/embed.ts b/packages/myst-directives/src/embed.ts index 351d2d45e..923dfa96e 100644 --- a/packages/myst-directives/src/embed.ts +++ b/packages/myst-directives/src/embed.ts @@ -1,6 +1,7 @@ import type { DirectiveSpec, DirectiveData } from 'myst-common'; import { normalizeLabel } from 'myst-common'; import type { Embed } from 'myst-spec-ext'; +import { commonDirectiveOptions, addCommonDirectiveOptions } from './utils.js'; export const embedDirective: DirectiveSpec = { name: 'embed', @@ -11,6 +12,7 @@ export const embedDirective: DirectiveSpec = { required: true, }, options: { + ...commonDirectiveOptions('embed'), 'remove-input': { type: Boolean, doc: 'If embedding a Jupyter Notebook cell, remove the input of the cell.', @@ -26,13 +28,13 @@ export const embedDirective: DirectiveSpec = { const arg = argString.startsWith('#') ? argString.substring(1) : argString; const { label } = normalizeLabel(arg) || {}; if (!label) return []; - return [ - { - type: 'embed', - source: { label }, - 'remove-input': data.options?.['remove-input'] as boolean | undefined, - 'remove-output': data.options?.['remove-output'] as boolean | undefined, - }, - ]; + const embed: Embed = { + type: 'embed', + source: { label }, + 'remove-input': data.options?.['remove-input'] as boolean | undefined, + 'remove-output': data.options?.['remove-output'] as boolean | undefined, + }; + addCommonDirectiveOptions(data, embed); + return [embed]; }, }; diff --git a/packages/myst-directives/src/figure.ts b/packages/myst-directives/src/figure.ts index 7c01e4def..406c12d19 100644 --- a/packages/myst-directives/src/figure.ts +++ b/packages/myst-directives/src/figure.ts @@ -102,7 +102,6 @@ export const figureDirective: DirectiveSpec = { const container: GenericParent = { type: 'container', kind: (data.options?.kind as string) || 'figure', - class: data.options?.class, children, }; addCommonDirectiveOptions(data, container); diff --git a/packages/myst-directives/src/glossary.ts b/packages/myst-directives/src/glossary.ts index 6764e3526..67d855f98 100644 --- a/packages/myst-directives/src/glossary.ts +++ b/packages/myst-directives/src/glossary.ts @@ -1,4 +1,5 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const glossaryDirective: DirectiveSpec = { name: 'glossary', @@ -6,12 +7,15 @@ export const glossaryDirective: DirectiveSpec = { type: 'myst', required: true, }, + options: { + ...commonDirectiveOptions('glossary'), + }, run(data: DirectiveData): GenericNode[] { - return [ - { - type: 'glossary', - children: data.body as GenericNode[], - }, - ]; + const glossary = { + type: 'glossary', + children: data.body as GenericNode[], + }; + addCommonDirectiveOptions(data, glossary); + return [glossary]; }, }; diff --git a/packages/myst-directives/src/iframe.ts b/packages/myst-directives/src/iframe.ts index 5a2d8f477..2d82ba423 100644 --- a/packages/myst-directives/src/iframe.ts +++ b/packages/myst-directives/src/iframe.ts @@ -11,10 +11,6 @@ export const iframeDirective: DirectiveSpec = { }, options: { ...commonDirectiveOptions('iframe'), - class: { - type: String, - // class_option: list of strings? - }, width: { type: String, doc: 'The iframe width, in CSS units, for example `50%` or `300px`.', @@ -50,14 +46,12 @@ export const iframeDirective: DirectiveSpec = { ]; } if (!data.body) { - iframe.class = data.options?.class as string; addCommonDirectiveOptions(data, iframe); return [iframe]; } const container = { type: 'container', kind: 'figure', - class: data.options?.class, children: [iframe, { type: 'caption', children: data.body as GenericNode[] }], }; addCommonDirectiveOptions(data, container); diff --git a/packages/myst-directives/src/image.ts b/packages/myst-directives/src/image.ts index 35775f1fb..ddf95b744 100644 --- a/packages/myst-directives/src/image.ts +++ b/packages/myst-directives/src/image.ts @@ -11,10 +11,6 @@ export const imageDirective: DirectiveSpec = { }, options: { ...commonDirectiveOptions('image'), - class: { - type: String, - // class_option: list of strings? - }, height: { type: String, doc: 'The image height, in CSS units, for example `4em` or `300px`.', @@ -46,13 +42,12 @@ export const imageDirective: DirectiveSpec = { }, }, run(data: DirectiveData): GenericNode[] { - const { alt, class: c, height, width, align, title } = data.options || {}; + const { alt, height, width, align, title } = data.options || {}; const image = { type: 'image', url: data.arg as string, alt: alt ?? (data.body ? toText(data.body as GenericNode[]) : undefined), title, - class: c, height, width, align: align ?? 'center', diff --git a/packages/myst-directives/src/include.ts b/packages/myst-directives/src/include.ts index fece35d41..e8e3014b6 100644 --- a/packages/myst-directives/src/include.ts +++ b/packages/myst-directives/src/include.ts @@ -4,6 +4,7 @@ import { CODE_DIRECTIVE_OPTIONS, getCodeBlockOptions } from './code.js'; import type { Include } from 'myst-spec-ext'; import type { VFile } from 'vfile'; import { select } from 'unist-util-select'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; /** * RST documentation: @@ -22,10 +23,7 @@ export const includeDirective: DirectiveSpec = { required: true, }, options: { - label: { - type: String, - alias: ['name'], - }, + ...commonDirectiveOptions('include'), literal: { 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.', @@ -70,21 +68,18 @@ export const includeDirective: DirectiveSpec = { }, }, run(data, vfile): Include[] { - const { label, identifier } = normalizeLabel(data.options?.label as string | undefined) || {}; const literal = data.name === 'literalinclude' || !!data.options?.literal || !!data.options?.lang; const file = data.arg as string; if (!literal) { // TODO: warn on unused options - return [ - { - type: 'include', - file, - label, - identifier, - }, - ]; + const include: Include = { + type: 'include', + file, + }; + addCommonDirectiveOptions(data, include); + return [include]; } const lang = (data.options?.lang as string) ?? extToLanguage(file.split('.').pop()); const opts = getCodeBlockOptions( @@ -122,19 +117,17 @@ export const includeDirective: DirectiveSpec = { ]; } } - return [ - { - type: 'include', - file, - literal, - lang, - label, - identifier, - caption: data.options?.caption as any[], - filter: Object.keys(filter).length > 0 ? filter : undefined, - ...opts, - }, - ]; + const include: Include = { + type: 'include', + file, + literal, + lang, + caption: data.options?.caption as any[], + filter: Object.keys(filter).length > 0 ? filter : undefined, + ...opts, + }; + addCommonDirectiveOptions(data, include); + return [include]; }, }; diff --git a/packages/myst-directives/src/mdast.ts b/packages/myst-directives/src/mdast.ts index 87ff3c732..d177d2d2f 100644 --- a/packages/myst-directives/src/mdast.ts +++ b/packages/myst-directives/src/mdast.ts @@ -1,4 +1,5 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const mdastDirective: DirectiveSpec = { name: 'mdast', @@ -6,12 +7,15 @@ export const mdastDirective: DirectiveSpec = { type: String, required: true, }, + options: { + ...commonDirectiveOptions('mdast'), + }, run(data: DirectiveData): GenericNode[] { - return [ - { - type: 'mdast', - id: data.arg as string, - }, - ]; + const mdast = { + type: 'mdast', + id: data.arg as string, + }; + addCommonDirectiveOptions(data, mdast); + return [mdast]; }, }; diff --git a/packages/myst-directives/src/mermaid.ts b/packages/myst-directives/src/mermaid.ts index a15066fc8..047c31ac8 100644 --- a/packages/myst-directives/src/mermaid.ts +++ b/packages/myst-directives/src/mermaid.ts @@ -1,10 +1,10 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; -import { addCommonDirectiveOptions, labelDirectiveOption } from './utils.js'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const mermaidDirective: DirectiveSpec = { name: 'mermaid', options: { - ...labelDirectiveOption('mermaid'), + ...commonDirectiveOptions('mermaid'), }, body: { type: String, diff --git a/packages/myst-directives/src/mystdemo.ts b/packages/myst-directives/src/mystdemo.ts index bb3eded9a..4a51e6c23 100644 --- a/packages/myst-directives/src/mystdemo.ts +++ b/packages/myst-directives/src/mystdemo.ts @@ -1,9 +1,11 @@ import yaml from 'js-yaml'; import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js'; export const mystdemoDirective: DirectiveSpec = { name: 'myst', options: { + ...commonDirectiveOptions('myst'), numbering: { type: String, }, @@ -21,12 +23,12 @@ export const mystdemoDirective: DirectiveSpec = { //pass } } - return [ - { - type: 'myst', - numbering, - value: data.body as string, - }, - ]; + const myst = { + type: 'myst', + numbering, + value: data.body as string, + }; + addCommonDirectiveOptions(data, myst); + return [myst]; }, }; diff --git a/packages/myst-directives/src/table.ts b/packages/myst-directives/src/table.ts index 7739e5638..0f6387180 100644 --- a/packages/myst-directives/src/table.ts +++ b/packages/myst-directives/src/table.ts @@ -47,7 +47,6 @@ export const tableDirective: DirectiveSpec = { const container = { type: 'container', kind: 'table', - class: data.options?.class, children, }; addCommonDirectiveOptions(data, container); @@ -156,7 +155,6 @@ export const listTableDirective: DirectiveSpec = { const container = { type: 'container', kind: 'table', - class: data.options?.class, children, }; addCommonDirectiveOptions(data, container); @@ -324,7 +322,6 @@ export const csvTableDirective: DirectiveSpec = { const container = { type: 'container', kind: 'table', - class: data.options?.class, children: [...captions, table], }; addCommonDirectiveOptions(data, container); diff --git a/packages/myst-directives/src/utils.ts b/packages/myst-directives/src/utils.ts index 893a6802a..654dab3e1 100644 --- a/packages/myst-directives/src/utils.ts +++ b/packages/myst-directives/src/utils.ts @@ -5,6 +5,15 @@ import { type GenericNode, } from 'myst-common'; +export function classDirectiveOption(nodeType = 'node') { + return { + class: { + type: String, + doc: `Annotate the ${nodeType} with a set of space-delimited class names.`, + }, + }; +} + export function labelDirectiveOption(nodeType = 'node') { return { label: { @@ -31,15 +40,23 @@ export function enumerationDirectiveOptions(nodeType = 'node'): Required['options'] { return { + ...classDirectiveOption(nodeType), ...labelDirectiveOption(nodeType), ...enumerationDirectiveOptions(nodeType), }; } +export function addClassOptions(data: DirectiveData, node: GenericNode): GenericNode { + if (typeof data.options?.class === 'string') { + node.class = data.options.class; + } + return node; +} + export function addLabelOptions(data: DirectiveData, node: GenericNode): GenericNode { const { label, identifier } = normalizeLabel(data.options?.label as string | undefined) || {}; if (label) node.label = label; @@ -58,6 +75,7 @@ export function addEnumerationOptions(data: DirectiveData, node: GenericNode): G } export function addCommonDirectiveOptions(data: DirectiveData, node: GenericNode) { + addClassOptions(data, node); addLabelOptions(data, node); addEnumerationOptions(data, node); return node; diff --git a/packages/myst-ext-exercise/src/exercise.ts b/packages/myst-ext-exercise/src/exercise.ts index 30eb6b2e4..43cf952f5 100644 --- a/packages/myst-ext-exercise/src/exercise.ts +++ b/packages/myst-ext-exercise/src/exercise.ts @@ -1,5 +1,6 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; import { createId, normalizeLabel } from 'myst-common'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from 'myst-directives'; export const exerciseDirective: DirectiveSpec = { name: 'exercise', @@ -8,14 +9,10 @@ export const exerciseDirective: DirectiveSpec = { type: 'myst', }, options: { - label: { - type: String, - }, - class: { - type: String, - }, + ...commonDirectiveOptions('exercise'), nonumber: { type: Boolean, + doc: 'Legacy flag to disable numbering of exercises; equivalent to `enumerated: false`', }, hidden: { type: Boolean, @@ -35,20 +32,30 @@ export const exerciseDirective: DirectiveSpec = { if (data.body) { children.push(...(data.body as GenericNode[])); } - const nonumber = (data.options?.nonumber as boolean) ?? false; - // Numbered, unlabeled exercises still need a label - const backupLabel = nonumber ? undefined : `exercise-${createId()}`; - const rawLabel = (data.options?.label as string) || backupLabel; - const { label, identifier } = normalizeLabel(rawLabel) || {}; + const exercise: GenericNode = { type: 'exercise', - label, - identifier, - class: data.options?.class as string, hidden: data.options?.hidden as boolean, - enumerated: !nonumber, children: children as any[], }; + addCommonDirectiveOptions(data, exercise); + + // Override default `enumerated` behavior + if (data.options?.nonumber !== undefined) { + // If `nonumber` is defined, it takes precedence over enumerated + exercise.enumerated = !data.options.nonumber as boolean; + } else { + // Default `enumerated` value is true if unspecified + exercise.enumerated = (data.options?.enumerated as boolean | undefined) ?? true; + } + + // Numbered, unlabeled exercises still need a label + const backupLabel = exercise.enumerated ? `exercise-${createId()}` : undefined; + const rawLabel = (data.options?.label as string) || backupLabel; + const { label, identifier } = normalizeLabel(rawLabel) || {}; + exercise.label = label; + exercise.identifier = identifier; + if (data.name.endsWith('-start')) { exercise.gate = 'start'; } @@ -64,12 +71,7 @@ export const solutionDirective: DirectiveSpec = { required: true, }, options: { - label: { - type: String, - }, - class: { - type: String, - }, + ...commonDirectiveOptions('solution'), hidden: { type: Boolean, }, @@ -92,16 +94,12 @@ export const solutionDirective: DirectiveSpec = { if (data.body) { children.push(...(data.body as GenericNode[])); } - const rawLabel = data.options?.label as string; - const { label, identifier } = normalizeLabel(rawLabel) || {}; const solution: GenericNode = { type: 'solution', - label, - identifier, - class: data.options?.class as string, hidden: data.options?.hidden as boolean, children: children as any[], }; + addCommonDirectiveOptions(data, solution); if (data.name.endsWith('-start')) { solution.gate = 'start'; } diff --git a/packages/myst-ext-exercise/tests/exercise.spec.ts b/packages/myst-ext-exercise/tests/exercise.spec.ts index 8720e29b6..2b2046934 100644 --- a/packages/myst-ext-exercise/tests/exercise.spec.ts +++ b/packages/myst-ext-exercise/tests/exercise.spec.ts @@ -60,4 +60,104 @@ describe('exercise directive', () => { }); expect(deletePositions(output)).toEqual(expected); }); + it('nonumber is prioritized over enumerated', async () => { + const content = + '```{exercise} Exercise Title\n:label: ex-1\n:nonumber:\n:enumerated:\nExercise content\n```'; + const expected = { + type: 'root', + children: [ + { + type: 'mystDirective', + name: 'exercise', + options: { + label: 'ex-1', + enumerated: true, + nonumber: true, + }, + args: 'Exercise Title', + value: 'Exercise content', + children: [ + { + type: 'exercise', + enumerated: false, + identifier: 'ex-1', + label: 'ex-1', + children: [ + { + type: 'admonitionTitle', + children: [ + { + type: 'text', + value: 'Exercise Title', + }, + ], + }, + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Exercise content', + }, + ], + }, + ], + }, + ], + }, + ], + }; + const output = mystParse(content, { + directives: [exerciseDirective], + }); + expect(deletePositions(output)).toEqual(expected); + }); + it('exercises are enumerated with labels by default', async () => { + const content = '```{exercise} Exercise Title\nExercise content\n```'; + const output = mystParse(content, { + directives: [exerciseDirective], + }); + const label = output.children?.[0]?.children?.[0]?.label; + expect(label).toBeTypeOf('string'); + const expected = { + type: 'root', + children: [ + { + type: 'mystDirective', + name: 'exercise', + args: 'Exercise Title', + value: 'Exercise content', + children: [ + { + type: 'exercise', + enumerated: true, + identifier: label?.toLowerCase(), + label, + children: [ + { + type: 'admonitionTitle', + children: [ + { + type: 'text', + value: 'Exercise Title', + }, + ], + }, + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Exercise content', + }, + ], + }, + ], + }, + ], + }, + ], + }; + expect(deletePositions(output)).toEqual(expected); + }); }); diff --git a/packages/myst-ext-grid/src/index.ts b/packages/myst-ext-grid/src/index.ts index 87d13b253..716245a02 100644 --- a/packages/myst-ext-grid/src/index.ts +++ b/packages/myst-ext-grid/src/index.ts @@ -4,6 +4,7 @@ import { type GenericNode, normalizeLabel, } from 'myst-common'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from 'myst-directives'; function getColumns(columnString?: string) { const columns = (columnString ?? '1 2 2 3') @@ -29,18 +30,21 @@ export const gridDirective: DirectiveSpec = { // padding // reverse // outline + options: { + ...commonDirectiveOptions('grid'), + }, body: { type: 'myst', required: true, }, run(data: DirectiveData): GenericNode[] { - return [ - { - type: 'grid', - columns: getColumns(data.arg as string | undefined), - children: data.body as GenericNode[], - }, - ]; + const grid = { + type: 'grid', + columns: getColumns(data.arg as string | undefined), + children: data.body as GenericNode[], + }; + addCommonDirectiveOptions(data, grid); + return [grid]; }, }; diff --git a/packages/myst-ext-proof/src/proof.ts b/packages/myst-ext-proof/src/proof.ts index e3a83fb2f..7458acb1a 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 } from 'myst-common'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from 'myst-directives'; export const proofDirective: DirectiveSpec = { name: 'proof', @@ -24,15 +24,10 @@ export const proofDirective: DirectiveSpec = { type: 'myst', }, options: { - label: { - type: String, - alias: ['name'], - }, - class: { - type: String, - }, + ...commonDirectiveOptions('proof'), nonumber: { type: Boolean, + doc: 'Legacy flag to disable numbering of proofs; equivalent to `enumerated: false`', }, }, body: { @@ -50,18 +45,21 @@ export const proofDirective: DirectiveSpec = { if (data.body) { children.push(...(data.body as GenericNode[])); } - const nonumber = (data.options?.nonumber as boolean) ?? false; - const rawLabel = data.options?.label as string; - const { label, identifier } = normalizeLabel(rawLabel) || {}; + + // Let `nonumber` take precedence over enumerated + let enumerated: boolean; + if (data.options?.nonumber !== undefined) { + enumerated = !data.options.nonumber as boolean; + } else { + enumerated = (data.options?.enumerated as boolean) ?? true; + } const proof = { type: 'proof', kind: data.name !== 'proof' ? data.name.replace('prf:', '') : undefined, - label, - identifier, - class: data.options?.class as string, - enumerated: !nonumber, + enumerated, children: children as any[], }; + addCommonDirectiveOptions(data, proof); return [proof]; }, }; diff --git a/packages/myst-ext-tabs/src/index.ts b/packages/myst-ext-tabs/src/index.ts index 70cf6199c..a082fedf4 100644 --- a/packages/myst-ext-tabs/src/index.ts +++ b/packages/myst-ext-tabs/src/index.ts @@ -1,24 +1,22 @@ import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; +import { addCommonDirectiveOptions, commonDirectiveOptions } from 'myst-directives'; export const tabSetDirective: DirectiveSpec = { name: 'tab-set', alias: ['tabSet'], options: { - class: { - type: String, - }, + ...commonDirectiveOptions('tab-set'), }, body: { type: 'myst', }, run(data: DirectiveData): GenericNode[] { - return [ - { - type: 'tabSet', - class: data.options?.class, - children: (data.body || []) as GenericNode[], - }, - ]; + const tabSet = { + type: 'tabSet', + children: (data.body || []) as GenericNode[], + }; + addCommonDirectiveOptions(data, tabSet); + return [tabSet]; }, }; @@ -29,6 +27,7 @@ export const tabItemDirective: DirectiveSpec = { type: String, }, options: { + ...commonDirectiveOptions('tab-item'), sync: { type: String, }, @@ -40,15 +39,15 @@ export const tabItemDirective: DirectiveSpec = { type: 'myst', }, run(data: DirectiveData): GenericNode[] { - return [ - { - type: 'tabItem', - title: data.arg ?? 'Tab Title', - sync: data.options?.sync, - selected: data.options?.selected, - children: (data.body || []) as GenericNode[], - }, - ]; + const tabItem = { + type: 'tabItem', + title: data.arg ?? 'Tab Title', + sync: data.options?.sync, + selected: data.options?.selected, + children: (data.body || []) as GenericNode[], + }; + addCommonDirectiveOptions(data, tabItem); + return [tabItem]; }, };