11import type { RemarkPlugin } from "@astrojs/markdown-remark" ;
2-
2+ import type { ContainerDirective } from "mdast-util-directive" ;
33import { visit } from "unist-util-visit" ;
44import type { VFile } from "vfile" ;
55
66const groupsPath = `guidelines/groups` ;
77const 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+
1932const isTermFile = ( file : VFile ) => file . dirname ?. startsWith ( `${ file . cwd } /guidelines/terms` ) ;
2033
2134const 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+
3971const 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