1
- import type { Cheerio , Element } from "cheerio" ;
1
+ import axios from "axios" ;
2
+ import type { CheerioAPI } from "cheerio" ;
2
3
import { glob } from "glob" ;
3
4
4
5
import { readFile } from "fs/promises" ;
5
6
import { basename } from "path" ;
6
7
7
- import { flattenDomFromFile , load } from "./cheerio" ;
8
+ import { flattenDomFromFile , load , type CheerioAnyNode } from "./cheerio" ;
8
9
import { generateId } from "./common" ;
9
10
10
11
export type WcagVersion = "20" | "21" | "22" ;
@@ -34,40 +35,21 @@ export const actRules = (
34
35
) [ "act-rules" ] ;
35
36
36
37
/**
37
- * Returns an object with keys for each existing WCAG 2 version,
38
- * each mapping to an array of basenames of HTML files under understanding/<version>
39
- * (Functionally equivalent to "guidelines-versions" target in build.xml)
38
+ * Flattened object hash, mapping each WCAG 2 SC slug to the earliest WCAG version it applies to.
39
+ * (Functionally equivalent to "guidelines-versions" target in build.xml; structurally inverted)
40
40
*/
41
- export async function getGuidelinesVersions ( ) {
41
+ const scVersions = await ( async function ( ) {
42
42
const paths = await glob ( "*/*.html" , { cwd : "understanding" } ) ;
43
- const versions : Record < WcagVersion , string [ ] > = { "20" : [ ] , "21" : [ ] , "22" : [ ] } ;
43
+ const map : Record < string , WcagVersion > = { } ;
44
44
45
45
for ( const path of paths ) {
46
- const [ version , filename ] = path . split ( "/" ) ;
47
- assertIsWcagVersion ( version ) ;
48
- versions [ version ] . push ( basename ( filename , ".html" ) ) ;
46
+ const [ fileVersion , filename ] = path . split ( "/" ) ;
47
+ assertIsWcagVersion ( fileVersion ) ;
48
+ map [ basename ( filename , ".html" ) ] = fileVersion ;
49
49
}
50
50
51
- for ( const version of Object . keys ( versions ) ) {
52
- assertIsWcagVersion ( version ) ;
53
- versions [ version ] . sort ( ) ;
54
- }
55
- return versions ;
56
- }
57
-
58
- /**
59
- * Like getGuidelinesVersions, but mapping each basename to the version it appears in
60
- */
61
- export async function getInvertedGuidelinesVersions ( ) {
62
- const versions = await getGuidelinesVersions ( ) ;
63
- const invertedVersions : Record < string , string > = { } ;
64
- for ( const [ version , basenames ] of Object . entries ( versions ) ) {
65
- for ( const basename of basenames ) {
66
- invertedVersions [ basename ] = version ;
67
- }
68
- }
69
- return invertedVersions ;
70
- }
51
+ return map ;
52
+ } ) ( ) ;
71
53
72
54
export interface DocNode {
73
55
id : string ;
@@ -79,15 +61,15 @@ export interface DocNode {
79
61
export interface Principle extends DocNode {
80
62
content : string ;
81
63
num : `${number } `; // typed as string for consistency with guidelines/SC
82
- version : "WCAG20 " ;
64
+ version : "20 " ;
83
65
guidelines : Guideline [ ] ;
84
66
type : "Principle" ;
85
67
}
86
68
87
69
export interface Guideline extends DocNode {
88
70
content : string ;
89
71
num : `${Principle [ "num" ] } .${number } `;
90
- version : `WCAG${ "20" | "21" } ` ;
72
+ version : "20" | "21" ;
91
73
successCriteria : SuccessCriterion [ ] ;
92
74
type : "Guideline" ;
93
75
}
@@ -97,50 +79,63 @@ export interface SuccessCriterion extends DocNode {
97
79
num : `${Guideline [ "num" ] } .${number } `;
98
80
/** Level may be empty for obsolete criteria */
99
81
level : "A" | "AA" | "AAA" | "" ;
100
- version : `WCAG${ WcagVersion } ` ;
82
+ version : WcagVersion ;
101
83
type : "SC" ;
102
84
}
103
85
104
86
export function isSuccessCriterion ( criterion : any ) : criterion is SuccessCriterion {
105
87
return ! ! ( criterion ?. type === "SC" && "level" in criterion ) ;
106
88
}
107
89
90
+ /** Version-dependent overrides of SC shortcodes for older versions */
91
+ export const scSlugOverrides : Record < string , ( version : WcagVersion ) => string > = {
92
+ "target-size-enhanced" : ( version ) => ( version < "22" ? "target-size" : "target-size-enhanced" ) ,
93
+ } ;
94
+
95
+ /** Selectors ignored when capturing content of each Principle / Guideline / SC */
96
+ const contentIgnores = [
97
+ "h1, h2, h3, h4, h5, h6" ,
98
+ "section" ,
99
+ ".change" ,
100
+ ".conformance-level" ,
101
+ // Selectors below are specific to pre-published guidelines (for previous versions)
102
+ ".header-wrapper" ,
103
+ ".doclinks" ,
104
+ ] ;
105
+
108
106
/**
109
- * Returns HTML content used for Understanding guideline/SC boxes.
107
+ * Returns HTML content used for Understanding guideline/SC boxes and term definitions .
110
108
* @param $el Cheerio element of the full section from flattened guidelines/index.html
111
109
*/
112
- const getContentHtml = ( $el : Cheerio < Element > ) => {
110
+ const getContentHtml = ( $el : CheerioAnyNode ) => {
113
111
// Load HTML into a new instance, remove elements we don't want, then return the remainder
114
112
const $ = load ( $el . html ( ) ! , null , false ) ;
115
- $ ( "h1, h2, h3, h4, h5, h6, section, .change, .conformance-level" ) . remove ( ) ;
116
- return $ . html ( ) ;
113
+ $ ( contentIgnores . join ( ", " ) ) . remove ( ) ;
114
+ return $ . html ( ) . trim ( ) ;
117
115
} ;
118
116
119
- /**
120
- * Resolves information from guidelines/index.html;
121
- * comparable to the principles section of wcag.xml from the guidelines-xml Ant task.
122
- */
123
- export async function getPrinciples ( ) {
124
- const versions = await getInvertedGuidelinesVersions ( ) ;
125
- const $ = await flattenDomFromFile ( "guidelines/index.html" ) ;
126
-
117
+ /** Performs processing common across WCAG versions */
118
+ function processPrinciples ( $ : CheerioAPI ) {
127
119
const principles : Principle [ ] = [ ] ;
128
120
$ ( ".principle" ) . each ( ( i , el ) => {
129
121
const guidelines : Guideline [ ] = [ ] ;
130
- $ ( ".guideline" , el ) . each ( ( j , guidelineEl ) => {
122
+ $ ( "> .guideline" , el ) . each ( ( j , guidelineEl ) => {
131
123
const successCriteria : SuccessCriterion [ ] = [ ] ;
132
- $ ( ".sc" , guidelineEl ) . each ( ( k , scEl ) => {
133
- const resolvedVersion = versions [ scEl . attribs . id ] ;
134
- assertIsWcagVersion ( resolvedVersion ) ;
135
-
124
+ // Source uses sc class, published uses guideline class (again)
125
+ $ ( "> .guideline, > .sc" , guidelineEl ) . each ( ( k , scEl ) => {
126
+ const scId = scEl . attribs . id ;
136
127
successCriteria . push ( {
137
128
content : getContentHtml ( $ ( scEl ) ) ,
138
- id : scEl . attribs . id ,
129
+ id : scId ,
139
130
name : $ ( "h4" , scEl ) . text ( ) . trim ( ) ,
140
131
num : `${ i + 1 } .${ j + 1 } .${ k + 1 } ` ,
141
- level : $ ( "p.conformance-level" , scEl ) . text ( ) . trim ( ) as SuccessCriterion [ "level" ] ,
132
+ // conformance-level contains only letters in source, full (Level ...) in publish
133
+ level : $ ( "p.conformance-level" , scEl )
134
+ . text ( )
135
+ . trim ( )
136
+ . replace ( / ^ \( L e v e l ( .* ) \) $ / , "$1" ) as SuccessCriterion [ "level" ] ,
142
137
type : "SC" ,
143
- version : `WCAG ${ resolvedVersion } ` ,
138
+ version : scVersions [ scId ] ,
144
139
} ) ;
145
140
} ) ;
146
141
@@ -150,7 +145,7 @@ export async function getPrinciples() {
150
145
name : $ ( "h3" , guidelineEl ) . text ( ) . trim ( ) ,
151
146
num : `${ i + 1 } .${ j + 1 } ` ,
152
147
type : "Guideline" ,
153
- version : guidelineEl . attribs . id === "input-modalities" ? "WCAG21 " : "WCAG20 " ,
148
+ version : guidelineEl . attribs . id === "input-modalities" ? "21 " : "20 " ,
154
149
successCriteria,
155
150
} ) ;
156
151
} ) ;
@@ -161,14 +156,21 @@ export async function getPrinciples() {
161
156
name : $ ( "h2" , el ) . text ( ) . trim ( ) ,
162
157
num : `${ i + 1 } ` ,
163
158
type : "Principle" ,
164
- version : "WCAG20 " ,
159
+ version : "20 " ,
165
160
guidelines,
166
161
} ) ;
167
162
} ) ;
168
163
169
164
return principles ;
170
165
}
171
166
167
+ /**
168
+ * Resolves information from guidelines/index.html;
169
+ * comparable to the principles section of wcag.xml from the guidelines-xml Ant task.
170
+ */
171
+ export const getPrinciples = async ( ) =>
172
+ processPrinciples ( await flattenDomFromFile ( "guidelines/index.html" ) ) ;
173
+
172
174
/**
173
175
* Returns a flattened object hash, mapping shortcodes to each principle/guideline/SC.
174
176
*/
@@ -225,3 +227,62 @@ export async function getTermsMap() {
225
227
226
228
return terms ;
227
229
}
230
+
231
+ // Version-specific APIs
232
+
233
+ const remoteGuidelines$ : Partial < Record < WcagVersion , CheerioAPI > > = { } ;
234
+
235
+ /** Loads guidelines from TR space for specific version, caching for future calls. */
236
+ const loadRemoteGuidelines = async ( version : WcagVersion ) => {
237
+ if ( ! remoteGuidelines$ [ version ] ) {
238
+ const $ = load (
239
+ ( await axios . get ( `https://www.w3.org/TR/WCAG${ version } /` , { responseType : "text" } ) ) . data
240
+ ) ;
241
+
242
+ // Re-collapse definition links and notes, to be processed by this build system
243
+ $ ( ".guideline a.internalDFN" ) . removeAttr ( "class data-link-type id href title" ) ;
244
+ $ ( ".guideline [role='note'] .marker" ) . remove ( ) ;
245
+ $ ( ".guideline [role='note']" ) . find ( "> div, > p" ) . addClass ( "note" ) . unwrap ( ) ;
246
+
247
+ // Bibliography references are not processed in Understanding SC boxes
248
+ $ ( ".guideline cite:has(a.bibref:only-child)" ) . each ( ( _ , el ) => {
249
+ const $el = $ ( el ) ;
250
+ const $parent = $el . parent ( ) ;
251
+ $el . remove ( ) ;
252
+ // Remove surrounding square brackets (which aren't in a dedicated element)
253
+ $parent . html ( $parent . html ( ) ! . replace ( / \[ \] / g, "" ) ) ;
254
+ } ) ;
255
+
256
+ // Remove extra markup from headings so they can be parsed for names
257
+ $ ( "bdi" ) . remove ( ) ;
258
+
259
+ // Remove abbr elements which exist only in TR, not in informative docs
260
+ $ ( "#acknowledgements li abbr" ) . each ( ( _ , abbrEl ) => {
261
+ $ ( abbrEl ) . replaceWith ( $ ( abbrEl ) . text ( ) ) ;
262
+ } ) ;
263
+
264
+ remoteGuidelines$ [ version ] = $ ;
265
+ }
266
+ return remoteGuidelines$ [ version ] ! ;
267
+ } ;
268
+
269
+ /**
270
+ * Retrieves heading and content information for acknowledgement subsections,
271
+ * for preserving the section in About pages for earlier versions.
272
+ */
273
+ export const getAcknowledgementsForVersion = async ( version : WcagVersion ) => {
274
+ const $ = await loadRemoteGuidelines ( version ) ;
275
+ const subsections : Record < string , string > = { } ;
276
+
277
+ $ ( "section#acknowledgements section" ) . each ( ( _ , el ) => {
278
+ subsections [ el . attribs . id ] = $ ( ".header-wrapper + *" , el ) . html ( ) ! ;
279
+ } ) ;
280
+
281
+ return subsections ;
282
+ } ;
283
+
284
+ /**
285
+ * Retrieves and processes a pinned WCAG version using published guidelines.
286
+ */
287
+ export const getPrinciplesForVersion = async ( version : WcagVersion ) =>
288
+ processPrinciples ( await loadRemoteGuidelines ( version ) ) ;
0 commit comments