Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/myst-to-md/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"myst-frontmatter": "^1.7.6",
"unist-util-select": "^4.0.3",
"vfile": "^5.3.7",
"vfile-reporter": "^7.0.4"
"vfile-reporter": "^7.0.4",
"myst-ext-exercise": "^1.0.9"
Copy link
Member

Choose a reason for hiding this comment

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

PR looks good in general! I am trying to think of a way to not have the extension as a dependency?

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the review @rowanc1. Hmm, do you have any suggestions for that?

}
}
116 changes: 93 additions & 23 deletions packages/myst-to-md/src/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,97 @@ function mystDirective(node: any, _: Parent, state: NestedState, info: Info): st
return writeStaticDirective(node.name, { argsKey: 'args' })(node, _, state, info);
}

/**
* Handler for any directive with children nodes
*
* This adds multiple backticks in cases where directives are nested
*/
function writeFlowDirective(name: string, args?: string, options?: DirectiveOptions) {
return (node: any, _: Parent, state: NestedState, info: Info): string => {
incrementNestedLevel('directive', state);
const optionsLines = optionsFromNode(node, options);
const content = state.containerFlow(node, info);
const nesting = popNestedLevel('directive', state);
const marker = directiveChar.repeat(nesting + 3);
if (optionsLines.length && content) optionsLines.push('');
const directiveLines = [
`${marker}{${name}}${args ? ' ' : ''}${args ? args : ''}`,
...optionsLines,
];
if (content) directiveLines.push(content);
directiveLines.push(marker);
return directiveLines.join('\n');
};
}

/**
* Handler for exercise nodes
*/
function exercise(node: any, _: Parent, state: NestedState, info: Info): string {
const admonitionTitle = select('admonitionTitle', node);
const args = admonitionTitle ? state.containerPhrasing(admonitionTitle as any, info) : '';

// Handle gate property for exercise-start/exercise-end
if (node.gate === 'start') {
const nodeCopy = {
...node,
children: node.children.filter((n: GenericNode) => n.type !== 'admonitionTitle'),
};
const options = {
keys: ['label', 'class', 'hidden', 'enumerated', 'nonumber'],
aliases: {
enumerated: 'enumerated',
nonumber: 'nonumber',
},
};
return writeFlowDirective('exercise-start', args, options)(nodeCopy, _, state, info);
} else if (node.gate === 'end') {
return '```{exercise-end}\n```';
}

const nodeCopy = {
...node,
children: node.children.filter((n: GenericNode) => n.type !== 'admonitionTitle'),
};
const options = {
keys: ['label', 'class', 'hidden', 'enumerated', 'nonumber'],
aliases: {
enumerated: 'enumerated',
nonumber: 'nonumber',
},
};
return writeFlowDirective('exercise', args, options)(nodeCopy, _, state, info);
}

/**
* Handler for solution nodes
*/
function solution(node: any, _: Parent, state: NestedState, info: Info): string {
// Handle gate property for solution-start/solution-end
const args = '';
if (node.gate === 'start') {
const nodeCopy = {
...node,
children: node.children.filter((n: GenericNode) => n.type !== 'admonitionTitle'),
};
const options = {
keys: ['class', 'hidden'],
};
return writeFlowDirective('solution-start', args, options)(nodeCopy, _, state, info);
} else if (node.gate === 'end') {
return '```{solution-end}\n```';
}

const nodeCopy = {
...node,
children: node.children.filter((n: GenericNode) => n.type !== 'admonitionTitle'),
};
const options = {
keys: ['class', 'hidden'],
};
return writeFlowDirective('solution', args, options)(nodeCopy, _, state, info);
}

const CODE_BLOCK_KEYS = [
'class',
'emphasizeLines',
Expand Down Expand Up @@ -142,29 +233,6 @@ function image(node: any, _: Parent, state: NestedState, info: Info): string {
return writeStaticDirective('image', options)(node, _, state, info);
}

/**
* Handler for any directive with children nodes
*
* This adds multiple backticks in cases where directives are nested
*/
function writeFlowDirective(name: string, args?: string, options?: DirectiveOptions) {
return (node: any, _: Parent, state: NestedState, info: Info): string => {
incrementNestedLevel('directive', state);
const optionsLines = optionsFromNode(node, options);
const content = state.containerFlow(node, info);
const nesting = popNestedLevel('directive', state);
const marker = directiveChar.repeat(nesting + 3);
if (optionsLines.length && content) optionsLines.push('');
const directiveLines = [
`${marker}{${name}}${args ? ' ' : ''}${args ? args : ''}`,
...optionsLines,
];
if (content) directiveLines.push(content);
directiveLines.push(marker);
return directiveLines.join('\n');
};
}

function containerValidator(node: any, file: VFile) {
const { kind } = node;
if (kind === 'figure' && !select('image', node)) {
Expand Down Expand Up @@ -397,6 +465,8 @@ export const directiveHandlers: Record<string, Handle> = {
iframe,
aside,
card,
exercise,
solution,
grid: writeFlowDirective('grid', undefined, {
keys: ['columns'],
transforms: { columns: (val) => val.join(' ') },
Expand Down
6 changes: 6 additions & 0 deletions packages/myst-to-md/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gfmTableToMarkdown } from 'mdast-util-gfm-table';
import type { Options } from 'mdast-util-to-markdown';
import { defaultHandlers, toMarkdown } from 'mdast-util-to-markdown';
import { directiveHandlers, directiveValidators } from './directives.js';
import { exerciseDirectives } from 'myst-ext-exercise';
import { miscHandlers, miscValidators } from './misc.js';
import { referenceHandlers } from './references.js';
import { roleHandlers } from './roles.js';
Expand All @@ -22,9 +23,14 @@ export function writeMd(file: VFile, node: Root, frontmatter?: PageFrontmatter)
...referenceHandlers,
...miscHandlers,
};
const exerciseDirectivesNames = exerciseDirectives.flatMap(({ name, alias }) => [
...(name && name.trim() !== '' ? [name] : []),
...(alias?.filter((a) => a && a.trim() !== '') || []),
]);
const handlerKeys = [
...Object.keys(handlers),
...Object.keys(defaultHandlers),
...exerciseDirectivesNames,
...FOOTNOTE_HANDLER_KEYS,
...TABLE_HANDLER_KEYS,
];
Expand Down
160 changes: 160 additions & 0 deletions packages/myst-to-md/tests/exercises.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
title: myst-to-md exercise directives
cases:
- title: exercise directive - basic
mdast:
type: root
children:
- type: exercise
label: ex-1
identifier: ex-1
enumerated: true
children:
- type: admonitionTitle
children:
- type: text
value: Plot a function
- type: paragraph
children:
- type: text
value: Plot the function f(x) = cos(x)
markdown: |-
:::{exercise} Plot a function
:label: ex-1
:enumerated:

Plot the function f(x) = cos(x)
:::
- title: exercise directive - no title
mdast:
type: root
children:
- type: exercise
label: ex-2
identifier: ex-2
enumerated: true
children:
- type: paragraph
children:
- type: text
value: Plot the function f(x) = sin(x)
markdown: |-
:::{exercise}
:label: ex-2
:enumerated:

Plot the function f(x) = sin(x)
:::
- title: exercise-start directive
mdast:
type: root
children:
- type: exercise
label: ex-3
identifier: ex-3
enumerated: true
gate: start
children:
- type: admonitionTitle
children:
- type: text
value: Complex Exercise
- type: paragraph
children:
- type: text
value: This is a complex exercise with multiple parts.
markdown: |-
:::{exercise-start} Complex Exercise
:label: ex-3
:enumerated:

This is a complex exercise with multiple parts.
:::
- title: exercise-end directive
mdast:
type: root
children:
- type: exercise
gate: end
markdown: |-
```{exercise-end}
```
- title: solution directive - basic
mdast:
type: root
children:
- type: solution
hidden: false
children:
- type: admonitionTitle
children:
- type: text
value: 'Solution to '
- type: crossReference
label: ex-1
identifier: ex-1
- type: paragraph
children:
- type: text
value: Here is the solution
markdown: |-
:::{solution}
Here is the solution
:::
- title: solution-start directive with class
mdast:
type: root
children:
- type: solution
gate: start
class: dropdown
children:
- type: admonitionTitle
children:
- type: text
value: 'Solution to '
- type: crossReference
label: mpl_ex1
identifier: mpl_ex1
- type: paragraph
children:
- type: text
value: Here's one solution
markdown: |-
:::{solution-start}
:class: dropdown

Here's one solution
:::
- title: solution-end directive
mdast:
type: root
children:
- type: solution
gate: end
markdown: |-
```{solution-end}
```
- title: exercise with options
mdast:
type: root
children:
- type: exercise
label: ex-4
identifier: ex-4
enumerated: false
nonumber: true
hidden: false
class: my-exercise
children:
- type: paragraph
children:
- type: text
value: Exercise content here
markdown: |-
:::{exercise}
:label: ex-4
:class: my-exercise
:nonumber:

Exercise content here
:::
Loading