reffy 7.2.10 → 8.0.2

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.
@@ -14,16 +14,15 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
  const specs = require('web-specs');
16
16
  const cssDfnParser = require('./css-grammar-parser');
17
- const { generateIdlParsed, saveIdlParsed } = require('../cli/generate-idlparsed');
18
- const { generateIdlNames, saveIdlNames } = require('../cli/generate-idlnames');
17
+ const postProcessor = require('./post-processor');
19
18
  const {
20
19
  completeWithAlternativeUrls,
21
20
  expandBrowserModules,
22
21
  expandCrawlResult,
23
22
  expandSpecResult,
24
- getGeneratedIDLNamesByCSSProperty,
25
23
  isLatestLevelThatPasses,
26
24
  processSpecification,
25
+ requireFromWorkingDirectory,
27
26
  setupBrowser,
28
27
  teardownBrowser,
29
28
  createFolderIfNeeded
@@ -72,111 +71,52 @@ async function specOrFallback(spec, fallbackFolder, fallbackData) {
72
71
  */
73
72
  async function crawlSpec(spec, crawlOptions) {
74
73
  crawlOptions = crawlOptions || {};
75
- spec.crawled = crawlOptions.publishedVersion ?
74
+
75
+ const urlToCrawl = crawlOptions.publishedVersion ?
76
76
  (spec.release ? spec.release : spec.nightly) :
77
77
  spec.nightly;
78
78
  const fallbackFolder = crawlOptions.fallback ?
79
79
  path.dirname(crawlOptions.fallback) : '';
80
80
 
81
- if (spec.error) {
82
- return specOrFallback(spec, fallbackFolder, crawlOptions.fallbackData?.results);
83
- }
84
-
85
81
  try {
86
82
  const fallback = crawlOptions.fallbackData?.results?.find(s => s.url === spec.url);
87
83
  let cacheInfo = {};
88
84
  if (crawlOptions.fallbackData?.crawler === `reffy-${reffyVersion}`) {
89
85
  cacheInfo = Object.assign({}, fallback?.crawlCacheInfo);
90
86
  }
91
- const result = await processSpecification(
92
- spec.crawled,
93
- (spec, modules) => {
94
- const idToHeading = modules.find(m => m.needsIdToHeadingMap) ?
95
- window.reffy.mapIdsToHeadings() : null;
96
- const res = {
97
- crawled: window.location.toString()
98
- };
99
- modules.forEach(mod => {
100
- res[mod.property] = window.reffy[mod.name](spec, idToHeading);
101
- });
102
- return res;
103
- },
104
- [spec, crawlOptions.modules],
105
- { quiet: crawlOptions.quiet,
106
- forceLocalFetch: crawlOptions.forceLocalFetch,
107
- ...cacheInfo}
108
- );
109
- if (result.status === "notmodified" && fallback) {
110
- crawlOptions.quiet ?? console.warn(`skipping ${spec.url}, no change`);
111
- const copy = Object.assign({}, fallback);
112
- return expandSpecResult(copy, fallbackFolder);
87
+ let result = null;
88
+ if (crawlOptions.useCrawl) {
89
+ result = await expandSpecResult(spec, crawlOptions.useCrawl);
113
90
  }
114
-
115
- // Specific rule for IDL extracts:
116
- // parse the extracted WebIdl content
117
- await generateIdlParsed(result);
118
-
119
- if (result.css) {
120
- // Specific rule for CSS properties:
121
- // Add CSS property definitions that weren't in a table
122
- if (result.dfns) {
123
- result.dfns
124
- .filter(dfn => dfn.type == "property" && !dfn.informative)
125
- .forEach(propDfn => {
126
- propDfn.linkingText.forEach(lt => {
127
- if (!result.css.properties.hasOwnProperty(lt)) {
128
- result.css.properties[lt] = {
129
- name: lt
130
- };
131
- }
132
- });
91
+ else {
92
+ result = await processSpecification(
93
+ urlToCrawl,
94
+ (spec, modules) => {
95
+ const idToHeading = modules.find(m => m.needsIdToHeadingMap) ?
96
+ window.reffy.mapIdsToHeadings() : null;
97
+ const res = {
98
+ crawled: window.location.toString()
99
+ };
100
+ modules.forEach(mod => {
101
+ res[mod.property] = window.reffy[mod.name](spec, idToHeading);
133
102
  });
103
+ return res;
104
+ },
105
+ [spec, crawlOptions.modules],
106
+ { quiet: crawlOptions.quiet,
107
+ forceLocalFetch: crawlOptions.forceLocalFetch,
108
+ ...cacheInfo}
109
+ );
110
+ if (result.status === "notmodified" && fallback) {
111
+ crawlOptions.quiet ?? console.warn(`skipping ${spec.url}, no change`);
112
+ const copy = Object.assign({}, fallback);
113
+ return expandSpecResult(copy, fallbackFolder);
134
114
  }
115
+ }
135
116
 
136
- // Specific rule for CSS properties:
137
- // Ideally, the sample definition (property-name) in CSS2 and the custom
138
- // property definition (--*) in CSS Variables would not be flagged as
139
- // real CSS properties. In practice, they are. Let's remove them from
140
- // the extract.
141
- ['property-name', '--*'].forEach(prop => {
142
- if ((result.css.properties || {})[prop]) {
143
- delete result.css.properties[prop];
144
- }
145
- });
146
-
147
- // Specific rule for CSS extracts:
148
- // Parse extracted CSS definitions and add generated IDL attribute names
149
- Object.entries(result.css.properties || {}).forEach(([prop, dfn]) => {
150
- if (dfn.value || dfn.newValues) {
151
- try {
152
- dfn.parsedValue = cssDfnParser.parsePropDefValue(
153
- dfn.value || dfn.newValues);
154
- } catch (e) {
155
- dfn.valueParseError = e.message;
156
- }
157
- }
158
- dfn.styleDeclaration = getGeneratedIDLNamesByCSSProperty(prop);
159
- });
160
- Object.entries(result.css.descriptors || {}).forEach(([desc, dfn]) => {
161
- if (dfn.value) {
162
- try {
163
- dfn.parsedValue = cssDfnParser.parsePropDefValue(
164
- dfn.value);
165
- } catch (e) {
166
- dfn.valueParseError = e.message;
167
- }
168
- }
169
- });
170
- Object.entries(result.css.valuespaces || {}).forEach(([vs, dfn]) => {
171
- if (dfn.value) {
172
- try {
173
- dfn.parsedValue = cssDfnParser.parsePropDefValue(
174
- dfn.value);
175
- } catch (e) {
176
- dfn.valueParseError = e.message;
177
- }
178
- }
179
- });
117
+ // Run post-processing modules at the spec level
118
+ for (const mod of (crawlOptions.post ?? [])) {
119
+ await postProcessor.run(mod, result, crawlOptions);
180
120
  }
181
121
 
182
122
  // Copy results back into initial spec object
@@ -187,9 +127,12 @@ async function crawlSpec(spec, crawlOptions) {
187
127
  crawlOptions.modules.forEach(mod => {
188
128
  if (result[mod.property]) {
189
129
  spec[mod.property] = result[mod.property];
190
- if (mod.property === 'idl') {
191
- spec.idlparsed = result.idlparsed;
192
- }
130
+ }
131
+ });
132
+ crawlOptions.post?.forEach(mod => {
133
+ const prop = mod.property ?? mod.name;
134
+ if (postProcessor.appliesAtLevel(mod, 'spec') && result[prop]) {
135
+ spec[prop] = result[prop];
193
136
  }
194
137
  });
195
138
  }
@@ -236,12 +179,6 @@ async function saveSpecResults(spec, settings) {
236
179
  continue;
237
180
  }
238
181
  folders[mod.property] = await getSubfolder(mod.property);
239
-
240
- // Specific rule for IDL:
241
- // Raw IDL goes to "idl" subfolder, parsed IDL goes to "idlparsed"
242
- if (mod.property === 'idl') {
243
- folders.idlparsed = await getSubfolder('idlparsed');
244
- }
245
182
  }
246
183
 
247
184
  function getBaseJSON(spec) {
@@ -301,9 +238,6 @@ async function saveSpecResults(spec, settings) {
301
238
  if (spec.idl) {
302
239
  spec.idl = await saveIdl(spec);
303
240
  }
304
- if (spec.idlparsed) {
305
- spec.idlparsed = await saveIdlParsed(spec, settings.output);
306
- }
307
241
 
308
242
  // Save CSS dumps
309
243
  function defineCSSContent(spec) {
@@ -331,14 +265,16 @@ async function saveSpecResults(spec, settings) {
331
265
  (typeof thing == 'object') && (Object.keys(thing).length === 0);
332
266
  }
333
267
 
334
- // Save all other extracts
268
+ // Save all other extracts from crawling modules
335
269
  const remainingModules = modules.filter(mod =>
336
270
  !mod.metadata && mod.property !== 'css' && mod.property !== 'idl');
337
271
  for (const mod of remainingModules) {
338
272
  await saveExtract(spec, mod.property, spec => !isEmpty(spec[mod.property]));
339
- if (spec[mod.property] && typeof spec[mod.property] !== 'string') {
340
- delete spec[mod.property];
341
- }
273
+ }
274
+
275
+ // Save extracts from post-processing modules that run at the spec level
276
+ for (const mod of (settings.post ?? [])) {
277
+ await postProcessor.save(mod, spec, settings);
342
278
  }
343
279
 
344
280
  return spec;
@@ -372,10 +308,17 @@ async function crawlList(speclist, crawlOptions) {
372
308
  }
373
309
  }
374
310
 
375
- // Prepare Puppeteer instance
376
- await setupBrowser(crawlOptions.modules);
311
+ // Prepare Puppeteer instance unless we already have crawl results and
312
+ // we're only interested in post-processing
313
+ let list = null;
314
+ if (crawlOptions.useCrawl) {
315
+ list = speclist;
316
+ }
317
+ else {
318
+ await setupBrowser(crawlOptions.modules);
319
+ list = speclist.map(completeWithAlternativeUrls);
320
+ }
377
321
 
378
- const list = speclist.map(completeWithAlternativeUrls);
379
322
  const listAndPromise = list.map(spec => {
380
323
  let resolve = null;
381
324
  let reject = null;
@@ -418,7 +361,9 @@ async function crawlList(speclist, crawlOptions) {
418
361
  const results = await Promise.all(listAndPromise.map(crawlSpecAndPromise));
419
362
 
420
363
  // Close Puppeteer instance
421
- teardownBrowser();
364
+ if (!crawlOptions.useCrawl) {
365
+ teardownBrowser();
366
+ }
422
367
 
423
368
  return results;
424
369
  }
@@ -564,8 +509,12 @@ function crawlSpecs(options) {
564
509
  });
565
510
  }
566
511
 
567
- const requestedList = options?.specs ?
568
- prepareListOfSpecs(options.specs) :
512
+ const crawlIndex = options?.useCrawl ?
513
+ requireFromWorkingDirectory(options.useCrawl) :
514
+ null;
515
+
516
+ const requestedList = crawlIndex ? crawlIndex.results :
517
+ options?.specs ? prepareListOfSpecs(options.specs) :
569
518
  specs;
570
519
 
571
520
  // Make a shallow copy of passed options parameter and expand modules
@@ -579,9 +528,11 @@ function crawlSpecs(options) {
579
528
  for (const mod of options.modules) {
580
529
  if (mod.extractsPerSeries) {
581
530
  await adjustExtractsPerSeries(results, mod.property, options);
582
- if (mod.property === 'idl') {
583
- await adjustExtractsPerSeries(results, 'idlparsed', options);
584
- }
531
+ }
532
+ }
533
+ for (const mod of options.post ?? []) {
534
+ if (postProcessor.extractsPerSeries(mod)) {
535
+ await adjustExtractsPerSeries(results, mod.property, options);
585
536
  }
586
537
  }
587
538
  return results;
@@ -612,14 +563,21 @@ function crawlSpecs(options) {
612
563
  }
613
564
  })
614
565
  .then(async crawlIndex => {
615
- // Generate IDL names extracts from IDL extracts
616
- // (and dfns extracts to create links to definitions)
617
- if (!options.output || !crawlIndex?.options?.modules?.find(mod => mod === 'idl')) {
618
- return;
566
+ // Run post-processing modules at the crawl level
567
+ for (const mod of (options.post ?? [])) {
568
+ if (!postProcessor.appliesAtLevel(mod, 'crawl')) {
569
+ continue;
570
+ }
571
+ const crawlResults = await expandCrawlResult(
572
+ crawlIndex, options.output, postProcessor.dependsOn(mod));
573
+ const result = await postProcessor.run(mod, crawlResults, options);
574
+ await postProcessor.save(mod, result, options);
575
+
576
+ if (!options.output) {
577
+ console.log();
578
+ console.log(JSON.stringify(result, null, 2));
579
+ }
619
580
  }
620
- const crawlResults = await expandCrawlResult(crawlIndex, options.output, ['idlparsed', 'dfns']);
621
- const idlNames = generateIdlNames(crawlResults.results, options);
622
- await saveIdlNames(idlNames, options.output);
623
581
  });
624
582
  }
625
583
 
@@ -627,5 +585,13 @@ function crawlSpecs(options) {
627
585
  /**************************************************
628
586
  Export methods for use as module
629
587
  **************************************************/
630
- module.exports.crawlList = crawlList;
631
- module.exports.crawlSpecs = crawlSpecs;
588
+ // TODO: consider more alignment between the two crawl functions or
589
+ // find more explicit names to distinguish between them:
590
+ // - "crawlList" takes an explicit list of specs as input, does not run the
591
+ // post-processor, and returns the results without saving them to files.
592
+ // - "crawlSpecs" takes options as input, runs all steps and saves results
593
+ // to files (or outputs the results to the console). It does not return
594
+ // anything.
595
+ module.exports.crawlSpecs = (...args) => Array.isArray(args[0]) ?
596
+ crawlList.apply(this, args) :
597
+ crawlSpecs.apply(this, args);
package/src/lib/util.js CHANGED
@@ -744,10 +744,10 @@ function isLatestLevelThatPasses(spec, list, predicate) {
744
744
  return true;
745
745
  }
746
746
  while (spec.seriesNext) {
747
+ spec = list.find(s => s.shortname === spec.seriesNext);
747
748
  if (!spec) {
748
749
  break;
749
750
  }
750
- spec = list.find(s => s.shortname === spec.seriesNext);
751
751
  if ((spec.seriesComposition === 'full') && predicate(spec)) {
752
752
  return false;
753
753
  }
@@ -816,10 +816,19 @@ async function expandSpecResult(spec, baseFolder, properties) {
816
816
  // Also drop header that may have been added when extract was
817
817
  // serialized.
818
818
  if (contents.startsWith('// GENERATED CONTENT - DO NOT EDIT')) {
819
- const endOfHeader = contents.indexOf('\n\n');
820
- contents = contents.substring(endOfHeader + 2)
821
- // remove trailing newline added in saveIdl
822
- .slice(0, -1);
819
+ const hasWindowsEndings = contents.includes('\r\n\r\n');
820
+ if (hasWindowsEndings) {
821
+ const endOfHeader = contents.indexOf('\r\n\r\n');
822
+ contents = contents.substring(endOfHeader + 4)
823
+ // remove trailing newline added in saveIdl
824
+ .slice(0, -2);
825
+ }
826
+ else {
827
+ const endOfHeader = contents.indexOf('\n\n');
828
+ contents = contents.substring(endOfHeader + 2)
829
+ // remove trailing newline added in saveIdl
830
+ .slice(0, -1);
831
+ }
823
832
  }
824
833
  spec.idl = contents;
825
834
  }
@@ -930,6 +939,66 @@ async function createFolderIfNeeded(folder) {
930
939
  }
931
940
 
932
941
 
942
+ /**
943
+ * Tree hierarchies on which events may bubble
944
+ *
945
+ * First interface is the tree root, further interfaces are deeper levels in
946
+ * the tree.
947
+ */
948
+ const trees = {
949
+ // DOM tree:
950
+ // https://dom.spec.whatwg.org/#node-trees
951
+ 'dom': ['Window', 'Document', 'Element', 'Node'],
952
+
953
+ // IndexedDB tree (defined through "get the parent" algorithms)
954
+ // https://www.w3.org/TR/IndexedDB/#ref-for-get-the-parent%E2%91%A0
955
+ // https://www.w3.org/TR/IndexedDB/#ref-for-get-the-parent%E2%91%A1
956
+ 'IndexedDB': ['IDBDatabase', 'IDBTransaction', 'IDBRequest'],
957
+
958
+ // Web Bluetooth tree
959
+ // https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-tree-bluetooth-tree
960
+ 'web-bluetooth': [
961
+ 'Bluetooth', 'BluetoothDevice', 'BluetoothRemoteGATTService',
962
+ 'BluetoothRemoteGATTCharacteristic', 'BluetoothRemoteGATTDescriptor'],
963
+
964
+ // Serial tree
965
+ // https://wicg.github.io/serial/#serialport-interface
966
+ 'serial': ['Serial', 'SerialPort']
967
+ };
968
+
969
+
970
+ /**
971
+ * Return information about the tree hierarchy the IDL interface is linked to.
972
+ *
973
+ * @function
974
+ * @param {String} iface Name of the IDL interface to link to a tree
975
+ * @param {Array(Object)} interfaces A list of all known IDL interfaces with
976
+ * inheritance information in an "inheritance" property.
977
+ * @return {Object} An object with a "tree" property set to the shortname of the
978
+ * spec that defines the tree hierarchy, an "interface" property set to the
979
+ * interface name of the closest interface in the inheritance chain of the
980
+ * given interface that belongs to the tree, and a "depth" property that gives
981
+ * the depth of that interface in the tree hierarchy (where 0 is the tree
982
+ * root). The object is null if the interface cannot be associated with a
983
+ * tree.
984
+ */
985
+ function getInterfaceTreeInfo(iface, interfaces) {
986
+ while (iface) {
987
+ for (const [tree, nodes] of Object.entries(trees)) {
988
+ if (nodes.includes(iface)) {
989
+ return {
990
+ tree,
991
+ interface: iface,
992
+ depth: nodes.findIndex(i => i === iface)
993
+ };
994
+ }
995
+ }
996
+ iface = interfaces.find(i => i.name === iface)?.inheritance;
997
+ }
998
+ return null;
999
+ }
1000
+
1001
+
933
1002
  module.exports = {
934
1003
  fetch,
935
1004
  requireFromWorkingDirectory,
@@ -942,5 +1011,6 @@ module.exports = {
942
1011
  expandCrawlResult,
943
1012
  expandSpecResult,
944
1013
  getGeneratedIDLNamesByCSSProperty,
945
- createFolderIfNeeded
1014
+ createFolderIfNeeded,
1015
+ getInterfaceTreeInfo
946
1016
  };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Post-processing module that adds CSS property definitions found in prose
3
+ * from the dfns extract, clean up property definitions that should never have
4
+ * been extracted, and adds the generated IDL attribute names in a
5
+ * styleDeclaration sub-property.
6
+ *
7
+ * Module runs at the spec level. It does not create a distinct property but
8
+ * rather completes the `css` property with additional info.
9
+ */
10
+
11
+ const { getGeneratedIDLNamesByCSSProperty } = require('../lib/util');
12
+
13
+ module.exports = {
14
+ dependsOn: ['css', 'dfns'],
15
+ input: 'spec',
16
+
17
+ run: async function(spec, options) {
18
+ if (spec.dfns && spec.css) {
19
+ spec.dfns
20
+ .filter(dfn => dfn.type == "property" && !dfn.informative)
21
+ .forEach(propDfn => {
22
+ propDfn.linkingText.forEach(lt => {
23
+ if (!spec.css.properties.hasOwnProperty(lt)) {
24
+ spec.css.properties[lt] = {
25
+ name: lt
26
+ };
27
+ }
28
+ });
29
+ });
30
+ }
31
+
32
+ if (spec.css) {
33
+ // Add generated IDL attribute names
34
+ Object.entries(spec.css.properties || {}).forEach(([prop, dfn]) => {
35
+ dfn.styleDeclaration = getGeneratedIDLNamesByCSSProperty(prop);
36
+ });
37
+
38
+ // Drop the sample definition (property-name) in CSS2 and the custom
39
+ // property definition (--*) in CSS Variables that specs incorrectly flag
40
+ // as real CSS properties.
41
+ ['property-name', '--*'].forEach(prop => {
42
+ if ((spec.css.properties || {})[prop]) {
43
+ delete spec.css.properties[prop];
44
+ }
45
+ });
46
+ }
47
+
48
+ return spec;
49
+ }
50
+ };
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Post-processing module that consolidates events extracts into one extract
3
+ * per event.
4
+ */
5
+
6
+ const { isLatestLevelThatPasses, getInterfaceTreeInfo } = require('../lib/util');
7
+
8
+ module.exports = {
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
+ cleanTargetInTrees(event, parsedInterfaces);
50
+ }
51
+
52
+ // Consolidate events extended in other specs
53
+ const eventsToDrop = events
54
+ .filter(event => event.isExtension)
55
+ .map(event => {
56
+ const err = extendEvent(event, events);
57
+ if (err) {
58
+ // Event could not be extended, let's keep extension event
59
+ console.warn(err);
60
+ return null;
61
+ }
62
+ else {
63
+ // Event successfully extended, extension can be dropped
64
+ return event;
65
+ }
66
+ })
67
+ .filter(event => !!event);
68
+
69
+ return events
70
+ .filter(event => !eventsToDrop.includes(event))
71
+ .map(event => {
72
+ delete event.spec;
73
+ return event;
74
+ });
75
+ }
76
+ };
77
+
78
+
79
+ function expandMixinTargets(event, mixins) {
80
+ const expandedTargets = event.targets?.map(i => mixins[i] || i)?.flat();
81
+ // This assumes a mixin matches more than one interface
82
+ if (expandedTargets && expandedTargets.length !== event.targets?.length) {
83
+ event.targets = expandedTargets;
84
+ return true;
85
+ }
86
+ return false;
87
+ }
88
+
89
+
90
+ function setBubblingPerTarget(event, parsedInterfaces) {
91
+ // if an event targets an interface in a tree
92
+ // but the root of the tree wasn't detected as a target
93
+ // we can assume bubbles is false
94
+ // (ideally, we should check the existence of the event handler on the
95
+ // root interface, but there is no easy way to get a consolidated IDL view
96
+ // of the root at the moment)
97
+ if (!event.targets) return;
98
+ const updatedTargets = [];
99
+ const detected = {};
100
+ const treeInterfaces = [];
101
+ for (let iface of event.targets) {
102
+ const treeInfo = getInterfaceTreeInfo(iface, parsedInterfaces);
103
+ if (!treeInfo) {
104
+ updatedTargets.push({target: iface});
105
+ continue;
106
+ }
107
+ const { tree, depth } = treeInfo;
108
+ if (!detected[tree]) {
109
+ detected[tree] = {root: false, nonroot: false};
110
+ }
111
+ if (depth === 0) {
112
+ // bubbling doesn't matter on the root interface
113
+ updatedTargets.push({target: iface});
114
+ detected[tree].root = true;
115
+ } else {
116
+ treeInterfaces.push(iface);
117
+ detected[tree].nonroot = true;
118
+ }
119
+ }
120
+ // if the event is sent at targets in a tree, but isn't detected
121
+ // on the root target, and no bubbling info is available,
122
+ // assume it doesn't bubble
123
+ if (Object.values(detected).length) {
124
+ if (!event.hasOwnProperty('bubbles') && Object.values(detected).every(x => !x.root && x.nonroot )) {
125
+ event.bubbles = false;
126
+ }
127
+ }
128
+ for (let iface of treeInterfaces) {
129
+ if (event.hasOwnProperty('bubbles')) {
130
+ updatedTargets.push({target: iface, bubbles: event.bubbles});
131
+ }
132
+ }
133
+ event.targets = updatedTargets;
134
+ delete event.bubbles;
135
+ }
136
+
137
+
138
+ function cleanTargetInTrees(event, parsedInterfaces) {
139
+ // When several targets are attached to an event that bubbles
140
+ // keep only the "deepest" target
141
+ if (event.bubbles && event.targets?.length > 1) {
142
+ const filteredTargets = deepestInterfaceInTree(event.targets, parsedInterfaces);
143
+ if (filteredTargets.length !== event.targets.length) {
144
+ event.targets = filteredTargets;
145
+ return true;
146
+ }
147
+ }
148
+ return false;
149
+ }
150
+
151
+
152
+ function deepestInterfaceInTree(targets, parsedInterfaces) {
153
+ let deepestInTrees = {};
154
+ let filteredTargets = [];
155
+ for (let {target, bubbles} of targets) {
156
+ const treeInfo = getInterfaceTreeInfo(target, parsedInterfaces);
157
+ if (!treeInfo) { // Not in a tree, we keep it in
158
+ filteredTargets.push({target});
159
+ continue;
160
+ }
161
+ const { tree, depth } = treeInfo;
162
+ const currentDeepest = deepestInTrees[tree]?.target;
163
+ if (currentDeepest) {
164
+ const { depth: currentDeepestDepth } = getInterfaceTreeInfo(currentDeepest, parsedInterfaces);
165
+ if (depth > currentDeepestDepth) {
166
+ deepestInTrees[tree] = {target, bubbles};
167
+ }
168
+ } else {
169
+ deepestInTrees[tree] = {target, bubbles};
170
+ }
171
+ }
172
+ return filteredTargets.concat(Object.values(deepestInTrees));
173
+ }
174
+
175
+
176
+ function extendEvent(event, events) {
177
+ const extendedEvent =
178
+ events.find(e => !e.isExtension && e.href === event.href) ||
179
+ events.find(e => !e.isExtension && event.href.startsWith(e.spec.crawled) && e.type === event.type);
180
+ if (!extendedEvent) {
181
+ // make this a fatal error
182
+ return `Found extended event with link ${event.href} in ${event.spec.shortname}, but did not find a matching original event`;
183
+ }
184
+ if (extendedEvent.interface && event.interface && extendedEvent.interface !== event.interface) {
185
+ 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}`;
186
+ }
187
+ // Document potential additional targets
188
+ const newTargets = event.targets?.filter(t => !extendedEvent.targets?.find(tt => tt.target === t.target));
189
+ if (newTargets) {
190
+ extendedEvent.targets = (extendedEvent.targets || []).concat(newTargets);
191
+ }
192
+ // Document the fact that the event has been extended
193
+ if (!extendedEvent.extendedIn) {
194
+ extendedEvent.extendedIn = [];
195
+ }
196
+ extendedEvent.extendedIn.push({ spec: event.spec.series.shortname, href: event.src?.href });
197
+ }