reffy 20.0.9 → 20.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/browserlib/extract-events.mjs +489 -439
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reffy",
|
|
3
|
-
"version": "20.0.
|
|
3
|
+
"version": "20.0.11",
|
|
4
4
|
"description": "W3C/WHATWG spec dependencies exploration companion. Features a short set of tools to study spec references as well as WebIDL term definitions and references found in W3C specifications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
"main": "index.js",
|
|
34
34
|
"bin": "./reffy.js",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"ajv": "8.
|
|
36
|
+
"ajv": "8.18.0",
|
|
37
37
|
"ajv-formats": "3.0.1",
|
|
38
|
-
"commander": "14.0.
|
|
38
|
+
"commander": "14.0.3",
|
|
39
39
|
"fetch-filecache-for-crawling": "5.1.1",
|
|
40
|
-
"puppeteer": "24.
|
|
40
|
+
"puppeteer": "24.37.3",
|
|
41
41
|
"semver": "^7.3.5",
|
|
42
|
-
"web-specs": "3.
|
|
42
|
+
"web-specs": "3.79.0",
|
|
43
43
|
"webidl2": "24.5.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"respec": "35.6.1",
|
|
47
47
|
"respec-hljs": "2.1.1",
|
|
48
|
-
"rollup": "4.57.
|
|
48
|
+
"rollup": "4.57.1",
|
|
49
49
|
"undici": "^7.0.0"
|
|
50
50
|
},
|
|
51
51
|
"overrides": {
|
|
@@ -1,439 +1,489 @@
|
|
|
1
|
-
import informativeSelector from './informative-selector.mjs';
|
|
2
|
-
import extractWebIdl from './extract-webidl.mjs';
|
|
3
|
-
import {parse} from "../../node_modules/webidl2/index.js";
|
|
4
|
-
import getAbsoluteUrl from './get-absolute-url.mjs';
|
|
5
|
-
|
|
6
|
-
const singlePage = !document.querySelector('[data-reffy-page]');
|
|
7
|
-
const href = el => el?.getAttribute("id") ? getAbsoluteUrl(el, {singlePage}) : null;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export default function (spec) {
|
|
11
|
-
// Used to find eventhandler attributes
|
|
12
|
-
let idlInterfaces = [];
|
|
13
|
-
try {
|
|
14
|
-
const idl = extractWebIdl();
|
|
15
|
-
const idlTree = parse(idl);
|
|
16
|
-
idlInterfaces = idlTree.filter(item =>
|
|
17
|
-
item.type === "interface" ||
|
|
18
|
-
item.type === "interface mixin");
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
// Spec defines some invalid Web IDL, proceed without it
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// associate event names from event handlers to interfaces with such an handler
|
|
25
|
-
const handledEventNames = idlInterfaces
|
|
26
|
-
.map(iface => iface.members
|
|
27
|
-
.filter(m => m.idlType?.idlType === "EventHandler" && m.type === "attribute" && m.name?.startsWith("on"))
|
|
28
|
-
.map(m => [m.name.slice(2), iface.name]))
|
|
29
|
-
.flat()
|
|
30
|
-
.reduce((acc, b) => {
|
|
31
|
-
if (!acc[b[0]]) acc[b[0]] = [];
|
|
32
|
-
acc[b[0]].push(b[1]);
|
|
33
|
-
return acc;
|
|
34
|
-
}, {});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
table.querySelectorAll("
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
if (
|
|
134
|
-
event.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
event.
|
|
295
|
-
} else if (
|
|
296
|
-
event.
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
1
|
+
import informativeSelector from './informative-selector.mjs';
|
|
2
|
+
import extractWebIdl from './extract-webidl.mjs';
|
|
3
|
+
import {parse} from "../../node_modules/webidl2/index.js";
|
|
4
|
+
import getAbsoluteUrl from './get-absolute-url.mjs';
|
|
5
|
+
|
|
6
|
+
const singlePage = !document.querySelector('[data-reffy-page]');
|
|
7
|
+
const href = el => el?.getAttribute("id") ? getAbsoluteUrl(el, {singlePage}) : null;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export default function (spec) {
|
|
11
|
+
// Used to find eventhandler attributes
|
|
12
|
+
let idlInterfaces = [];
|
|
13
|
+
try {
|
|
14
|
+
const idl = extractWebIdl();
|
|
15
|
+
const idlTree = parse(idl);
|
|
16
|
+
idlInterfaces = idlTree.filter(item =>
|
|
17
|
+
item.type === "interface" ||
|
|
18
|
+
item.type === "interface mixin");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Spec defines some invalid Web IDL, proceed without it
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// associate event names from event handlers to interfaces with such an handler
|
|
25
|
+
const handledEventNames = idlInterfaces
|
|
26
|
+
.map(iface => iface.members
|
|
27
|
+
.filter(m => m.idlType?.idlType === "EventHandler" && m.type === "attribute" && m.name?.startsWith("on"))
|
|
28
|
+
.map(m => [m.name.slice(2), iface.name]))
|
|
29
|
+
.flat()
|
|
30
|
+
.reduce((acc, b) => {
|
|
31
|
+
if (!acc[b[0]]) acc[b[0]] = [];
|
|
32
|
+
acc[b[0]].push(b[1]);
|
|
33
|
+
return acc;
|
|
34
|
+
}, {});
|
|
35
|
+
|
|
36
|
+
// Return true if the second event object describes the same event as the
|
|
37
|
+
// first one. Note event types defined in event tables typically complete
|
|
38
|
+
// event definitions for which we don't have any target information.
|
|
39
|
+
function isSameEvent(e1, e2) {
|
|
40
|
+
const res = e1.type === e2.type &&
|
|
41
|
+
((e1.href && e1.href === e2.href ) ||
|
|
42
|
+
(e1.targets?.sort()?.join("|") === e2.targets?.sort()?.join("|")) ||
|
|
43
|
+
(e2.src.format === 'event table'));
|
|
44
|
+
if (res && e1.cancelable !== undefined && e2.cancelable !== undefined && e1.cancelable !== e2.cancelable) {
|
|
45
|
+
console.error(`[reffy] Found two occurrences of same event with different "cancelable" properties in ${spec.title}: type=${e1.type} targets=${e1.targets.join(', ')} href=${e1.href}`);
|
|
46
|
+
}
|
|
47
|
+
return res;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fromEventElementToTargetInterfaces(eventEl) {
|
|
51
|
+
if (!eventEl) return;
|
|
52
|
+
|
|
53
|
+
if (eventEl.dataset?.dfnFor || eventEl.dataset?.linkFor) {
|
|
54
|
+
return (eventEl.dataset.dfnFor || eventEl.dataset.linkFor).split(",").map(t => t.trim());
|
|
55
|
+
} else if (eventEl.getAttribute("href")?.startsWith("#")) {
|
|
56
|
+
const dfn = document.getElementById(eventEl.getAttribute("href").slice(1));
|
|
57
|
+
if (dfn && dfn.dataset?.dfnFor) {
|
|
58
|
+
return dfn.dataset.dfnFor.split(",").map(t => t.trim());
|
|
59
|
+
}
|
|
60
|
+
} else if (handledEventNames[eventEl.textContent]?.length) {
|
|
61
|
+
// Search for on<event> EventHandler in IDL
|
|
62
|
+
const matchingInterfaces = handledEventNames[eventEl.textContent];
|
|
63
|
+
if (matchingInterfaces.length === 1) {
|
|
64
|
+
// only one such handler, we assume it's a match
|
|
65
|
+
return matchingInterfaces;
|
|
66
|
+
} else {
|
|
67
|
+
console.error(`[reffy] Multiple event handler named ${eventEl.textContent}, cannot associate reliably to an interface in ${spec.title}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
let events = [];
|
|
74
|
+
|
|
75
|
+
// Look for event summary tables
|
|
76
|
+
// ignore DOM spec which uses a matching table format
|
|
77
|
+
// to map to legacy event types
|
|
78
|
+
let hasStructuredData = false;
|
|
79
|
+
if (spec.shortname !== "dom") {
|
|
80
|
+
document.querySelectorAll("table").forEach(table => {
|
|
81
|
+
const firstHeading = table.querySelector("thead tr th")?.textContent?.trim();
|
|
82
|
+
if (firstHeading?.match(/^Event/) && firstHeading !== "Event handler") {
|
|
83
|
+
hasStructuredData = true;
|
|
84
|
+
// Useful e.g. for pointerevents
|
|
85
|
+
const bubblingInfoColumn = [...table.querySelectorAll("thead th")]
|
|
86
|
+
.findIndex(n => n.textContent.trim().match(/^bubbl/i));
|
|
87
|
+
const cancelableInfoColumn = [...table.querySelectorAll("thead th")]
|
|
88
|
+
.findIndex(n => n.textContent.trim().match(/^cancel/i));
|
|
89
|
+
const interfaceColumn = [...table.querySelectorAll("thead th")]
|
|
90
|
+
.findIndex(n => n.textContent.trim().match(/^(dom )?interface/i));
|
|
91
|
+
const targetsColumn = [...table.querySelectorAll("thead th")]
|
|
92
|
+
.findIndex(n => n.textContent.trim().match(/target/i));
|
|
93
|
+
|
|
94
|
+
table.querySelectorAll("tbody tr").forEach(tr => {
|
|
95
|
+
const event = {};
|
|
96
|
+
// clean up possible MDN annotations
|
|
97
|
+
// but keeping the original to swap it back in after processing
|
|
98
|
+
// to leave the DOM intact for other processing scripts
|
|
99
|
+
// (we need the clean up node in-tree to compute the proper href)
|
|
100
|
+
const origEventEl = tr.querySelector("*:first-child");
|
|
101
|
+
const eventEl = origEventEl.cloneNode(true);
|
|
102
|
+
origEventEl.replaceWith(eventEl);
|
|
103
|
+
const annotations = eventEl.querySelectorAll("aside, .mdn-anno");
|
|
104
|
+
annotations.forEach(n => n.remove());
|
|
105
|
+
|
|
106
|
+
let el = eventEl.querySelector("dfn,a");
|
|
107
|
+
if (!el) {
|
|
108
|
+
el = eventEl.querySelector("code");
|
|
109
|
+
}
|
|
110
|
+
if (!el) {
|
|
111
|
+
eventEl.replaceWith(origEventEl);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (el.tagName === "DFN" && el.id) {
|
|
115
|
+
event.href = href(el);
|
|
116
|
+
} else if (el.tagName === "A") {
|
|
117
|
+
if (!el.getAttribute("href").startsWith("https://")) {
|
|
118
|
+
const url = new URL(el.href);
|
|
119
|
+
event.href = href(document.getElementById(url.hash.slice(1)));
|
|
120
|
+
} else {
|
|
121
|
+
event.href = el.href;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
event.src = { format: "summary table", href: href(el.closest('*[id]')) };
|
|
125
|
+
event.type = eventEl.textContent.trim();
|
|
126
|
+
event.targets = fromEventElementToTargetInterfaces(eventEl.querySelector("dfn,a[href^='#']"));
|
|
127
|
+
if (bubblingInfoColumn >= 0) {
|
|
128
|
+
event.bubbles = tr.querySelector(`td:nth-child(${bubblingInfoColumn + 1})`)?.textContent?.trim() === "Yes";
|
|
129
|
+
}
|
|
130
|
+
if (cancelableInfoColumn >= 0) {
|
|
131
|
+
event.cancelable = !!tr.querySelector(`td:nth-child(${cancelableInfoColumn + 1})`)?.textContent?.trim().match(/(yes)|✓|(varies)/i);
|
|
132
|
+
}
|
|
133
|
+
if (interfaceColumn >= 0) {
|
|
134
|
+
event.interface =
|
|
135
|
+
tr.querySelector(`td:nth-child(${interfaceColumn + 1}) a`)?.textContent ??
|
|
136
|
+
tr.querySelector(`td:nth-child(${interfaceColumn + 1}) code`)?.textContent;
|
|
137
|
+
}
|
|
138
|
+
if (targetsColumn >= 0 && !event.targets) {
|
|
139
|
+
event.targets = tr.querySelector(`td:nth-child(${targetsColumn + 1})`)?.textContent?.split(',').map(t => t.trim());
|
|
140
|
+
}
|
|
141
|
+
events.push(event);
|
|
142
|
+
eventEl.replaceWith(origEventEl);
|
|
143
|
+
});
|
|
144
|
+
} else if (table.className === "def") {
|
|
145
|
+
// Used in https://drafts.csswg.org/css-nav-1/
|
|
146
|
+
const rowHeadings = [...table.querySelectorAll("tbody th")];
|
|
147
|
+
if (!rowHeadings.find(th => th.textContent.trim() === "Bubbles")) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const eventTypeRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim().match(/^type/i));
|
|
151
|
+
const bubblingInfoRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim() === "Bubbles");
|
|
152
|
+
const cancelableInfoRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim() === "Cancelable");
|
|
153
|
+
const interfaceRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim().match(/^interface/i));
|
|
154
|
+
const eventName = table.querySelector(`tr:nth-child(${eventTypeRow + 1}) td:nth-child(2)`)?.textContent?.trim();
|
|
155
|
+
const bubblesCell = table.querySelector(`tr:nth-child(${bubblingInfoRow + 1}) td:nth-child(2)`);
|
|
156
|
+
const bubbles = bubblesCell ? bubblesCell.textContent.trim() === "Yes" : null;
|
|
157
|
+
const cancelableCell = table.querySelector(`tr:nth-child(${cancelableInfoRow + 1}) td:nth-child(2)`);
|
|
158
|
+
const cancelable = cancelableCell ? cancelableCell.textContent.trim() === "Yes" : null;
|
|
159
|
+
const iface = table.querySelector(`tr:nth-child(${interfaceRow + 1}) td:nth-child(2)`)?.textContent?.trim();
|
|
160
|
+
if (eventName) {
|
|
161
|
+
events.push({
|
|
162
|
+
type: eventName, interface: iface, bubbles, cancelable,
|
|
163
|
+
src: { format: "css definition table", href: href(table.closest('*[id]')) },
|
|
164
|
+
href: href(table.closest('*[id]')) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Look for definitions in event-definition tables
|
|
171
|
+
// (used in Pointer Events and UI Events)
|
|
172
|
+
[...document.querySelectorAll('table.event-definition')].forEach(table => {
|
|
173
|
+
const properties = [...table.querySelectorAll('tr')]
|
|
174
|
+
.map(line => {
|
|
175
|
+
const nameEl = line.querySelector('th');
|
|
176
|
+
const valueEl = line.querySelector('td');
|
|
177
|
+
if (!nameEl || !valueEl) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
let name = nameEl.textContent.trim().toLowerCase();
|
|
181
|
+
let value = valueEl.textContent.trim();
|
|
182
|
+
if (name === 'trusted targets') {
|
|
183
|
+
name = 'targets';
|
|
184
|
+
value = value.split(',').map(v => v.trim());
|
|
185
|
+
}
|
|
186
|
+
if (['type', 'interface', 'targets'].includes(name)) {
|
|
187
|
+
return { name, value };
|
|
188
|
+
}
|
|
189
|
+
else if (['bubbles', 'cancelable'].includes(name)) {
|
|
190
|
+
return { name, value: value.toLowerCase() === 'yes' ? true : false };
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
.filter(prop => !!prop);
|
|
197
|
+
const event = {};
|
|
198
|
+
for (const prop of properties) {
|
|
199
|
+
event[prop.name] = prop.value;
|
|
200
|
+
}
|
|
201
|
+
event.src = {
|
|
202
|
+
format: 'event table',
|
|
203
|
+
href: href(table.closest('*[id]'))
|
|
204
|
+
};
|
|
205
|
+
// Prefer summary table to definition in an event table if both exist
|
|
206
|
+
// because the latter may include prose around the interface and target
|
|
207
|
+
// names that make it harder to extract meaningful values.
|
|
208
|
+
if (!events.find(e => isSameEvent(e, event))) {
|
|
209
|
+
events.push(event);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Look for the DOM-suggested sentence "Fire an event named X"
|
|
214
|
+
// or the Service Worker extension of "fire (a) functional event named"
|
|
215
|
+
const isFiringLink = a => a.href === "https://dom.spec.whatwg.org/#concept-event-fire" ||
|
|
216
|
+
a.href === "https://w3c.github.io/ServiceWorker/#fire-functional-event" ||
|
|
217
|
+
a.href === "https://www.w3.org/TR/service-workers-1/#fire-functional-event-algorithm" ||
|
|
218
|
+
a.href === "https://www.w3.org/TR/service-workers-1/#fire-functional-event" ||
|
|
219
|
+
a.href === "https://www.w3.org/TR/service-workers/#fire-functional-event-algorithm" ||
|
|
220
|
+
a.href === "https://www.w3.org/TR/service-workers/#fire-functional-event" ||
|
|
221
|
+
a.href === "https://w3c.github.io/pointerevents/#dfn-fire-a-pointer-event";
|
|
222
|
+
[...document.querySelectorAll("a")]
|
|
223
|
+
.filter(a => !a.closest(informativeSelector) && isFiringLink(a))
|
|
224
|
+
.forEach(a => {
|
|
225
|
+
// Clone and drop possible annotations to avoid extracting asides.
|
|
226
|
+
// (note the need to temporarily add the cloned node to the document
|
|
227
|
+
// so that ranges can be used)
|
|
228
|
+
const apos = [...a.parentNode.children].findIndex(c => c === a);
|
|
229
|
+
const container = a.parentNode.cloneNode(true);
|
|
230
|
+
const aclone = container.children[apos];
|
|
231
|
+
|
|
232
|
+
const annotations = container.querySelectorAll("aside, .mdn-anno");
|
|
233
|
+
annotations.forEach(n => n.remove());
|
|
234
|
+
document.body.appendChild(container);
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
// There can be multiple "fire an event" links in a container,
|
|
238
|
+
// limiting our text parsing to content in between two such links,
|
|
239
|
+
// or to the first time aside appears (no whitespaces in Bikeshed
|
|
240
|
+
// so code would extract the beginning of the annotation otherwise),
|
|
241
|
+
// or the end of the container if neither of the above occurs.
|
|
242
|
+
const range = document.createRange();
|
|
243
|
+
range.selectNode(container);
|
|
244
|
+
range.setStart(aclone, 0);
|
|
245
|
+
|
|
246
|
+
let nextFiringEl, curEl = aclone;
|
|
247
|
+
while ((curEl = curEl.nextElementSibling)) {
|
|
248
|
+
if (curEl.tagName === "A" && isFiringLink(curEl)) {
|
|
249
|
+
nextFiringEl = curEl;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (nextFiringEl) {
|
|
255
|
+
range.setEndBefore(nextFiringEl);
|
|
256
|
+
}
|
|
257
|
+
const parsedText = range.toString();
|
|
258
|
+
document.body.removeChild(container);
|
|
259
|
+
let phrasing;
|
|
260
|
+
let m = parsedText.match(/fir(e|ing)\s+a(n|\s+pointer)\s+event\s+named\s+"?(?<eventName>[a-z]+)/i);
|
|
261
|
+
if (m) {
|
|
262
|
+
if (m[2] === "n") {
|
|
263
|
+
phrasing = "fire an event";
|
|
264
|
+
} else {
|
|
265
|
+
phrasing = "fire a pointer event";
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
m = parsedText.match(/fir(e|ing)\sa?\s*functional\s+event\s+((named|given)\s+)?"?(?<eventName>[a-z]+)/i);
|
|
269
|
+
if (m) {
|
|
270
|
+
phrasing = "fire functional event";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (phrasing) {
|
|
275
|
+
const name = m.groups.eventName;
|
|
276
|
+
let newEvent = true;
|
|
277
|
+
let event = {
|
|
278
|
+
src: { format: "fire an event phrasing", href: href(a.closest('*[id]')) },
|
|
279
|
+
href: href(a.closest('*[id]'))
|
|
280
|
+
};
|
|
281
|
+
// this matches "fire an event named eventName" in battery-status and
|
|
282
|
+
// media capture main, named type in fullscreen, named e, event in html
|
|
283
|
+
// name in notifications API
|
|
284
|
+
if (name === 'eventName' || name === 'type' || name === 'e' || name === 'event' || name === 'name') {
|
|
285
|
+
return;
|
|
286
|
+
} else {
|
|
287
|
+
event.type = name;
|
|
288
|
+
// looking at the element following the link
|
|
289
|
+
// if its content match the name of the event
|
|
290
|
+
const eventEl = a.nextElementSibling?.textContent?.trim() === event.type ? a.nextElementSibling.querySelector("a,dfn") || a.nextElementSibling : null;
|
|
291
|
+
if (eventEl) {
|
|
292
|
+
if (eventEl.tagName === "A" && eventEl.getAttribute("href")) {
|
|
293
|
+
// use the target of the link as our href
|
|
294
|
+
event.href = eventEl.href;
|
|
295
|
+
} else if (eventEl.tagName === "DFN" && eventEl.id) {
|
|
296
|
+
event.href = href(eventEl);
|
|
297
|
+
}
|
|
298
|
+
event.targets = fromEventElementToTargetInterfaces(eventEl);
|
|
299
|
+
}
|
|
300
|
+
// if we have already detected this combination, skip it
|
|
301
|
+
if (events.find(e => isSameEvent(event, e))) {
|
|
302
|
+
newEvent = false;
|
|
303
|
+
event = events.find(e => isSameEvent(event, e));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!event.interface) {
|
|
307
|
+
let curEl = aclone, iface;
|
|
308
|
+
while ((curEl = curEl.nextElementSibling) && curEl !== nextFiringEl) {
|
|
309
|
+
if (curEl.textContent.match(/^([A-Z]+[a-z0-9]*)+Event$/)) {
|
|
310
|
+
iface = curEl.textContent.trim();
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (iface) {
|
|
315
|
+
event.interface = iface;
|
|
316
|
+
} else {
|
|
317
|
+
// Fire an event ⇒ Event interface
|
|
318
|
+
if (phrasing === "fire an event") {
|
|
319
|
+
event.interface = "Event";
|
|
320
|
+
} else if (phrasing === "fire a pointer event") {
|
|
321
|
+
// Fire a pointerevent ⇒ PointerEvent interface
|
|
322
|
+
event.interface = "PointerEvent";
|
|
323
|
+
} else {
|
|
324
|
+
// Functional event ⇒ Extendable interface
|
|
325
|
+
event.interface = "ExtendableEvent";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (event.bubbles === undefined && event.cancelable === undefined) {
|
|
330
|
+
if (parsedText.match(/bubbles and cancelable attributes/)) {
|
|
331
|
+
if (parsedText.match(/true/)) {
|
|
332
|
+
event.bubbles = true;
|
|
333
|
+
event.cancelable = true;
|
|
334
|
+
} else if (parsedText.match(/false/)) {
|
|
335
|
+
event.bubbles = false;
|
|
336
|
+
event.cancelable = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (event.bubbles === undefined) {
|
|
341
|
+
if (parsedText.match(/bubbles attribute/)) {
|
|
342
|
+
if (parsedText.match(/true/)) {
|
|
343
|
+
event.bubbles = true;
|
|
344
|
+
} else if (parsedText.match(/false/)) {
|
|
345
|
+
event.bubbles = false;
|
|
346
|
+
}
|
|
347
|
+
} else if (parsedText.match(/bubbles/) || parsedText.match(/bubbling/)) {
|
|
348
|
+
event.bubbles = true;
|
|
349
|
+
} else if (parsedText.match(/not bubble/)) {
|
|
350
|
+
event.bubbles = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (event.cancelable === undefined) {
|
|
354
|
+
if (parsedText.match(/cancelable attribute/)) {
|
|
355
|
+
if (parsedText.match(/true/)) {
|
|
356
|
+
event.cancelable = true;
|
|
357
|
+
} else if (parsedText.match(/false/)) {
|
|
358
|
+
event.cancelable = false;
|
|
359
|
+
}
|
|
360
|
+
} else if (parsedText.match(/not cancelable/) || parsedText.match(/not be cancelable/)) {
|
|
361
|
+
event.cancelable = false;
|
|
362
|
+
} else if (parsedText.match(/cancelable/)) {
|
|
363
|
+
event.cancelable = true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (newEvent) {
|
|
367
|
+
events.push(event);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// find events via IDL on<event> attributes with type EventHandler
|
|
373
|
+
for (let eventName of Object.keys(handledEventNames)) {
|
|
374
|
+
const matchingEvents = events.filter(e => e.type === eventName);
|
|
375
|
+
if (matchingEvents.length === 0 && !hasStructuredData) {
|
|
376
|
+
// We have not encountered such an event so far
|
|
377
|
+
for (let iface of handledEventNames[eventName]) {
|
|
378
|
+
events.push({
|
|
379
|
+
type: eventName, targets: [iface], interface: null,
|
|
380
|
+
src: { format: "IDL eventHandler", href: href(document.body) } }); // FIXME: find id of the IDL fragment
|
|
381
|
+
}
|
|
382
|
+
} else if (matchingEvents.length === 1) {
|
|
383
|
+
// A single matching event, we assume all event handlers relate to it
|
|
384
|
+
const [matchingEvent] = matchingEvents;
|
|
385
|
+
// assign all interfaces at once if none is set
|
|
386
|
+
// but don't add to existing interfaces otherwise
|
|
387
|
+
if (!matchingEvent.targets) {
|
|
388
|
+
matchingEvent.targets = handledEventNames[eventName];
|
|
389
|
+
} else if (!hasStructuredData) {
|
|
390
|
+
const missingIface = handledEventNames[eventName]
|
|
391
|
+
.find(iface => !matchingEvent.targets.includes(iface));
|
|
392
|
+
if (missingIface) {
|
|
393
|
+
console.warn(`[reffy] More event handlers matching name ${eventName}, e.g. on ${missingIface} than ones identified in spec definitions`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
// More than one event with that name
|
|
398
|
+
// we can only check if this matches known information
|
|
399
|
+
// to warn of the gap otherwise
|
|
400
|
+
for (let iface of handledEventNames[eventName]) {
|
|
401
|
+
if (!matchingEvents.find(e => e.targets?.includes(iface))) {
|
|
402
|
+
console.warn(`[reffy] Could not determine which event named ${eventName} match EventHandler of ${iface} interface in ${spec.title}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Find definitions marked as of event type
|
|
409
|
+
[...document.querySelectorAll('dfn[data-dfn-type="event"')].forEach(dfn => {
|
|
410
|
+
const type = dfn.textContent.trim();
|
|
411
|
+
const container = dfn.parentNode;
|
|
412
|
+
const event = {
|
|
413
|
+
type, interface: null, targets: fromEventElementToTargetInterfaces(dfn),
|
|
414
|
+
src: { format: "dfn", href: href(dfn.closest("*[id]")) },
|
|
415
|
+
href: href(dfn)
|
|
416
|
+
};
|
|
417
|
+
// CSS Animations & Transitions uses dt/dd to describe events
|
|
418
|
+
// and uses a ul in the dd to describe bubbling behavior
|
|
419
|
+
let bubbles, iface, cancelable;
|
|
420
|
+
if (container.tagName === "DT") {
|
|
421
|
+
const bubbleItem = [...container.nextElementSibling.querySelectorAll("li")]
|
|
422
|
+
.find(li => li.textContent.startsWith("Bubbles:"));
|
|
423
|
+
if (bubbleItem) {
|
|
424
|
+
bubbles = !!bubbleItem.textContent.match(/yes/i);
|
|
425
|
+
}
|
|
426
|
+
const cancelableItem = [...container.nextElementSibling.querySelectorAll("li")]
|
|
427
|
+
.find(li => li.textContent.startsWith("Cancelable:"));
|
|
428
|
+
if (cancelableItem) {
|
|
429
|
+
cancelable = !!cancelableItem.textContent.match(/yes/i);
|
|
430
|
+
}
|
|
431
|
+
// CSS Animation & Transitions document the event in the heading
|
|
432
|
+
// of the section where the definitions are located
|
|
433
|
+
let currentEl = container.parentNode;
|
|
434
|
+
while(currentEl) {
|
|
435
|
+
if (currentEl.tagName.match(/^H[1-6]$/)) {
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
currentEl = currentEl.previousElementSibling;
|
|
439
|
+
}
|
|
440
|
+
const interfaceEl = currentEl?.querySelector("code");
|
|
441
|
+
if (interfaceEl?.textContent?.match(/^[A-Z][a-z]+Event$/)) {
|
|
442
|
+
iface = interfaceEl.textContent;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const ev = events.find(e => isSameEvent(event, e));
|
|
446
|
+
if (!ev) {
|
|
447
|
+
if (iface) {
|
|
448
|
+
event.interface = iface;
|
|
449
|
+
}
|
|
450
|
+
event.bubbles = bubbles;
|
|
451
|
+
event.cancelable = cancelable;
|
|
452
|
+
events.push(event);
|
|
453
|
+
if (!iface) {
|
|
454
|
+
console.error(`[reffy] No interface hint found for event definition ${event.type} in ${spec.title}`);
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
if (iface) {
|
|
458
|
+
ev.interface = iface;
|
|
459
|
+
}
|
|
460
|
+
if (!ev.href && event.href) {
|
|
461
|
+
ev.href = event.href;
|
|
462
|
+
}
|
|
463
|
+
if (bubbles !== undefined) {
|
|
464
|
+
ev.bubbles = bubbles;
|
|
465
|
+
}
|
|
466
|
+
if (cancelable !== undefined) {
|
|
467
|
+
ev.cancelable = cancelable;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return events
|
|
473
|
+
.map(e => {
|
|
474
|
+
// Drop null properties (mandated by the schema for event extracts)
|
|
475
|
+
if (e.hasOwnProperty('interface') && !e.interface) {
|
|
476
|
+
delete e.interface;
|
|
477
|
+
}
|
|
478
|
+
if (e.hasOwnProperty('href') && !e.href) {
|
|
479
|
+
delete e.href;
|
|
480
|
+
}
|
|
481
|
+
if (e.src && e.src.hasOwnProperty('href') && !e.src.href) {
|
|
482
|
+
delete e.src.href;
|
|
483
|
+
}
|
|
484
|
+
return e;
|
|
485
|
+
})
|
|
486
|
+
.map(e => e.href && !e.href.startsWith(window.location.toString()) ?
|
|
487
|
+
Object.assign(e, {isExtension: true}) :
|
|
488
|
+
e) ;
|
|
489
|
+
}
|