@@ -63,11 +63,20 @@ log(`loading generated HTML "${options.html}"...`);
63
63
let file = await fs . readFile ( options . html , 'utf8' ) ;
64
64
65
65
log ( 'massaging HTML...' ) ;
66
- // node-html-parser doesn't understand that DT and DD are mutually self-closing;
66
+ // node-html-parser doesn't understand that some elements are mutually self-closing;
67
67
// tweak the source using regex magic.
68
- file = file . replaceAll (
69
- / ( < ( d t | d d ) \b [ ^ > ] * > ) ( .* ?) (? = < ( : ? d t | d d | \/ d l ) \b ) / sg,
70
- ( _ , opener , tag , content ) => `${ opener } ${ content } </${ tag } >` ) ;
68
+ [ { tags : [ 'dt' , 'dd' ] , containers : [ 'dl' ] } ,
69
+ { tags : [ 'thead' , 'tbody' , 'tfoot' ] , containers : [ 'table' ] } ,
70
+ { tags : [ 'tr' ] , containers : [ 'thead' , 'tbody' , 'tfoot' , 'table' ] } ,
71
+ ] . forEach ( ( { tags, containers} ) => {
72
+ const re = new RegExp (
73
+ '(<(' + tags . join ( '|' ) + ')\\b[^>]*>)' +
74
+ '(.*?)' +
75
+ '(?=<(:?' + tags . join ( '|' ) + '|/(' + containers . join ( '|' ) + '))\\b)' ,
76
+ 'sg' ) ;
77
+ file = file . replaceAll (
78
+ re , ( _ , opener , tag , content ) => `${ opener } ${ content } </${ tag } >` ) ;
79
+ } ) ;
71
80
72
81
log ( 'parsing HTML...' ) ;
73
82
const root = parse ( file , {
@@ -102,13 +111,25 @@ function error(message) {
102
111
103
112
function format ( match ) {
104
113
const CONTEXT = 20 ;
105
- const prefix = match . input . substring ( match . index - CONTEXT , match . index )
114
+
115
+ let prefix = match . input . substring ( match . index - CONTEXT , match . index )
106
116
. split ( / \n / )
107
117
. pop ( ) ;
108
- const suffix = match . input . substr ( match . index + match [ 0 ] . length , CONTEXT )
118
+ let suffix = match . input . substr ( match . index + match [ 0 ] . length , CONTEXT )
109
119
. split ( / \n / )
110
120
. shift ( ) ;
111
- return ( prefix . length === CONTEXT ? '...' : '' ) + prefix + match [ 0 ] + suffix +
121
+ let infix = match [ 0 ] ;
122
+
123
+ if ( infix . startsWith ( '\n' ) ) {
124
+ prefix = '' ;
125
+ infix = infix . slice ( 1 ) ;
126
+ }
127
+ if ( infix . endsWith ( '\n' ) ) {
128
+ suffix = '' ;
129
+ infix = infix . slice ( 0 , - 1 ) ;
130
+ }
131
+
132
+ return ( prefix . length === CONTEXT ? '...' : '' ) + prefix + infix + suffix +
112
133
( suffix . length === CONTEXT ? '...' : '' ) ;
113
134
}
114
135
@@ -128,22 +149,26 @@ const ALGORITHM_STEP_SELECTOR = '.algorithm li > p:not(.issue)';
128
149
// * `text` - rendered text content
129
150
// * `root.querySelectorAll()` - operate on DOM-like nodes
130
151
131
- // Look for merge markers
152
+ // Checks are marked with one of these tags:
153
+ // * [Generic] - could apply to any spec
154
+ // * [WebNN] - very specific to the WebNN spec
155
+
156
+ // [Generic] Look for merge markers
132
157
for ( const match of source . matchAll ( / < { 7 } | > { 7 } | ^ = { 7 } $ / mg) ) {
133
158
error ( `Merge conflict marker: ${ format ( match ) } ` ) ;
134
159
}
135
160
136
- // Look for residue of unterminated auto-links in rendered text
161
+ // [Generic] Look for residue of unterminated auto-links in rendered text
137
162
for ( const match of text . matchAll ( / ( { { | } } | \[ = | = \] ) / g) ) {
138
163
error ( `Unterminated autolink: ${ format ( match ) } ` ) ;
139
164
}
140
165
141
- // Look for duplicate words (in source, since [=realm=] |realm| is okay)
142
- for ( const match of html . matchAll ( / ( \ w+ ) \1 / g ) ) {
166
+ // [Generic] Look for duplicate words (in source, since [=realm=] |realm| is okay)
167
+ for ( const match of html . matchAll ( / (?: ^ | \s ) ( \ w+ ) \1(?: $ | \s ) / ig ) ) {
143
168
error ( `Duplicate word: ${ format ( match ) } ` ) ;
144
169
}
145
170
146
- // Verify IDL lines wrap to avoid horizontal scrollbars
171
+ // [Generic] Verify IDL lines wrap to avoid horizontal scrollbars
147
172
const MAX_IDL_WIDTH = 88 ;
148
173
for ( const idl of root . querySelectorAll ( 'pre.idl' ) ) {
149
174
idl . innerText . split ( / \n / ) . forEach ( line => {
@@ -154,19 +179,19 @@ for (const idl of root.querySelectorAll('pre.idl')) {
154
179
} ) ;
155
180
}
156
181
157
- // Look for undesired punctuation
182
+ // [WebNN] Look for undesired punctuation
158
183
for ( const match of text . matchAll ( / ( : : | × | ÷ | ∗ | − ) / g) ) {
159
184
error ( `Bad punctuation: ${ format ( match ) } ` ) ;
160
185
}
161
186
162
- // Look for undesired entity usage
187
+ // [WebNN] Look for undesired entity usage
163
188
for ( const match of source . matchAll ( / & ( \w + ) ; / g) ) {
164
189
if ( ! [ 'amp' , 'lt' , 'gt' , 'quot' ] . includes ( match [ 1 ] ) ) {
165
190
error ( `Avoid entities: ${ format ( match ) } ` ) ;
166
191
}
167
192
}
168
193
169
- // Look for undesired phrasing
194
+ // [WebNN] Look for undesired phrasing
170
195
for ( const match of source . matchAll ( / t h e ( \[ = .* ?= \] ) o f ( \| .* ?\| ) [ ^ , ] / g) ) {
171
196
error ( `Prefer "x's y" to "y of x": ${ format ( match ) } ` ) ;
172
197
}
@@ -180,17 +205,17 @@ for (const match of text.matchAll(/\bthe \S+ argument\b/g)) {
180
205
error ( `Drop 'the' and 'argument': ${ format ( match ) } ` ) ;
181
206
}
182
207
183
- // Look for incorrect use of shape for an MLOperandDescriptor
208
+ // [WebNN] Look for incorrect use of shape for an MLOperandDescriptor
184
209
for ( const match of source . matchAll ( / ( \| \w * d e s c \w * \| ) ' s \[ = M L O p e r a n d \/ s h a p e = \] / ig) ) {
185
210
error ( `Use ${ match [ 1 ] } .{{MLOperandDescriptor/dimensions}} not shape: ${ format ( match ) } ` ) ;
186
211
}
187
212
188
- // Look for missing dict-member dfns
213
+ // [Generic] Look for missing dict-member dfns
189
214
for ( const element of root . querySelectorAll ( '.idl dfn[data-dfn-type=dict-member]' ) ) {
190
215
error ( `Dictionary member missing dfn: ${ element . innerText } ` ) ;
191
216
}
192
217
193
- // Look for suspicious stuff in algorithm steps
218
+ // [WebNN] Look for suspicious stuff in algorithm steps
194
219
for ( const element of root . querySelectorAll ( ALGORITHM_STEP_SELECTOR ) ) {
195
220
// [] used for anything but indexing, slots, and refs
196
221
// Exclude \w[ for indexing (e.g. shape[n])
@@ -205,7 +230,7 @@ for (const element of root.querySelectorAll(ALGORITHM_STEP_SELECTOR)) {
205
230
}
206
231
}
207
232
208
- // Ensure vars are method/algorithm arguments, or initialized correctly
233
+ // [Generic] Ensure vars are method/algorithm arguments, or initialized correctly
209
234
for ( const algorithm of root . querySelectorAll ( '.algorithm' ) ) {
210
235
const vars = algorithm . querySelectorAll ( 'var' ) ;
211
236
const seen = new Set ( ) ;
@@ -253,17 +278,17 @@ for (const algorithm of root.querySelectorAll('.algorithm')) {
253
278
}
254
279
}
255
280
256
- // Eschew vars outside of algorithms.
281
+ // [Generic] Eschew vars outside of algorithms.
257
282
const algorithmVars = new Set ( root . querySelectorAll ( '.algorithm var' ) ) ;
258
283
for ( const v of root . querySelectorAll ( 'var' ) . filter ( v => ! algorithmVars . has ( v ) ) ) {
259
284
error ( `Variable outside of algorithm: ${ v . innerText } ` ) ;
260
285
}
261
286
262
-
263
- // Prevent accidental normative references to other specs. This reports an error
264
- // if there is a normative reference to any spec *other* than these ones. This
265
- // helps avoid an autolink like [=object=] adding an unexpected reference to
266
- // [FILEAPI]. Add to this list if a new normative reference is intended.
287
+ // [WebNN] Prevent accidental normative references to other specs. This reports
288
+ // an error if there is a normative reference to any spec *other* than these
289
+ // ones. This helps avoid an autolink like [=object=] adding an unexpected
290
+ // reference to [FILEAPI]. Add to this list if a new normative reference is
291
+ // intended.
267
292
const NORMATIVE_REFERENCES = new Set ( [
268
293
'[ECMASCRIPT]' ,
269
294
'[HTML]' ,
@@ -282,7 +307,7 @@ for (const term of root.querySelectorAll('#normative + dl > dt')) {
282
307
}
283
308
}
284
309
285
- // Detect syntax errors in JS.
310
+ // [Generic] Detect syntax errors in JS.
286
311
for ( const pre of root . querySelectorAll ( 'pre.highlight:not(.idl)' ) ) {
287
312
const script = pre . innerText . replaceAll ( / & a m p ; / g, '&' )
288
313
. replaceAll ( / & l t ; / g, '<' )
@@ -294,20 +319,20 @@ for (const pre of root.querySelectorAll('pre.highlight:not(.idl)')) {
294
319
}
295
320
}
296
321
297
- // Ensure algorithm steps end in '.' or ':'.
322
+ // [Generic] Ensure algorithm steps end in '.' or ':'.
298
323
for ( const p of root . querySelectorAll ( ALGORITHM_STEP_SELECTOR ) ) {
299
324
const match = p . innerText . match ( / [ ^ . : ] $ / ) ;
300
325
if ( match ) {
301
326
error ( `Algorithm steps should end with '.' or ':': ${ format ( match ) } ` ) ;
302
327
}
303
328
}
304
329
305
- // Avoid incorrect links to list/empty.
330
+ // [Generic] Avoid incorrect links to list/empty.
306
331
for ( const match of source . matchAll ( / i s ( n o t ) ? \[ = ( l i s t \/ | s t a c k \/ | q u e u e \/ | ) e m p t y = \] / g) ) {
307
332
error ( `Link to 'is empty' (adjective) not 'empty' (verb): ${ format ( match ) } ` ) ;
308
333
}
309
334
310
- // Ensure every method dfn is correctly associated with an interface.
335
+ // [Generic] Ensure every method dfn is correctly associated with an interface.
311
336
const interfaces = new Set (
312
337
root . querySelectorAll ( 'dfn[data-dfn-type=interface]' ) . map ( e => e . innerText ) ) ;
313
338
for ( const dfn of root . querySelectorAll ( 'dfn[data-dfn-type=method]' ) ) {
@@ -317,13 +342,13 @@ for (const dfn of root.querySelectorAll('dfn[data-dfn-type=method]')) {
317
342
}
318
343
}
319
344
320
- // Ensure every IDL argument is linked to a definition.
345
+ // [Generic] Ensure every IDL argument is linked to a definition.
321
346
for ( const dfn of root . querySelectorAll ( 'pre.idl dfn[data-dfn-type=argument]' ) ) {
322
347
const dfnFor = dfn . getAttribute ( 'data-dfn-for' ) ;
323
348
error ( `Missing <dfn argument for="${ dfnFor } ">${ dfn . innerText } </dfn> (or equivalent)` ) ;
324
349
}
325
350
326
- // Ensure every argument dfn is correctly associated with a method.
351
+ // [Generic] Ensure every argument dfn is correctly associated with a method.
327
352
// This tries to catch extraneous definitions, e.g. after an arg is removed.
328
353
for ( const dfn of root . querySelectorAll ( 'dfn[data-dfn-type=argument]' ) ) {
329
354
const dfnFor = dfn . getAttribute ( 'data-dfn-for' ) ;
@@ -332,8 +357,9 @@ for (const dfn of root.querySelectorAll('dfn[data-dfn-type=argument]')) {
332
357
}
333
358
}
334
359
335
- // Try to catch type mismatches like |tensor|.{{MLGraph/...}}. Note that the
336
- // test is keyed on the variable name; variables listed here are not validated.
360
+ // [WebNN] Try to catch type mismatches like |tensor|.{{MLGraph/...}}. Note that
361
+ // the test is keyed on the variable name; variables listed here are not
362
+ // validated.
337
363
for ( const match of source . matchAll ( / \| ( \w + ) \| \. { { ( \w + ) \/ .* ?} } / g) ) {
338
364
const [ _ , v , i ] = match ;
339
365
[ [ 'MLTensor' , [ 'tensor' ] ] ,
@@ -351,13 +377,42 @@ for (const match of source.matchAll(/\|(\w+)\|\.{{(\w+)\/.*?}}/g)) {
351
377
} ) ;
352
378
}
353
379
380
+ // [WebNN] Verify that linked constraints table IDs are reasonable. Bikeshed
381
+ // will flag any broken links; this just tries to ensure that links within the
382
+ // algorithm go to that algorithm's associated table.
383
+ for ( const algorithm of root . querySelectorAll (
384
+ '.algorithm[data-algorithm-for=MLGraphBuilder]' ) ) {
385
+ const name = algorithm . getAttribute ( 'data-algorithm' ) ;
386
+ if ( name . match ( / ^ ( \w + ) \( / ) ) {
387
+ const method = RegExp . $1 ;
388
+ for ( const href of algorithm . querySelectorAll ( 'a' )
389
+ . map ( a => a . getAttribute ( 'href' ) )
390
+ . filter ( href => href . match ( / # c o n s t r a i n t s - / ) ) ) {
391
+ // Allow either exact case or lowercase match for table ID.
392
+ if (
393
+ href !== '#constraints-' + method &&
394
+ href !== '#constraints-' + method . toLowerCase ( ) ) {
395
+ error ( `Steps for ${ method } () link to ${ href } ` ) ;
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ // [WebNN] Ensure constraints tables use linking not styling
402
+ for ( const table of root . querySelectorAll ( 'table.data' ) . filter ( e => e . id . startsWith ( 'constraints-' ) ) ) {
403
+ for ( const match of table . innerHTML . matchAll ( / < e m > (? ! o u t p u t ) ( \w + ) < \/ e m > / ig) ) {
404
+ error ( `Constraints table should link not style args: ${ format ( match ) } ` ) ;
405
+ }
406
+ }
407
+
354
408
// TODO: Generate this from the IDL itself.
355
409
const dictionaryTypes = [ 'MLOperandDescriptor' , 'MLContextLostInfo' ] ;
356
410
357
- // Ensure JS objects are created with explicit realm
411
+ // [Generic] Ensure JS objects are created with explicit realm
358
412
for ( const match of text . matchAll ( / a n e w p r o m i s e \b (? ! i n r e a l m ) / g) ) {
359
413
error ( `Promise creation must specify realm: ${ format ( match ) } ` ) ;
360
414
}
415
+ // [Generic] Ensure JS objects are created with explicit realm
361
416
for ( const match of text . matchAll ( / b e a n e w ( [ A - Z ] \w + ) \b (? ! i n r e a l m ) / g) ) {
362
417
const type = match [ 1 ] ;
363
418
// Dictionaries are just maps, so they don't need a realm.
0 commit comments