diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1f42b82..7f217c1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,14 +54,20 @@ properties: - `url`: same as the [`url`](README.md#url) property in `index.json`. - `shortname`: same as the [`shortname`](README.md#shortname) property in -`index.json`. +`index.json`. When the `forkOf` property is also set, note that the actual +shortname in the final list will be prefixed by the shortname of the base +spec (as in: `${forkOf}-fork-${shortname}`). +- `forkOf`: same as the [`forkOf`](README.md#forkof) property in `index.json`. +No need to set `seriesComposition` to `"fork"` when this property is set, the +build logic will take care of that automatically. - `series`: same as the [`series`](README.md#series) property in `index.json`, but note the `currentSpecification` property will be ignored. - `seriesVersion`: same as the [`seriesVersion`](README.md#seriesversion) property in `index.json`. - `seriesComposition`: same as the [`seriesComposition`](README.md#seriesComposition) -property in `index.json`. The property must only be set for delta spec (since -full is the default). +property in `index.json`. The property must only be set for delta spec, since +full is the default and fork specs are identified through the `forkOf` property +in `specs.json`. - `organization`: same as the [`organization`](README.md#organization) property in `index.json` to specify the name of the organization that owns the spec. - `groups`: same as the [`groups`](README.md#groups) property in `index.json` diff --git a/README.md b/README.md index ce6d5026..6eea8588 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ cross-references, WebIDL, quality, etc. - [`seriesComposition`](#seriescomposition) - [`seriesPrevious`](#seriesprevious) - [`seriesNext`](#seriesnext) + - [`forkOf`](#forkof) + - [`forks`](#forks) - [`organization`](#organization) - [`groups`](#groups) - [`release`](#release) @@ -150,6 +152,13 @@ For WHATWG specs, this is the shortname that appears at the beginning of the URL (e.g. `compat` for `https://compat.spec.whatwg.org/`). For specs developed on GitHub, this is usually the name of repository that holds the spec. +When the spec is a fork (see [`forkOf`](#forkof)) of a base spec, its shortname +will start with the shortname of the base spec completed by `-fork-` and the +actual shortname of the fork spec. For instance, given an exception handling +fork of the WebAssembly spec for which the raw shortname would be +`exception-handling`, the actual spec shortname will be +`wasm-js-api-1-fork-exception-handling`. + The `shortname` property is always set. @@ -285,8 +294,9 @@ number. ### `seriesComposition` -Whether the spec is a standalone spec, or whether it is a delta spec over the -previous level or version in the series. Possible values are `full` or `delta`. +Whether the spec is a standalone spec, whether it is a delta spec over the +previous level or version in the series, or whether it is a temporary fork of +another spec. Possible values are `full`, `delta`, or `fork`. The `seriesComposition` property is always set. @@ -306,6 +316,28 @@ The `shortname` of the next spec in the series. The `seriesNext` property is only set where there is a next level or version. +### `forkOf` + +The shortname of the spec that this spec is a fork of. + +The `forkOf` property is only set when the spec is a fork of another one. The +[`seriesComposition`](#seriescomposition) property is always `"fork"` when the +`forkOf` property is set. + +A forked specs is supposed to be temporary by nature. It will be removed from +the list as soon as it gets merged into the main spec, or as soon as it gets +abandoned. + + +### `forks` + +An array that lists shortnames of known forks of the spec in the list. + +The `forks` property is only set when there exists at least one fork of the +spec in the list, meaning when there is an entry in the list that has a +[`forkOf`](#forkof) property set to the spec's shortname. + + ### `organization` The name of the standardization organization that owns the spec such as `W3C`, diff --git a/packages/browser-specs/package.json b/packages/browser-specs/package.json index 71801dc9..1e871c70 100644 --- a/packages/browser-specs/package.json +++ b/packages/browser-specs/package.json @@ -1,6 +1,6 @@ { "name": "browser-specs", - "version": "2.30.1", + "version": "3.0.0", "description": "Curated list of technical Web specifications that are directly implemented or that will be implemented by Web browsers.", "repository": { "type": "git", @@ -14,4 +14,4 @@ "index.json" ], "main": "index.json" -} \ No newline at end of file +} diff --git a/packages/web-specs/package.json b/packages/web-specs/package.json index 44334499..9cb8293e 100644 --- a/packages/web-specs/package.json +++ b/packages/web-specs/package.json @@ -1,6 +1,6 @@ { "name": "web-specs", - "version": "1.3.1", + "version": "2.0.0", "description": "Curated list of technical Web specifications", "repository": { "type": "git", @@ -14,4 +14,4 @@ "index.json" ], "main": "index.json" -} \ No newline at end of file +} diff --git a/schema/definitions.json b/schema/definitions.json index a17cce7d..0db66a50 100644 --- a/schema/definitions.json +++ b/schema/definitions.json @@ -47,7 +47,7 @@ "seriesComposition": { "type": "string", - "enum": ["full", "delta"] + "enum": ["full", "delta", "fork"] }, "forceCurrent": { @@ -151,6 +151,11 @@ "minItems": 1 } ] + }, + + "forks": { + "type": "array", + "items": { "$ref": "#/$defs/shortname" } } } } \ No newline at end of file diff --git a/schema/index.json b/schema/index.json index e7d42c3e..1c9d598a 100644 --- a/schema/index.json +++ b/schema/index.json @@ -8,6 +8,8 @@ "properties": { "url": { "$ref": "definitions.json#/$defs/url" }, "shortname": { "$ref": "definitions.json#/$defs/shortname" }, + "forkOf": { "$ref": "definitions.json#/$defs/shortname" }, + "forks": { "$ref": "definitions.json#/$defs/forks" }, "series": { "$ref": "definitions.json#/$defs/series" }, "seriesVersion": { "$ref": "definitions.json#/$defs/seriesVersion" }, "seriesComposition": { "$ref": "definitions.json#/$defs/seriesComposition" }, diff --git a/schema/specs.json b/schema/specs.json index 5fef7a5b..136da35d 100644 --- a/schema/specs.json +++ b/schema/specs.json @@ -14,6 +14,7 @@ "properties": { "url": { "$ref": "definitions.json#/$defs/url" }, "shortname": { "$ref": "definitions.json#/$defs/shortname" }, + "forkOf": { "$ref": "definitions.json#/$defs/shortname" }, "series": { "$ref": "definitions.json#/$defs/series" }, "seriesVersion": { "$ref": "definitions.json#/$defs/seriesVersion" }, "seriesComposition": { "$ref": "definitions.json#/$defs/seriesComposition" }, diff --git a/specs.json b/specs.json index e3131350..a87104ce 100644 --- a/specs.json +++ b/specs.json @@ -181,6 +181,10 @@ "https://w3c.github.io/webappsec-trusted-types/dist/spec/", "https://w3c.github.io/webdriver-bidi/", "https://w3c.github.io/webrtc-ice/", + { + "url": "https://webassembly.github.io/exception-handling/js-api/", + "forkOf": "wasm-js-api-1" + }, "https://webbluetoothcg.github.io/web-bluetooth/", "https://webidl.spec.whatwg.org/", "https://websockets.spec.whatwg.org/", diff --git a/src/build-index.js b/src/build-index.js index 2fb209bc..a9156670 100644 --- a/src/build-index.js +++ b/src/build-index.js @@ -93,9 +93,11 @@ async function generateIndex(specs, { previousIndex = null, log = console.log } delete spec.series; // Complete information + const seriesComposition = spec.seriesComposition ?? + (spec.forkOf ? "fork" : "full"); const res = Object.assign( - { url: spec.url, seriesComposition: spec.seriesComposition || "full" }, - computeShortname(spec.shortname || spec.url), + { url: spec.url, seriesComposition }, + computeShortname(spec.shortname ?? spec.url, spec.forkOf), spec); // Restore series info explicitly set in initial spec object @@ -114,7 +116,20 @@ async function generateIndex(specs, { previousIndex = null, log = console.log } .map(spec => { delete spec.forceCurrent; return spec; }) // Complete information with previous/next level links - .map((spec, _, list) => Object.assign(spec, computePrevNext(spec, list))); + .map((spec, _, list) => Object.assign(spec, computePrevNext(spec, list))) + + // Complete information with forks + .map((spec, _, list) => { + const forks = list.filter(s => + s.series.shortname === spec.series.shortname && + s.seriesComposition === "fork" && + s.forkOf === spec.shortname) + .map(s => s.shortname); + if (forks.length > 0) { + spec.forks = forks; + } + return spec; + }); log(`Prepare initial list of specs... found ${specs.length} specs`); // Fetch additional spec info from external sources and complete the list diff --git a/src/compute-currentlevel.js b/src/compute-currentlevel.js index 07d78ecb..80d12623 100644 --- a/src/compute-currentlevel.js +++ b/src/compute-currentlevel.js @@ -9,7 +9,7 @@ * (if needed) properties. * * By default, the current level is defined as the last level that is not a - * delta spec, unless a level is explicitly flagged with a "forceCurrent" + * delta/fork spec, unless a level is explicitly flagged with a "forceCurrent" * property in the list of specs. */ @@ -27,9 +27,12 @@ module.exports = function (spec, list) { const current = list.reduce((candidate, curr) => { if (curr.series.shortname === candidate.series.shortname && - !candidate.forceCurrent && - curr.seriesComposition !== "delta" && - (curr.forceCurrent || candidate.seriesComposition === "delta" || + !candidate.forceCurrent && + curr.seriesComposition !== "fork" && + curr.seriesComposition !== "delta" && + (curr.forceCurrent || + candidate.seriesComposition === "delta" || + candidate.seriesComposition === "fork" || (curr.seriesVersion || "0") > (candidate.seriesVersion || "0"))) { return curr; } diff --git a/src/compute-prevnext.js b/src/compute-prevnext.js index 1d7fa88a..aa8172ab 100644 --- a/src/compute-prevnext.js +++ b/src/compute-prevnext.js @@ -22,7 +22,7 @@ module.exports = function (spec, list) { const level = spec.seriesVersion || "0"; return list - .filter(s => s.series.shortname === spec.series.shortname) + .filter(s => s.series.shortname === spec.series.shortname && s.seriesComposition !== "fork") .sort((a, b) => (a.seriesVersion || "0").localeCompare(b.seriesVersion || "0")) .reduce((res, s) => { if ((s.seriesVersion || "0") < level) { diff --git a/src/compute-shortname.js b/src/compute-shortname.js index f236a2b9..1ad9c320 100644 --- a/src/compute-shortname.js +++ b/src/compute-shortname.js @@ -128,7 +128,7 @@ function computeShortname(url) { /** * Compute the shortname and level from the spec name, if possible. */ -function completeWithSeriesAndLevel(shortname, url) { +function completeWithSeriesAndLevel(shortname, url, forkOf) { // Use latest convention for CSS specs function modernizeShortname(name) { if (name.startsWith("css3-")) { @@ -142,22 +142,25 @@ function completeWithSeriesAndLevel(shortname, url) { } } + const seriesBasename = forkOf ?? shortname; + const specShortname = forkOf ? `${forkOf}-fork-${shortname}` : shortname; + // Shortnames of WebGL extensions sometimes end up with digits which are *not* // to be interpreted as level numbers. Similarly, shortnames of ECMA specs // typically have the form "ecma-ddd", and "ddd" is *not* a level number. - if (shortname.match(/^ecma-/) || url.match(/^https:\/\/www\.khronos\.org\/registry\/webgl\/extensions\//)) { + if (seriesBasename.match(/^ecma-/) || url.match(/^https:\/\/www\.khronos\.org\/registry\/webgl\/extensions\//)) { return { - shortname, - series: { shortname } + shortname: specShortname, + series: { shortname: seriesBasename } }; } // Extract X and X.Y levels, with form "name-X" or "name-X.Y". // (e.g. 5 for "mediaqueries-5", 1.2 for "wai-aria-1.2") - let match = shortname.match(/^(.*?)-(\d+)(.\d+)?$/); + let match = seriesBasename.match(/^(.*?)-(\d+)(.\d+)?$/); if (match) { return { - shortname, + shortname: specShortname, series: { shortname: modernizeShortname(match[1]) }, seriesVersion: match[3] ? match[2] + match[3] : match[2] }; @@ -165,10 +168,10 @@ function completeWithSeriesAndLevel(shortname, url) { // Extract X and X.Y levels with form "nameX" or "nameXY" (but not "nameXXY") // (e.g. 2.1 for "CSS21", 1.1 for "SVG11", 4 for "selectors4") - match = shortname.match(/^(.*?)(? p.shortname === linked.seriesNext) : null; - const isLast = !next || next.seriesComposition === "delta"; + const isLast = !next || next.seriesComposition === "delta" || + next.seriesComposition === "fork"; if (spec.forceCurrent && isLast) { spec.forceCurrent = false; } diff --git a/test/compute-currentlevel.js b/test/compute-currentlevel.js index c21cbfe2..8e60558a 100644 --- a/test/compute-currentlevel.js +++ b/test/compute-currentlevel.js @@ -8,7 +8,7 @@ describe("compute-currentlevel module", () => { function getSpec(options) { options = options || {}; const res = { - shortname: (options.seriesVersion ? `spec-${options.seriesVersion}` : "spec"), + shortname: options.shortname ?? (options.seriesVersion ? `spec-${options.seriesVersion}` : "spec"), series: { shortname: "spec" }, }; for (const property of Object.keys(options)) { @@ -49,6 +49,14 @@ describe("compute-currentlevel module", () => { spec.shortname); }); + it("returns the name of the latest level that is not a fork spec", () => { + const spec = getSpec({ seriesVersion: "1" }); + const fork = getSpec({ seriesVersion: "2", seriesComposition: "fork" }); + assert.equal( + getCurrentName(spec, [spec, fork]), + spec.shortname); + }); + it("gets back to the latest level when spec is a delta spec", () => { const spec = getSpec({ seriesVersion: "1" }); const delta = getSpec({ seriesVersion: "2", seriesComposition: "delta" }); @@ -57,6 +65,14 @@ describe("compute-currentlevel module", () => { spec.shortname); }); + it("gets back to the latest level when spec is a fork spec", () => { + const spec = getSpec({ seriesVersion: "1" }); + const fork = getSpec({ seriesVersion: "2", seriesComposition: "fork" }); + assert.equal( + getCurrentName(fork, [spec, fork]), + spec.shortname); + }); + it("returns the spec name if it is flagged as current", () => { const spec = getSpec({ seriesVersion: "1", forceCurrent: true }); const last = getSpec({ seriesVersion: "2" }); @@ -81,4 +97,12 @@ describe("compute-currentlevel module", () => { getCurrentName(spec, [spec, other]), spec.shortname); }); + + it("does not take forks into account", () => { + const spec = getSpec({ shortname: "spec-1-fork-1", seriesVersion: "1", seriesComposition: "fork" }); + const base = getSpec({ seriesVersion: "1" }); + assert.equal( + getCurrentName(spec, [spec, base]), + base.shortname); + }); }); \ No newline at end of file diff --git a/test/compute-shortname.js b/test/compute-shortname.js index 15805f54..96c65984 100644 --- a/test/compute-shortname.js +++ b/test/compute-shortname.js @@ -105,6 +105,11 @@ describe("compute-shortname module", () => { () => computeInfo("https://w3c.github.io/spec4.2/"), /^Specification name contains unexpected characters/); }); + + it("handles forks", () => { + const url = "https://www.w3.org/TR/extension/"; + assert.equal(computeInfo(url, "source-2").shortname, "source-2-fork-extension"); + }); }); @@ -152,6 +157,11 @@ describe("compute-shortname module", () => { it("preserves digits at the end of WebGL extension names", () => { assertSeries("https://www.khronos.org/registry/webgl/extensions/EXT_wow32/", "EXT_wow32"); }); + + it("handles forks", () => { + const url = "https://www.w3.org/TR/the-ext/"; + assert.equal(computeInfo(url, "source-2").series.shortname, "source"); + }); }); @@ -203,5 +213,10 @@ describe("compute-shortname module", () => { it("does not confuse digits at the end of a WebGL extension spec with a series version", () => { assertNoSeriesVersion("https://www.khronos.org/registry/webgl/extensions/EXT_wow32/"); }); + + it("handles forks", () => { + const url = "https://www.w3.org/TR/the-ext/"; + assert.equal(computeInfo(url, "source-2").seriesVersion, "2"); + }); }); }); \ No newline at end of file diff --git a/test/index.js b/test/index.js index 977a08ef..abfd43d0 100644 --- a/test/index.js +++ b/test/index.js @@ -90,6 +90,18 @@ describe("List of specs", () => { assert.deepStrictEqual(wrong, []); }); + it("does not have previous links for fork specs", () => { + const wrong = specs.filter(s => + s.seriesComposition === "fork" && s.seriesPrevious); + assert.deepStrictEqual(wrong, []); + }); + + it("does not have next links for fork specs", () => { + const wrong = specs.filter(s => + s.seriesComposition === "fork" && s.seriesNext); + assert.deepStrictEqual(wrong, []); + }); + it("has consistent series info", () => { const wrong = specs.filter(s => { if (!s.seriesPrevious) { @@ -150,4 +162,28 @@ describe("List of specs", () => { const wrong = specs.filter(s => s.release && !s.release.filename); assert.deepStrictEqual(wrong, []); }); + + it("has a forkOf property for all fork specs", () => { + const wrong = specs.filter(s => s.seriesComposition === "fork" && !s.forkOf); + assert.deepStrictEqual(wrong, []); + }); + + it("has a fork composition level for all fork specs", () => { + const wrong = specs.filter(s => s.forkOf && s.seriesComposition !== "fork"); + assert.deepStrictEqual(wrong, []); + }); + + it("only has forks of existing specs", () => { + const wrong = specs.filter(s => s.forkOf && !specs.find(spec => spec.shortname === s.forkOf)); + assert.deepStrictEqual(wrong, []); + }); + + it("has consistent forks properties", () => { + const wrong = specs.filter(s => !!s.forks && + s.forks.find(shortname => !specs.find(spec => + spec.shortname === shortname && + spec.seriesComposition === "fork" && + spec.forkOf === s.shortname))); + assert.deepStrictEqual(wrong, []); + }); }); diff --git a/test/lint.js b/test/lint.js index ef7ee804..4500ca52 100644 --- a/test/lint.js +++ b/test/lint.js @@ -171,5 +171,16 @@ describe("Linter", () => { lintStr(toStr(specs)), toStr(["https://www.w3.org/TR/duplicate/"])); }); + + it("lints an object with a forkOf and a seriesComposition property", () => { + const specs = [ + "https://www.w3.org/TR/spec-1/", + { "url": "https://www.w3.org/TR/spec-2/", seriesComposition: "fork", forkOf: "spec-1" } + ]; + assert.equal(lintStr(toStr(specs)), toStr([ + "https://www.w3.org/TR/spec-1/", + { "url": "https://www.w3.org/TR/spec-2/", forkOf: "spec-1" } + ])); + }); }); }); diff --git a/test/specs.js b/test/specs.js index 0080d9ab..30245fea 100644 --- a/test/specs.js +++ b/test/specs.js @@ -92,7 +92,7 @@ function specs2objects(specs) { function specs2LinkedList(specs) { return specs2objects(specs) - .map(s => Object.assign({}, s, computeInfo(s.shortname || s.url))) + .map(s => Object.assign({}, s, computeInfo(s.shortname || s.url, s.forkOf))) .map((s, _, list) => Object.assign({}, s, computePrevNext(s, list))); } @@ -159,7 +159,7 @@ describe("Input list", () => { it("only contains specs for which a shortname can be generated", () => { // Convert entries to spec objects and compute shortname const specsWithoutShortname = specs2objects(specs) - .map(spec => Object.assign({}, spec, computeInfo(spec.shortname || spec.url))) + .map(spec => Object.assign({}, spec, computeInfo(spec.shortname || spec.url, spec.forkOf))) .filter(spec => !spec.shortname); // No exception thrown? That means we're good! @@ -172,7 +172,7 @@ describe("Input list", () => { it("does not have a delta spec without a previous full spec", () => { const fullPrevious = (spec, list) => { const previous = list.find(s => s.shortname === spec.seriesPrevious); - if (previous && previous.seriesComposition === "delta") { + if (previous && previous.seriesComposition && previous.seriesComposition !== "full") { return fullPrevious(previous, list); } return previous; @@ -188,6 +188,12 @@ describe("Input list", () => { assert.strictEqual(deltaCurrent[0], undefined); }); + it("does not have a fork spec flagged as 'current'", () => { + const forkCurrent = specs2LinkedList(specs) + .filter(s => s.forceCurrent && s.forkOf); + assert.strictEqual(forkCurrent[0], undefined); + }); + it("has only one spec flagged as 'current' per series shortname", () => { const linkedList = specs2LinkedList(specs); const problematicCurrent = linkedList @@ -196,5 +202,22 @@ describe("Input list", () => { p.series.shortname === s.series.shortname && p.forceCurrent)); assert.strictEqual(problematicCurrent[0], undefined); }); + + it("does not have a spec with a 'fork' seriesComposition property", () => { + const wrong = specs.find(s => s.seriesComposition === "fork"); + assert.strictEqual(wrong, undefined); + }); + + it("does not have a 'delta fork' spec", () => { + const wrong = specs.find(s => s.forkOf && s.seriesComposition === "delta"); + assert.strictEqual(wrong, undefined); + }); + + it("only has fork specs that reference existing specs", () => { + const linkedList = specs2LinkedList(specs); + const forkWithoutFull = linkedList.filter((s, _, list) => s.forkOf && + !linkedList.find(spec => spec.shortname === s.forkOf)); + assert.strictEqual(forkWithoutFull[0], undefined); + }); }); });