reffy 7.2.8 → 8.0.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.
@@ -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
@@ -605,7 +605,7 @@ async function processSpecification(spec, processFunction, args, options) {
605
605
  window.document.head.querySelector("script[src*='respec']");
606
606
 
607
607
  function sleep(ms) {
608
- return new Promise(resolve => setTimeout(resolve, ms));
608
+ return new Promise(resolve => setTimeout(resolve, ms, 'slept'));
609
609
  }
610
610
 
611
611
  async function isReady(counter) {
@@ -614,7 +614,10 @@ async function processSpecification(spec, processFunction, args, options) {
614
614
  throw new Error('Respec generation took too long');
615
615
  }
616
616
  if (window.document.respec?.ready) {
617
- await window.document.respec.ready;
617
+ const res = await Promise.race([window.document.respec.ready, sleep(60000)]);
618
+ if (res === 'slept') {
619
+ throw new Error('Respec generation took too long');
620
+ }
618
621
  }
619
622
  else if (usesRespec) {
620
623
  await sleep(1000);
@@ -741,10 +744,10 @@ function isLatestLevelThatPasses(spec, list, predicate) {
741
744
  return true;
742
745
  }
743
746
  while (spec.seriesNext) {
747
+ spec = list.find(s => s.shortname === spec.seriesNext);
744
748
  if (!spec) {
745
749
  break;
746
750
  }
747
- spec = list.find(s => s.shortname === spec.seriesNext);
748
751
  if ((spec.seriesComposition === 'full') && predicate(spec)) {
749
752
  return false;
750
753
  }
@@ -813,10 +816,19 @@ async function expandSpecResult(spec, baseFolder, properties) {
813
816
  // Also drop header that may have been added when extract was
814
817
  // serialized.
815
818
  if (contents.startsWith('// GENERATED CONTENT - DO NOT EDIT')) {
816
- const endOfHeader = contents.indexOf('\n\n');
817
- contents = contents.substring(endOfHeader + 2)
818
- // remove trailing newline added in saveIdl
819
- .slice(0, -1);
819
+ const hasWindowsEndings = contents.indexOf('\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
+ }
820
832
  }
821
833
  spec.idl = contents;
822
834
  }
@@ -927,6 +939,66 @@ async function createFolderIfNeeded(folder) {
927
939
  }
928
940
 
929
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
+
930
1002
  module.exports = {
931
1003
  fetch,
932
1004
  requireFromWorkingDirectory,
@@ -939,5 +1011,6 @@ module.exports = {
939
1011
  expandCrawlResult,
940
1012
  expandSpecResult,
941
1013
  getGeneratedIDLNamesByCSSProperty,
942
- createFolderIfNeeded
1014
+ createFolderIfNeeded,
1015
+ getInterfaceTreeInfo
943
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
+ };