reffy 20.0.2 → 20.0.3

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": "20.0.2",
3
+ "version": "20.0.3",
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",
@@ -37,15 +37,15 @@
37
37
  "ajv-formats": "3.0.1",
38
38
  "commander": "14.0.2",
39
39
  "fetch-filecache-for-crawling": "5.1.1",
40
- "puppeteer": "24.30.0",
40
+ "puppeteer": "24.31.0",
41
41
  "semver": "^7.3.5",
42
- "web-specs": "3.71.0",
42
+ "web-specs": "3.73.0",
43
43
  "webidl2": "24.5.0"
44
44
  },
45
45
  "devDependencies": {
46
- "respec": "35.6.0",
46
+ "respec": "35.6.1",
47
47
  "respec-hljs": "2.1.1",
48
- "rollup": "4.53.2",
48
+ "rollup": "4.53.3",
49
49
  "undici": "^7.0.0"
50
50
  },
51
51
  "overrides": {
@@ -1,259 +1,245 @@
1
- /**
2
- * Post-processing module that consolidates events extracts into one extract
3
- * per event.
4
- */
5
-
6
- import { isLatestLevelThatPasses, getInterfaceTreeInfo } from '../lib/util.js';
7
-
8
- export default {
9
- dependsOn: ['events'],
10
- input: 'crawl',
11
- property: 'events',
12
-
13
- run: async function(crawl, options) {
14
- // Collect list of mixin interfaces
15
- const mixins = {};
16
- const parsedInterfaces = [];
17
- crawl.results.forEach(s => {
18
- if (s.idlparsed) {
19
- if (s.idlparsed.idlNames) {
20
- Object.values(s.idlparsed.idlNames).forEach(dfn => {
21
- if (dfn.type === 'interface' && !dfn.partial) {
22
- parsedInterfaces.push(dfn);
23
- }
24
- });
25
- }
26
- if (s.idlparsed.idlExtendedNames) {
27
- Object.keys(s.idlparsed.idlExtendedNames).forEach(n => {
28
- s.idlparsed.idlExtendedNames[n].forEach(f => {
29
- if (f.type === 'includes') {
30
- if (!mixins[f.includes]) mixins[f.includes] = [];
31
- mixins[f.includes].push(n);
32
- }
33
- });
34
- });
35
- }
36
- }
37
- });
38
-
39
- // Only consider latest spec in a series to avoid fake duplicates
40
- const results = crawl.results.filter(spec =>
41
- (spec.seriesComposition !== 'delta' && isLatestLevelThatPasses(spec, crawl.results, s => s.events)) ||
42
- (spec.seriesComposition === 'delta' && spec.events));
43
-
44
- // Update events in place
45
- const events = results.map(spec => spec.events.map(e => Object.assign({ spec: spec }, e))).flat();
46
- for (const event of events) {
47
- expandMixinTargets(event, mixins);
48
- setBubblingPerTarget(event, parsedInterfaces);
49
- }
50
-
51
- // Consolidate events extended in other specs
52
- const eventsToDrop = events
53
- .filter(event => event.isExtension)
54
- .map(event => {
55
- const err = extendEvent(event, events);
56
- if (err) {
57
- // Event could not be extended, let's keep extension event
58
- console.warn(err);
59
- return null;
60
- }
61
- else {
62
- // Event successfully extended, extension can be dropped
63
- return event;
64
- }
65
- })
66
- .filter(event => !!event);
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.
73
- return events
74
- .filter(event => !eventsToDrop.includes(event))
75
- .filter(event => consolidateBabblingEvent(event, events))
76
- .map(event => {
77
- cleanTargetInterfaces(event, parsedInterfaces);
78
- delete event.spec;
79
- return event;
80
- })
81
- .sort((event1, event2) =>
82
- event1.type.localeCompare(event2.type, 'en-US') ||
83
- (!event2.interface ? -1 : 0) ||
84
- (!event1.interface ? 1 : 0) ||
85
- event1.interface.localeCompare(event2.interface, 'en-US') ||
86
- (!event2.href ? -1 : 0) ||
87
- (!event1.href ? 1 : 0) ||
88
- event1.href.localeCompare(event2.href, 'en-US'));
89
- }
90
- };
91
-
92
-
93
- function expandMixinTargets(event, mixins) {
94
- const expandedTargets = event.targets?.map(i => mixins[i] || i)?.flat();
95
- // This assumes a mixin matches more than one interface
96
- if (expandedTargets && expandedTargets.length !== event.targets?.length) {
97
- event.targets = expandedTargets;
98
- return true;
99
- }
100
- return false;
101
- }
102
-
103
-
104
- function setBubblingPerTarget(event, parsedInterfaces) {
105
- // if an event targets an interface in a tree
106
- // but the root of the tree wasn't detected as a target
107
- // we can assume bubbles is false
108
- // (ideally, we should check the existence of the event handler on the
109
- // root interface, but there is no easy way to get a consolidated IDL view
110
- // of the root at the moment)
111
- if (!event.targets) return;
112
- const updatedTargets = [];
113
- const detected = {};
114
- const treeInterfaces = [];
115
- for (let iface of event.targets) {
116
- const treeInfo = getInterfaceTreeInfo(iface, parsedInterfaces);
117
- if (!treeInfo) {
118
- updatedTargets.push({target: iface});
119
- continue;
120
- }
121
- const { tree, depth, bubblingPath } = treeInfo;
122
- if (!detected[tree]) {
123
- detected[tree] = {root: false, nonroot: false};
124
- }
125
- if (depth === 0) {
126
- // bubbling doesn't matter on the root interface
127
- updatedTargets.push({target: iface});
128
- detected[tree].root = true;
129
- } else {
130
- treeInterfaces.push({ iface, bubblingPath });
131
- detected[tree].nonroot = true;
132
- }
133
- }
134
- // if the event is sent at targets in a tree, but isn't detected
135
- // on the root target, and no bubbling info is available,
136
- // assume it doesn't bubble
137
- if (Object.values(detected).length) {
138
- if (!event.hasOwnProperty('bubbles') && Object.values(detected).every(x => !x.root && x.nonroot )) {
139
- event.bubbles = false;
140
- }
141
- }
142
- for (let { iface, bubblingPath } of treeInterfaces) {
143
- if (event.hasOwnProperty('bubbles')) {
144
- updatedTargets.push(Object.assign(
145
- { target: iface, bubbles: event.bubbles },
146
- event.bubbles ? { bubblingPath } : {}));
147
- }
148
- }
149
- event.targets = updatedTargets;
150
- delete event.bubbles;
151
- }
152
-
153
-
154
- /**
155
- * Filter the list of target interfaces to remove those that don't need to
156
- * appear explicitly because they are de facto already covered by another entry
157
- * in the list.
158
- *
159
- * Two reasons to drop a target interface t from the list:
160
- * 1. There exists another target interface o with similar bubbling properties
161
- * for the event and t inherits from o. If event fires at o, it can de facto
162
- * fire at t.
163
- * 2. There exists another target interface o such that t and o belong to the
164
- * same bubbling tree, o is at a deeper level than t in the bubbling tree, and
165
- * event bubbles when it fires at o. Event will de facto fire at t through
166
- * bubbling when that happens.
167
- */
168
- function cleanTargetInterfaces(event, parsedInterfaces) {
169
- // Helper function that returns true if the iface interface inherits from the
170
- // base interface
171
- function inheritsFrom(iface, base) {
172
- while (iface) {
173
- if (iface === base) {
174
- return true;
175
- }
176
- iface = parsedInterfaces.find(i => i.name === iface)?.inheritance;
177
- }
178
- return false;
179
- }
180
-
181
- if (!event.targets) {
182
- return;
183
- }
184
-
185
- event.targets = event.targets
186
- .filter(({ target, bubbles }) =>
187
- // Drop if an ancestor in the inheritance chain is already there
188
- !event.targets.find(({ target: other, bubbles: otherBubbles}) =>
189
- target !== other &&
190
- bubbles === otherBubbles &&
191
- inheritsFrom(target, other)))
192
- .filter(({ target, bubbles }) => {
193
- // Drop if a deeper bubbling target interface in the tree is already there
194
- const targetTreeInfo = getInterfaceTreeInfo(target, parsedInterfaces);
195
- return !targetTreeInfo ||
196
- !event.targets.find(({ target: other, bubbles: otherBubbles }) => {
197
- if (other === target) {
198
- return false;
199
- }
200
- const otherTreeInfo = getInterfaceTreeInfo(other, parsedInterfaces);
201
- return otherTreeInfo?.tree === targetTreeInfo.tree &&
202
- otherBubbles && otherTreeInfo.depth > targetTreeInfo.depth;
203
- });
204
- });
205
- }
206
-
207
-
208
- function extendEvent(event, events) {
209
- const extendedEvent =
210
- events.find(e => !e.isExtension && e.href === event.href) ||
211
- events.find(e => !e.isExtension && event.href.startsWith(e.spec.crawled) && e.type === event.type);
212
- if (!extendedEvent) {
213
- // make this a fatal error
214
- return `Found extended event with link ${event.href} in ${event.spec.shortname}, but did not find a matching original event`;
215
- }
216
- if (extendedEvent.interface && event.interface && extendedEvent.interface !== event.interface) {
217
- return `Found extended event with link ${event.href} in ${event.spec.shortname} set to use interface ${event.interface}, different from original event interface ${extendedEvent.interface} in ${extendedEvent.spec.shortname}`;
218
- }
219
- // Document potential additional targets
220
- const newTargets = event.targets?.filter(t => !extendedEvent.targets?.find(tt => tt.target === t.target));
221
- if (newTargets) {
222
- extendedEvent.targets = (extendedEvent.targets || []).concat(newTargets);
223
- }
224
- // Document the fact that the event has been extended
225
- if (!extendedEvent.extendedIn) {
226
- extendedEvent.extendedIn = [];
227
- }
228
- extendedEvent.extendedIn.push(Object.assign(
229
- { spec: event.spec.series.shortname },
230
- event.src?.href ? { href: event.src?.href } : {}));
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
- }
1
+ /**
2
+ * Post-processing module that consolidates events extracts into one extract
3
+ * per event.
4
+ */
5
+
6
+ import { isLatestLevelThatPasses, getInterfaceTreeInfo } from '../lib/util.js';
7
+
8
+ export default {
9
+ dependsOn: ['events'],
10
+ input: 'crawl',
11
+ property: 'events',
12
+
13
+ run: async function(crawl, options) {
14
+ // Collect list of mixin interfaces
15
+ const mixins = {};
16
+ const parsedInterfaces = [];
17
+ crawl.results.forEach(s => {
18
+ if (s.idlparsed) {
19
+ if (s.idlparsed.idlNames) {
20
+ Object.values(s.idlparsed.idlNames).forEach(dfn => {
21
+ if (dfn.type === 'interface' && !dfn.partial) {
22
+ parsedInterfaces.push(dfn);
23
+ }
24
+ });
25
+ }
26
+ if (s.idlparsed.idlExtendedNames) {
27
+ Object.keys(s.idlparsed.idlExtendedNames).forEach(n => {
28
+ s.idlparsed.idlExtendedNames[n].forEach(f => {
29
+ if (f.type === 'includes') {
30
+ if (!mixins[f.includes]) mixins[f.includes] = [];
31
+ mixins[f.includes].push(n);
32
+ }
33
+ });
34
+ });
35
+ }
36
+ }
37
+ });
38
+
39
+ // Only consider latest spec in a series to avoid fake duplicates
40
+ const results = crawl.results.filter(spec =>
41
+ (spec.seriesComposition !== 'delta' && isLatestLevelThatPasses(spec, crawl.results, s => s.events)) ||
42
+ (spec.seriesComposition === 'delta' && spec.events));
43
+
44
+ // Update events in place
45
+ const events = results.map(spec => spec.events.map(e => Object.assign({ spec: spec }, e))).flat();
46
+ for (const event of events) {
47
+ expandMixinTargets(event, mixins);
48
+ setBubblingPerTarget(event, parsedInterfaces);
49
+ }
50
+
51
+ // Consolidate events extended in other specs
52
+ const eventsToDrop = events
53
+ .filter(event => event.isExtension)
54
+ .map(event => {
55
+ const err = extendEvent(event, events);
56
+ if (err) {
57
+ // Event could not be extended, let's keep extension event
58
+ console.warn(err);
59
+ return null;
60
+ }
61
+ else {
62
+ // Event successfully extended, extension can be dropped
63
+ return event;
64
+ }
65
+ })
66
+ .filter(event => !!event);
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.
73
+ return events
74
+ .filter(event => !eventsToDrop.includes(event))
75
+ .filter(event => consolidateBabblingEvent(event, events))
76
+ .map(event => {
77
+ cleanTargetInterfaces(event, parsedInterfaces);
78
+ delete event.spec;
79
+ return event;
80
+ })
81
+ .sort((event1, event2) =>
82
+ event1.type.localeCompare(event2.type, 'en-US') ||
83
+ (!event2.interface ? -1 : 0) ||
84
+ (!event1.interface ? 1 : 0) ||
85
+ event1.interface.localeCompare(event2.interface, 'en-US') ||
86
+ (!event2.href ? -1 : 0) ||
87
+ (!event1.href ? 1 : 0) ||
88
+ event1.href.localeCompare(event2.href, 'en-US'));
89
+ }
90
+ };
91
+
92
+
93
+ function expandMixinTargets(event, mixins) {
94
+ const expandedTargets = event.targets?.map(i => mixins[i] || i)?.flat();
95
+ // This assumes a mixin matches more than one interface
96
+ if (expandedTargets && expandedTargets.length !== event.targets?.length) {
97
+ event.targets = expandedTargets;
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+
103
+
104
+ function setBubblingPerTarget(event, parsedInterfaces) {
105
+ // If an event targets a non root interface in a tree, we can assume that
106
+ // bubbles is false (ideally, we should check the existence of the event
107
+ // handler on the root interface, but there is no easy way to get a
108
+ // consolidated IDL view of the root at the moment)
109
+ if (!event.targets) return;
110
+ const updatedTargets = [];
111
+ const treeInterfaces = [];
112
+ for (let iface of event.targets) {
113
+ const treeInfo = getInterfaceTreeInfo(iface, parsedInterfaces);
114
+ if (!treeInfo) {
115
+ updatedTargets.push({target: iface});
116
+ continue;
117
+ }
118
+ const { tree, depth, bubblingPath } = treeInfo;
119
+ if (depth === 0) {
120
+ // bubbling doesn't matter on the root interface
121
+ updatedTargets.push({target: iface});
122
+ } else {
123
+ treeInterfaces.push({ iface, bubblingPath });
124
+ }
125
+ }
126
+ if (!event.hasOwnProperty('bubbles') && treeInterfaces.length > 0) {
127
+ event.bubbles = false;
128
+ }
129
+ for (let { iface, bubblingPath } of treeInterfaces) {
130
+ updatedTargets.push(Object.assign(
131
+ { target: iface, bubbles: event.bubbles },
132
+ event.bubbles ? { bubblingPath } : {}
133
+ ));
134
+ }
135
+ event.targets = updatedTargets;
136
+ delete event.bubbles;
137
+ }
138
+
139
+
140
+ /**
141
+ * Filter the list of target interfaces to remove those that don't need to
142
+ * appear explicitly because they are de facto already covered by another entry
143
+ * in the list.
144
+ *
145
+ * Two reasons to drop a target interface t from the list:
146
+ * 1. There exists another target interface o with similar bubbling properties
147
+ * for the event and t inherits from o. If event fires at o, it can de facto
148
+ * fire at t.
149
+ * 2. There exists another target interface o such that t and o belong to the
150
+ * same bubbling tree, o is at a deeper level than t in the bubbling tree, and
151
+ * event bubbles when it fires at o. Event will de facto fire at t through
152
+ * bubbling when that happens.
153
+ */
154
+ function cleanTargetInterfaces(event, parsedInterfaces) {
155
+ // Helper function that returns true if the iface interface inherits from the
156
+ // base interface
157
+ function inheritsFrom(iface, base) {
158
+ while (iface) {
159
+ if (iface === base) {
160
+ return true;
161
+ }
162
+ iface = parsedInterfaces.find(i => i.name === iface)?.inheritance;
163
+ }
164
+ return false;
165
+ }
166
+
167
+ if (!event.targets) {
168
+ return;
169
+ }
170
+
171
+ event.targets = event.targets
172
+ .filter(({ target, bubbles }) =>
173
+ // Drop if an ancestor in the inheritance chain is already there
174
+ !event.targets.find(({ target: other, bubbles: otherBubbles}) =>
175
+ target !== other &&
176
+ bubbles === otherBubbles &&
177
+ inheritsFrom(target, other)))
178
+ .filter(({ target, bubbles }) => {
179
+ // Drop if a deeper bubbling target interface in the tree is already there
180
+ const targetTreeInfo = getInterfaceTreeInfo(target, parsedInterfaces);
181
+ return !targetTreeInfo ||
182
+ !event.targets.find(({ target: other, bubbles: otherBubbles }) => {
183
+ if (other === target) {
184
+ return false;
185
+ }
186
+ const otherTreeInfo = getInterfaceTreeInfo(other, parsedInterfaces);
187
+ return otherTreeInfo?.tree === targetTreeInfo.tree &&
188
+ otherBubbles && otherTreeInfo.depth > targetTreeInfo.depth;
189
+ });
190
+ });
191
+ }
192
+
193
+
194
+ function extendEvent(event, events) {
195
+ const extendedEvent =
196
+ events.find(e => !e.isExtension && e.href === event.href) ||
197
+ events.find(e => !e.isExtension && event.href.startsWith(e.spec.crawled) && e.type === event.type);
198
+ if (!extendedEvent) {
199
+ // make this a fatal error
200
+ return `Found extended event with link ${event.href} in ${event.spec.shortname}, but did not find a matching original event`;
201
+ }
202
+ if (extendedEvent.interface && event.interface && extendedEvent.interface !== event.interface) {
203
+ return `Found extended event with link ${event.href} in ${event.spec.shortname} set to use interface ${event.interface}, different from original event interface ${extendedEvent.interface} in ${extendedEvent.spec.shortname}`;
204
+ }
205
+ // Document potential additional targets
206
+ const newTargets = event.targets?.filter(t => !extendedEvent.targets?.find(tt => tt.target === t.target));
207
+ if (newTargets) {
208
+ extendedEvent.targets = (extendedEvent.targets || []).concat(newTargets);
209
+ }
210
+ // Document the fact that the event has been extended
211
+ if (!extendedEvent.extendedIn) {
212
+ extendedEvent.extendedIn = [];
213
+ }
214
+ extendedEvent.extendedIn.push(Object.assign(
215
+ { spec: event.spec.series.shortname },
216
+ event.src?.href ? { href: event.src?.href } : {}));
217
+ }
218
+
219
+
220
+ /**
221
+ * Consolidate events that got duplicated in the extract because they bubble
222
+ * or don't bubble depending on the target interface.
223
+ *
224
+ * We'll say that these events "babble" because they don't seem to know whether
225
+ * they bubble or not.
226
+ */
227
+ function consolidateBabblingEvent(event, events) {
228
+ if (event.mergedIntoAnotherEvent) {
229
+ return null;
230
+ }
231
+ const newTargets = events
232
+ .filter(e =>
233
+ e !== event && !e.isExtension && !e.mergedIntoAnotherEvent &&
234
+ e.href && e.href === event.href && e.cancelable === event.cancelable)
235
+ .map(e => {
236
+ // Flag the event as merged so that we can filter it out afterwards
237
+ e.mergedIntoAnotherEvent = true;
238
+ return e.targets;
239
+ })
240
+ .flat();
241
+ if (newTargets.length > 0) {
242
+ event.targets = (event.targets || []).concat(newTargets);
243
+ }
244
+ return event;
245
+ }