reffy 15.0.2 → 15.2.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reffy",
3
- "version": "15.0.2",
3
+ "version": "15.2.0",
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
  "bin": "./reffy.js",
34
34
  "dependencies": {
35
35
  "ajv": "8.12.0",
36
- "ajv-formats": "2.1.1",
36
+ "ajv-formats": "3.0.1",
37
37
  "commander": "12.0.0",
38
38
  "fetch-filecache-for-crawling": "5.1.1",
39
- "puppeteer": "22.6.3",
39
+ "puppeteer": "22.6.5",
40
40
  "semver": "^7.3.5",
41
- "web-specs": "3.7.1",
41
+ "web-specs": "3.8.0",
42
42
  "webidl2": "24.4.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "mocha": "10.4.0",
46
46
  "respec": "34.5.0",
47
47
  "respec-hljs": "2.1.1",
48
- "rollup": "4.14.1",
48
+ "rollup": "4.14.3",
49
49
  "undici": "^6.1.0"
50
50
  },
51
51
  "overrides": {
@@ -15,6 +15,7 @@
15
15
  "items": { "$ref": "../common.json#/$defs/interface" }
16
16
  },
17
17
  "bubbles": { "type": "boolean" },
18
+ "cancelable": { "type": "boolean" },
18
19
  "isExtension": { "type": "boolean" },
19
20
  "href": { "$ref": "../common.json#/$defs/url" },
20
21
  "src": {
@@ -32,6 +32,7 @@
32
32
  "href": { "$ref": "../common.json#/$defs/url" }
33
33
  }
34
34
  },
35
+ "cancelable": { "type": "boolean" },
35
36
  "extendedIn": {
36
37
  "type": "array",
37
38
  "items": {
@@ -3,10 +3,6 @@ import extractWebIdl from './extract-webidl.mjs';
3
3
  import {parse} from "../../node_modules/webidl2/index.js";
4
4
  import getAbsoluteUrl from './get-absolute-url.mjs';
5
5
 
6
- const isSameEvent = (e1, e2) => e1.type === e2.type &&
7
- ((e1.href && e1.href === e2.href ) ||
8
- (e1.targets?.sort()?.join("|") === e2.targets?.sort()?.join("|")));
9
-
10
6
  const singlePage = !document.querySelector('[data-reffy-page]');
11
7
  const href = el => el?.getAttribute("id") ? getAbsoluteUrl(el, {singlePage}) : null;
12
8
 
@@ -37,6 +33,15 @@ export default function (spec) {
37
33
  return acc;
38
34
  }, {});
39
35
 
36
+ function isSameEvent(e1, e2) {
37
+ const res = e1.type === e2.type &&
38
+ ((e1.href && e1.href === e2.href ) ||
39
+ (e1.targets?.sort()?.join("|") === e2.targets?.sort()?.join("|")));
40
+ if (res && e1.cancelable !== undefined && e2.cancelable !== undefined && e1.cancelable !== e2.cancelable) {
41
+ 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}`);
42
+ }
43
+ return res;
44
+ }
40
45
 
41
46
  function fromEventElementToTargetInterfaces(eventEl) {
42
47
  if (!eventEl) return;
@@ -74,6 +79,8 @@ export default function (spec) {
74
79
  // Useful e.g. for pointerevents
75
80
  const bubblingInfoColumn = [...table.querySelectorAll("thead th")]
76
81
  .findIndex(n => n.textContent.trim().match(/^bubbl/i));
82
+ const cancelableInfoColumn = [...table.querySelectorAll("thead th")]
83
+ .findIndex(n => n.textContent.trim().match(/^cancel/i));
77
84
  const interfaceColumn = [...table.querySelectorAll("thead th")]
78
85
  .findIndex(n => n.textContent.trim().match(/^(dom )?interface/i));
79
86
  const targetsColumn = [...table.querySelectorAll("thead th")]
@@ -115,6 +122,9 @@ export default function (spec) {
115
122
  if (bubblingInfoColumn >= 0) {
116
123
  event.bubbles = tr.querySelector(`td:nth-child(${bubblingInfoColumn + 1})`)?.textContent?.trim() === "Yes";
117
124
  }
125
+ if (cancelableInfoColumn >= 0) {
126
+ event.cancelable = !!tr.querySelector(`td:nth-child(${cancelableInfoColumn + 1})`)?.textContent?.trim().match(/(yes)|✓|(varies)/i);
127
+ }
118
128
  if (interfaceColumn >= 0) {
119
129
  event.interface =
120
130
  tr.querySelector(`td:nth-child(${interfaceColumn + 1}) a`)?.textContent ??
@@ -134,14 +144,17 @@ export default function (spec) {
134
144
  }
135
145
  const eventTypeRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim().match(/^type/i));
136
146
  const bubblingInfoRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim() === "Bubbles");
147
+ const cancelableInfoRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim() === "Cancelable");
137
148
  const interfaceRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim().match(/^interface/i));
138
149
  const eventName = table.querySelector(`tr:nth-child(${eventTypeRow + 1}) td:nth-child(2)`)?.textContent?.trim();
139
150
  const bubblesCell = table.querySelector(`tr:nth-child(${bubblingInfoRow + 1}) td:nth-child(2)`);
140
151
  const bubbles = bubblesCell ? bubblesCell.textContent.trim() === "Yes" : null;
152
+ const cancelableCell = table.querySelector(`tr:nth-child(${cancelableInfoRow + 1}) td:nth-child(2)`);
153
+ const cancelable = cancelableCell ? cancelableCell.textContent.trim() === "Yes" : null;
141
154
  const iface = table.querySelector(`tr:nth-child(${interfaceRow + 1}) td:nth-child(2)`)?.textContent?.trim();
142
155
  if (eventName) {
143
156
  events.push({
144
- type: eventName, interface: iface, bubbles,
157
+ type: eventName, interface: iface, bubbles, cancelable,
145
158
  src: { format: "css definition table", href: href(table.closest('*[id]')) },
146
159
  href: href(table.closest('*[id]')) });
147
160
  }
@@ -208,6 +221,7 @@ export default function (spec) {
208
221
  phrasing = "fire functional event";
209
222
  }
210
223
  }
224
+
211
225
  if (phrasing) {
212
226
  const name = m.groups.eventName;
213
227
  let newEvent = true;
@@ -263,6 +277,17 @@ export default function (spec) {
263
277
  }
264
278
  }
265
279
  }
280
+ if (event.bubbles === undefined && event.cancelable === undefined) {
281
+ if (parsedText.match(/bubbles and cancelable attributes/)) {
282
+ if (parsedText.match(/true/)) {
283
+ event.bubbles = true;
284
+ event.cancelable = true;
285
+ } else if (parsedText.match(/false/)) {
286
+ event.bubbles = false;
287
+ event.cancelable = false;
288
+ }
289
+ }
290
+ }
266
291
  if (event.bubbles === undefined) {
267
292
  if (parsedText.match(/bubbles attribute/)) {
268
293
  if (parsedText.match(/true/)) {
@@ -276,6 +301,19 @@ export default function (spec) {
276
301
  event.bubbles = false;
277
302
  }
278
303
  }
304
+ if (event.cancelable === undefined) {
305
+ if (parsedText.match(/cancelable attribute/)) {
306
+ if (parsedText.match(/true/)) {
307
+ event.cancelable = true;
308
+ } else if (parsedText.match(/false/)) {
309
+ event.cancelable = false;
310
+ }
311
+ } else if (parsedText.match(/not cancelable/) || parsedText.match(/not be cancelable/)) {
312
+ event.cancelable = false;
313
+ } else if (parsedText.match(/cancelable/)) {
314
+ event.cancelable = true;
315
+ }
316
+ }
279
317
  if (newEvent) {
280
318
  events.push(event);
281
319
  }
@@ -329,13 +367,18 @@ export default function (spec) {
329
367
  };
330
368
  // CSS Animations & Transitions uses dt/dd to describe events
331
369
  // and uses a ul in the dd to describe bubbling behavior
332
- let bubbles, iface;
370
+ let bubbles, iface, cancelable;
333
371
  if (container.tagName === "DT") {
334
372
  const bubbleItem = [...container.nextElementSibling.querySelectorAll("li")]
335
373
  .find(li => li.textContent.startsWith("Bubbles:"));
336
374
  if (bubbleItem) {
337
375
  bubbles = !!bubbleItem.textContent.match(/yes/i);
338
376
  }
377
+ const cancelableItem = [...container.nextElementSibling.querySelectorAll("li")]
378
+ .find(li => li.textContent.startsWith("Cancelable:"));
379
+ if (cancelableItem) {
380
+ cancelable = !!cancelableItem.textContent.match(/yes/i);
381
+ }
339
382
  // CSS Animation & Transitions document the event in the heading
340
383
  // of the section where the definitions are located
341
384
  let currentEl = container.parentNode;
@@ -356,6 +399,7 @@ export default function (spec) {
356
399
  event.interface = iface;
357
400
  }
358
401
  event.bubbles = bubbles;
402
+ event.cancelable = cancelable;
359
403
  events.push(event);
360
404
  if (!iface) {
361
405
  console.error(`[reffy] No interface hint found for event definition ${event.type} in ${spec.title}`);
@@ -370,6 +414,9 @@ export default function (spec) {
370
414
  if (bubbles !== undefined) {
371
415
  ev.bubbles = bubbles;
372
416
  }
417
+ if (cancelable !== undefined) {
418
+ ev.cancelable = cancelable;
419
+ }
373
420
  }
374
421
  });
375
422
  return events
@@ -65,8 +65,14 @@ module.exports = {
65
65
  })
66
66
  .filter(event => !!event);
67
67
 
68
+ // Before we clean and sort the result, we'll consolidate events that
69
+ // don't always bubble. We'll call them... "babbling" events. Such events
70
+ // should remain exceptions to the rule, and will likely be artificially
71
+ // created through some patching mechanism (in Webref typically) because
72
+ // the events extraction logic does not (yet?) support this scenario.
68
73
  return events
69
74
  .filter(event => !eventsToDrop.includes(event))
75
+ .filter(event => consolidateBabblingEvent(event, events))
70
76
  .map(event => {
71
77
  cleanTargetInterfaces(event, parsedInterfaces);
72
78
  delete event.spec;
@@ -223,3 +229,31 @@ function extendEvent(event, events) {
223
229
  { spec: event.spec.series.shortname },
224
230
  event.src?.href ? { href: event.src?.href } : {}));
225
231
  }
232
+
233
+
234
+ /**
235
+ * Consolidate events that got duplicated in the extract because they bubble
236
+ * or don't bubble depending on the target interface.
237
+ *
238
+ * We'll say that these events "babble" because they don't seem to know whether
239
+ * they bubble or not.
240
+ */
241
+ function consolidateBabblingEvent(event, events) {
242
+ if (event.mergedIntoAnotherEvent) {
243
+ return null;
244
+ }
245
+ const newTargets = events
246
+ .filter(e =>
247
+ e !== event && !e.isExtension && !e.mergedIntoAnotherEvent &&
248
+ e.href && e.href === event.href && e.cancelable === event.cancelable)
249
+ .map(e => {
250
+ // Flag the event as merged so that we can filter it out afterwards
251
+ e.mergedIntoAnotherEvent = true;
252
+ return e.targets;
253
+ })
254
+ .flat();
255
+ if (newTargets.length > 0) {
256
+ event.targets = (event.targets || []).concat(newTargets);
257
+ }
258
+ return event;
259
+ }