Skip to content

Commit fae1ab1

Browse files
authored
🐛 Do not raise errors on non-accessible sub-trees (#2532)
1 parent e28fbef commit fae1ab1

File tree

7 files changed

+82
-8
lines changed

7 files changed

+82
-8
lines changed

.changeset/smart-pillows-follow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'myst-parser': patch
3+
---
4+
5+
Do not raise errors on unprocessed nodes

packages/myst-parser/src/directives.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import type {
33
DirectiveData,
44
DirectiveSpec,
55
DirectiveContext,
6-
ParseTypes,
76
GenericParent,
87
} from 'myst-common';
98
import { RuleId, fileError, fileWarn } from 'myst-common';
109
import { selectAll } from 'unist-util-select';
1110
import type { VFile } from 'vfile';
12-
import { contentFromNode } from './utils.js';
11+
import { contentFromNode, markChildrenAsProcessed } from './utils.js';
1312
import type { Directive } from 'myst-spec';
1413
import { parseOptions } from './inlineAttributes.js';
1514

1615
type MystDirectiveNode = GenericNode & {
1716
name: string;
17+
processed?: false;
1818
};
1919
/**
2020
* Apply directive `run()` methods to build directive ASTs.
@@ -47,12 +47,20 @@ export function applyDirectives(
4747
}
4848
});
4949
});
50-
// Find all raw directive nodes
50+
// Find all raw directive nodes, these will have a `processed` attribute set to `false`
5151
const nodes = selectAll('mystDirective[processed=false]', tree) as MystDirectiveNode[];
5252
nodes.forEach((node) => {
53-
delete node.processed; // Indicate that the directive has been processed
5453
const { name } = node;
5554
const spec = specLookup[name];
55+
if (spec && spec.body && spec.body.type !== 'myst') {
56+
// Do not process the children of the directive that is not a myst block
57+
// Doing so would raise errors (for example, if the parent directive is a code block)
58+
// See https://github.com/jupyter-book/mystmd/issues/2530
59+
markChildrenAsProcessed(node);
60+
}
61+
// Re-check if the directive has been processed
62+
if (node.processed !== false) return;
63+
delete node.processed; // Indicate that the directive has been processed
5664
if (!spec) {
5765
fileError(vfile, `unknown directive: ${name}`, { node, ruleId: RuleId.directiveKnown });
5866
// We probably want to do something better than just delete the children and

packages/myst-parser/src/roles.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { RuleId, fileError, fileWarn } from 'myst-common';
33
import type { Role } from 'myst-spec';
44
import { selectAll } from 'unist-util-select';
55
import type { VFile } from 'vfile';
6-
import { contentFromNode } from './utils.js';
6+
import { contentFromNode, markChildrenAsProcessed } from './utils.js';
77
import { parseOptions } from './inlineAttributes.js';
88

99
type MystRoleNode = GenericNode & {
1010
name: string;
11+
processed?: false;
1112
};
1213

1314
export function applyRoles(tree: GenericParent, specs: RoleSpec[], vfile: VFile) {
@@ -27,11 +28,20 @@ export function applyRoles(tree: GenericParent, specs: RoleSpec[], vfile: VFile)
2728
}
2829
});
2930
});
31+
// Find all raw role nodes, these will have a `processed` attribute set to `false`
3032
const nodes = selectAll('mystRole[processed=false]', tree) as MystRoleNode[];
3133
nodes.forEach((node) => {
32-
delete node.processed; // Indicate that the role has been processed
3334
const { name } = node;
3435
const spec = specLookup[name];
36+
if (spec && spec.body && spec.body.type !== 'myst') {
37+
// Do not process the children of the role that is not a myst block
38+
// Doing so would raise errors (for example, if the parent role is a cite)
39+
// See https://github.com/jupyter-book/mystmd/issues/2530
40+
markChildrenAsProcessed(node);
41+
}
42+
// Re-check if the role has been processed
43+
if (node.processed !== false) return;
44+
delete node.processed; // Indicate that the role has been processed
3545
if (!spec) {
3646
fileError(vfile, `unknown role: ${name}`, { node, ruleId: RuleId.roleKnown });
3747
// We probably want to do something better than just delete the children

packages/myst-parser/src/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fileError, fileWarn, ParseTypesEnum } from 'myst-common';
22
import type { GenericNode, ArgDefinition, RuleId } from 'myst-common';
3+
import { selectAll } from 'unist-util-select';
34
import type { VFile } from 'vfile';
45

56
export function contentFromNode(
@@ -59,3 +60,13 @@ export function contentFromNode(
5960
return !!value;
6061
}
6162
}
63+
64+
export function markChildrenAsProcessed(node: GenericNode) {
65+
const children = selectAll('mystDirective,mystRole', {
66+
type: 'root',
67+
children: node.children,
68+
}) as GenericNode[];
69+
children.forEach((child) => {
70+
delete child.processed;
71+
});
72+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { RuleId } from 'myst-common';
3+
import { mystParse } from '../../src';
4+
import { VFile } from 'vfile';
5+
6+
describe('basic directive tests', () => {
7+
test('Errors not raised for directives that are not processed', () => {
8+
const vfile1 = new VFile();
9+
mystParse('````{code}\n```{dontraiseerror}\n```\n````', { vfile: vfile1 }) as any;
10+
expect(vfile1.messages).toEqual([]);
11+
const vfile2 = new VFile();
12+
mystParse('```{raiseerror}\nblah\n```', { vfile: vfile2 }) as any;
13+
expect(vfile2.messages).toMatchObject([
14+
{
15+
message: 'unknown directive: raiseerror',
16+
ruleId: RuleId.directiveKnown,
17+
},
18+
]);
19+
});
20+
});

packages/myst-parser/tests/myst.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,6 @@ describe('Testing mdast --> html conversions', () => {
217217

218218
if (skipped.length) {
219219
describe('Skipped Tests', () => {
220-
test.skip.each(skipped)('%s', () => null);
220+
test.skip.each(skipped)('%s', () => undefined);
221221
});
222222
}

packages/myst-parser/tests/roles/basicRole.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { describe, expect, test } from 'vitest';
2-
import { normalizeLabel, type GenericNode, type RoleData, type RoleSpec } from 'myst-common';
2+
import {
3+
normalizeLabel,
4+
RuleId,
5+
type GenericNode,
6+
type RoleData,
7+
type RoleSpec,
8+
} from 'myst-common';
39
import { mystParse } from '../../src';
410
import { position, positionFn } from '../position';
11+
import { VFile } from 'vfile';
512

613
describe('role spec with options', () => {
714
test('complex inline role with options', () => {
@@ -88,4 +95,17 @@ describe('role spec with options', () => {
8895
],
8996
});
9097
});
98+
test('Errors not raised for roles that are not processed', () => {
99+
const vfile1 = new VFile();
100+
mystParse('{cite}`` {noerror}`asdf` ``', { vfile: vfile1 }) as any;
101+
expect(vfile1.messages).toEqual([]);
102+
const vfile2 = new VFile();
103+
mystParse('{error}`asdf`', { vfile: vfile2 }) as any;
104+
expect(vfile2.messages).toMatchObject([
105+
{
106+
message: 'unknown role: error',
107+
ruleId: RuleId.roleKnown,
108+
},
109+
]);
110+
});
91111
});

0 commit comments

Comments
 (0)