Skip to content

Commit

Permalink
馃悰 Correctly parse tags in {code-cell} (#1128)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Holdgraf <choldgraf@gmail.com>
  • Loading branch information
rowanc1 and choldgraf committed Apr 18, 2024
1 parent 3665dd9 commit f13d451
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/lucky-trees-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"myst-directives": patch
---

Correctly place tags in code-block
5 changes: 5 additions & 0 deletions .changeset/six-rice-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"myst-directives": patch
---

add label to code-cell
2 changes: 2 additions & 0 deletions docs/interactive-notebooks.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@
"tags": []
},
"source": [
"(notebooks:cell-visibility)=\n",
"\n",
"## Control cell visibility\n",
"\n",
"You can control the visibility of cell inputs and outputs by using **cell metadata tags**.\n",
Expand Down
83 changes: 83 additions & 0 deletions docs/notebooks-with-markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ You can define two types of markdown-based computation:
- [**code cells**](#myst:code-cell): for block-level content
- [**in-line expressions**](#myst:inline-expressions): for content inline with surrounding text

```{code-cell} python
:tag: hide-cell
import matplotlib.pyplot as plt
import numpy as np
```

(myst:code-cell)=

## Code cells with the `{code-cell}` directive
Expand Down Expand Up @@ -50,6 +56,83 @@ phrase = f"{hello}, {there}!"
print(phrase)
```

### Add tags to `{code-cell}` directives

You can add tags to the `{code-cell}` directive.
They will be parsed and used in the same way that cell tag metadata is used in `.ipynb` files.

For example, the following code defines a `remove-input` tag:

````markdown
```{code-cell} python
:tags: remove-input
print("This will show output with no input!")
```
````

and results in the following:

% Note that this block break can be removed after this is fixed:
% https://github.com/executablebooks/mystmd/issues/1134
+++

> ```{code-cell} python
> :tags: remove-input
> print("This will show output with no input!")
> ```
This can be particularly helpful for showing the output of a calculation or plot, which is reproducible in the source code, but not shown to the user.

```{code-cell} python
:tags: remove-input
# Data for plotting
t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t)
fig, ax = plt.subplots()
ax.plot(t, s)
ax.set(xlabel='time (s)', ylabel='voltage (mV)',
title='Waves in Time')
ax.grid()
fig.savefig("test.png")
plt.show()
```

For **multiple tags** you have two ways to provide them:

- If you specify argument options with `:`, tags will be parsed as a comma-separated string.
For example:

````markdown
```{code-cell} python
:tags: tag1, tag2,tag3
# Note that whitespace is removed from tags!
print("howdy!")
```
````

- If you specify argument options with YAML, tags should be given as a YAML list.
For example:

````markdown
```{code-cell} python
---
tags:
- tag1
- tag2
---
print("howdy!")
```
````

For more about how to specify directive options, see [](./syntax-overview.md).

:::{seealso} Controlling cell visibility with tags
See [](#notebooks:cell-visibility) for more information.
:::

(myst:inline-expressions)=

## Inline expressions with the `{eval}` role
Expand Down
23 changes: 21 additions & 2 deletions packages/myst-directives/src/code.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'vitest';
import { getCodeBlockOptions } from './code.js';
import { getCodeBlockOptions, parseTags } from './code.js';
import { VFile } from 'vfile';
import type { DirectiveData } from 'myst-common';
import type { DirectiveData, GenericNode } from 'myst-common';

function getCodeBlockOptionsWrap(options: DirectiveData['options'], vfile: VFile) {
return getCodeBlockOptions({ name: '', options, node: {} as any }, vfile);
Expand Down Expand Up @@ -56,4 +56,23 @@ describe('Code block options', () => {
});
expect(vfile.messages.length).toEqual(0);
});
test.each([
['', undefined, 0],
['a , b', ['a', 'b'], 0],
[' , a , 1', ['a', '1'], 0],
['[a, b]', ['a', 'b'], 0],
['[a, 1]', undefined, 1],
['[a, true]', undefined, 1],
['[a, yes]', ['a', 'yes'], 0],
["[a, '1']", ['a', '1'], 0],
['x', ['x'], 0],
[[' x '], ['x'], 0], // Trimmed
[[' x ,'], ['x ,'], 0], // Silly, but allowed if it is explicit
[[1], undefined, 1],
])('parseTags(%s) -> %s', (input, output, numErrors) => {
const vfile = new VFile();
const tags = parseTags(input, vfile, {} as GenericNode);
expect(tags).toEqual(output);
expect(vfile.messages.length).toBe(numErrors);
});
});
87 changes: 54 additions & 33 deletions packages/myst-directives/src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,44 @@ export const CODE_DIRECTIVE_OPTIONS: DirectiveSpec['options'] = {
// },
};

export function parseTags(input: any, vfile: VFile, node: GenericNode): string[] | undefined {
if (!input) return undefined;
if (typeof input === 'string' && input.startsWith('[') && input.endsWith(']')) {
try {
return parseTags(yaml.load(input) as string[], vfile, node);
} catch (error) {
fileError(vfile, 'Could not load tags for code-cell directive', {
node: select('mystDirectiveOption[name="tags"]', node) ?? node,
source: 'code-cell:tags',
ruleId: RuleId.directiveOptionsCorrect,
});
return undefined;
}
}
if (typeof input === 'string') {
const tags = input
.split(/[,\s]/)
.map((t) => t.trim())
.filter((t) => !!t);
return tags.length > 0 ? tags : undefined;
}
if (!Array.isArray(input)) return undefined;
// if the options are loaded directly as yaml (or in recursion)
const tags = input as unknown as string[];
if (tags && Array.isArray(tags) && tags.every((t) => typeof t === 'string')) {
if (tags.length > 0) {
return tags.map((t) => t.trim()).filter((t) => !!t);
}
} else if (tags) {
fileWarn(vfile, 'tags in code-cell directive must be a list of strings', {
node: select('mystDirectiveOption[name="tags"]', node) ?? node,
source: 'code-cell:tags',
ruleId: RuleId.directiveOptionsCorrect,
});
return undefined;
}
}

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.',
Expand Down Expand Up @@ -153,69 +191,52 @@ export const codeDirective: DirectiveSpec = {

export const codeCellDirective: DirectiveSpec = {
name: 'code-cell',
doc: 'An executable code cell',
arg: {
type: String,
doc: 'Language for execution and display, for example `python`. It will default to the language of the notebook or containing markdown file.',
},
options: {
label: {
type: String,
alias: ['name'],
},
tags: {
type: String,
alias: ['tag'],
doc: 'A comma-separated list of tags to add to the cell, for example, `remove-input` or `hide-cell`.',
},
},
body: {
type: String,
doc: 'The code to be executed and displayed.',
},
run(data, vfile): GenericNode[] {
const { label, identifier } = normalizeLabel(data.options?.label as string | undefined) || {};
const code: Code = {
type: 'code',
lang: data.arg as string,
executable: true,
value: (data.body ?? '') as string,
};
let tags: string[] | undefined;
// TODO: this validation should be done in a different place
// For example, specifying that the attribute is YAML,
// and providing a custom validation on the option.
if (typeof data.options?.tags === 'string') {
try {
tags = yaml.load(data.options.tags) as string[];
} catch (error) {
fileError(vfile, 'Could not load tags for code-cell directive', {
node: select('mystDirectiveOption[name="tags"]', data.node) ?? data.node,
source: 'code-cell:tags',
ruleId: RuleId.directiveOptionsCorrect,
});
}
} else if (data.options?.tags && Array.isArray(data.options.tags)) {
// if the options are loaded directly as yaml
tags = data.options.tags as unknown as string[];
}
if (tags && Array.isArray(tags) && tags.every((t) => typeof t === 'string')) {
if (tags && tags.length > 0) {
code.data = { tags: tags.map((t) => t.trim()) };
}
} else if (tags) {
fileWarn(vfile, 'tags in code-cell directive must be a list of strings', {
node: select('mystDirectiveOption[name="tags"]', data.node) ?? data.node,
source: 'code-cell:tags',
ruleId: RuleId.directiveOptionsCorrect,
});
}

const output = {
type: 'output',
id: nanoid(),
data: [],
};

const block = {
const block: GenericNode = {
type: 'block',
meta: undefined, // do we need to attach metadata?
label,
identifier,
children: [code, output],
data: {
type: 'notebook-code',
},
};

const tags = parseTags(data.options?.tags, vfile, data.node);
if (tags) block.data.tags = tags;

return [block];
},
};

0 comments on commit f13d451

Please sign in to comment.