Skip to content

Commit c6efe7b

Browse files
kfranqueiroiadawn
andauthored
Add block directives for applicability and exceptions (#411)
Co-authored-by: Kevin White <[email protected]>
1 parent 20db060 commit c6efe7b

File tree

2 files changed

+86
-9
lines changed

2 files changed

+86
-9
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,33 @@ Your content here
225225
:::
226226
```
227227

228+
### Applicability
229+
230+
The following block will be preceded by "Applies when:".
231+
This is _only_ valid within requirements.
232+
233+
```
234+
:::applicability
235+
a condition is true.
236+
:::
237+
```
238+
239+
If the block contains a single paragraph as above, "Applies when:" will appear inline before the content.
240+
Otherwise (e.g. if the block contains a list), it will appear in a separate paragraph before the content.
241+
242+
### Exceptions
243+
244+
The following block will be preceded by "Except when:".
245+
This is _only_ valid within requirements that also use `:::applicability`.
246+
247+
```
248+
:::exceptions
249+
a condition is true.
250+
:::
251+
```
252+
253+
This follows the same behavior as `:::applicability` regarding single paragraphs vs. other cases.
254+
228255
#### Glossary Term References
229256

230257
The text inside `:term[...]` will be transformed into a link referencing a term in the glossary,

src/lib/markdown/guidelines.ts

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { RemarkPlugin } from "@astrojs/markdown-remark";
2-
2+
import type { ContainerDirective } from "mdast-util-directive";
33
import { visit } from "unist-util-visit";
44
import type { VFile } from "vfile";
55

66
const groupsPath = `guidelines/groups`;
77
const isGuidelineFile = (file: VFile) => file.dirname?.startsWith(`${file.cwd}/${groupsPath}`);
88

9-
function getGuidelineFileType(file: VFile) {
9+
type GuidelineFileType = "group" | "guideline" | "requirement";
10+
11+
function getGuidelineFileType(file: VFile): GuidelineFileType | null {
1012
if (!isGuidelineFile(file)) return null;
1113
const remainingPath = file.dirname!.replace(`${file.cwd}/${groupsPath}/`, "");
1214
const segments = remainingPath?.split("/");
@@ -16,6 +18,17 @@ function getGuidelineFileType(file: VFile) {
1618
return null;
1719
}
1820

21+
/** Fails validation if the file passed is not at the expected hierarchy level. */
22+
function expectGuidelineFileType(
23+
file: VFile,
24+
expectedType: GuidelineFileType,
25+
directiveName: string
26+
) {
27+
const type = getGuidelineFileType(file);
28+
if (type !== expectedType)
29+
file.fail(`:::${directiveName} expected at ${expectedType} level but found at ${type} level`);
30+
}
31+
1932
const isTermFile = (file: VFile) => file.dirname?.startsWith(`${file.cwd}/guidelines/terms`);
2033

2134
const getFrontmatter = (file: VFile) => file.data.astro!.frontmatter!;
@@ -36,12 +49,33 @@ const addEmptyTermNote: RemarkPlugin = () => (tree, file) => {
3649
}
3750
};
3851

52+
/**
53+
* Prepends a <b> element containing the given label.
54+
* If the given node contains a single paragraph, it prepends inline;
55+
* otherwise, it prepends a preceding paragraph before the node.
56+
**/
57+
function prependBoldText(node: ContainerDirective, label: string) {
58+
if (node.children.length === 1 && node.children[0].type === "paragraph") {
59+
node.children[0].children.unshift({
60+
type: "html",
61+
value: `<b>${label}</b> `,
62+
});
63+
} else {
64+
node.children.unshift({
65+
type: "html",
66+
value: `<p><b>${label}</b></p>`,
67+
});
68+
}
69+
}
70+
3971
const customDirectives: RemarkPlugin = () => (tree, file) => {
4072
const isGuideline = isGuidelineFile(file);
4173
const isTerm = isTermFile(file);
4274
if (!isGuideline && !isTerm) return;
4375

44-
visit(tree, (node) => {
76+
const parentsWithApplicability = new Set();
77+
78+
visit(tree, (node, index, parent) => {
4579
if (node.type === "containerDirective") {
4680
if (isGuideline && node.name === "decision-tree") {
4781
const data = node.data || (node.data = {});
@@ -53,9 +87,7 @@ const customDirectives: RemarkPlugin = () => (tree, file) => {
5387
value: "<summary>Which foundational requirements apply?</summary>",
5488
});
5589
} else if (isGuideline && node.name === "user-needs") {
56-
const type = getGuidelineFileType(file);
57-
if (type !== "guideline")
58-
file.fail(`user-needs expected at guideline level but found at ${type} level`);
90+
expectGuidelineFileType(file, "guideline", "user-needs");
5991

6092
const data = node.data || (node.data = {});
6193
data.hName = "details";
@@ -65,9 +97,7 @@ const customDirectives: RemarkPlugin = () => (tree, file) => {
6597
value: "<summary>User Needs</summary><p><em>This section is non-normative.</em></p>",
6698
});
6799
} else if (isGuideline && node.name === "tests") {
68-
const type = getGuidelineFileType(file);
69-
if (type !== "requirement")
70-
file.fail(`tests expected at requirement level but found at ${type} level`);
100+
expectGuidelineFileType(file, "requirement", "tests");
71101

72102
const data = node.data || (node.data = {});
73103
data.hName = "details";
@@ -76,6 +106,26 @@ const customDirectives: RemarkPlugin = () => (tree, file) => {
76106
type: "html",
77107
value: "<summary>Tests</summary><p><em>This section is non-normative.</em></p>",
78108
});
109+
} else if (isGuideline && node.name === "applicability") {
110+
expectGuidelineFileType(file, "requirement", "applicability");
111+
112+
prependBoldText(node, "Applies when:");
113+
node.children.push({
114+
type: "html",
115+
value: "<p><b>Requirement:</b></p>",
116+
});
117+
if (parent && typeof index !== "undefined") {
118+
parentsWithApplicability.add(parent);
119+
parent.children.splice(index!, 1, ...node.children);
120+
}
121+
} else if (isGuideline && node.name === "exceptions") {
122+
expectGuidelineFileType(file, "requirement", "exceptions");
123+
if (!parent || !parentsWithApplicability.has(parent))
124+
file.fail(":::exceptions cannot be used without :::applicability");
125+
126+
prependBoldText(node, "Except when:");
127+
if (parent && typeof index !== "undefined")
128+
parent.children.splice(index!, 1, ...node.children);
79129
} else if (node.name === "ednote") {
80130
const data = node.data || (node.data = {});
81131
data.hName = "div";

0 commit comments

Comments
 (0)