From 42bd316a057a13404fe4d81ad3220450f61eecf9 Mon Sep 17 00:00:00 2001 From: Francois Daoust Date: Fri, 29 Jul 2022 16:34:52 +0200 Subject: [PATCH] Extract CSS at-rules, move descriptors under at-rules This creates an `atrules` property in CSS extracts that contains at-rules syntax as suggested in #1028. Descriptors associated with an at-rule (and all selectors are associated with an at-rule in practice) now appear under the at-rule definition. The new `atrules` property contains a `value` property when a formal syntax could be extracted from the spec, and a `descriptors` array that lists descriptors associated with the at-rule (if any). Breaking change: The former `descriptors` property that appeared at the root level of the extract no longer exists. The same info can be found under the `atrules` property. This update also adjusts a couple of tests and adds two new tests on at-rules extraction. The code can only extract the formal syntax of an at-rule if it is defined in a `
` block. That is the case in most CSS specs, but some of
the definitions are done in `
` tags without any class for now (e.g.
`@counter-style` and `@scroll-timeline`). The specs need fixing or the
extraction logic needs to also parse mere `
` tags (but that seems fragile).

As opposed to the structure proposed in #1028, the list of descriptors is not
indexed by the descriptor's name.

For instance, the `@container` at-rule would lead to:

```json
{
  "@container": {
    "value": "@container [  ]?  {  }",
    "descriptors": [
      {
        "name": "width",
        "for": "@container",
        "value": "",
        "type": "range"
      },
      {
        "name": "height",
        "for": "@container",
        "value": "",
        "type": "range"
      },
      {
        "name": "inline-size",
        "for": "@container",
        "value": "",
        "type": "range"
      },
      {
        "name": "block-size",
        "for": "@container",
        "value": "",
        "type": "range"
      },
      {
        "name": "aspect-ratio",
        "for": "@container",
        "value": "",
        "type": "range"
      },
      {
        "name": "orientation",
        "for": "@container",
        "value": "portrait | landscape",
        "type": "discrete"
      }
    ]
  }
}
```
---
 src/browserlib/extract-cssdfn.mjs | 60 +++++++++++++++++---
 tests/crawl-test.json             |  6 +-
 tests/extract-css.js              | 94 ++++++++++++++++++++++++++-----
 3 files changed, 134 insertions(+), 26 deletions(-)

diff --git a/src/browserlib/extract-cssdfn.mjs b/src/browserlib/extract-cssdfn.mjs
index c5e9648a..21da8b04 100644
--- a/src/browserlib/extract-cssdfn.mjs
+++ b/src/browserlib/extract-cssdfn.mjs
@@ -13,15 +13,37 @@ import informativeSelector from './informative-selector.mjs';
 export default function () {
   let res = {
     properties: extractTableDfns(document, 'propdef', { unique: true }),
-    descriptors: extractTableDfns(document, 'descdef', { unique: false }),
+    atrules: {},
     valuespaces: extractValueSpaces(document)
   };
+  let descriptors = extractTableDfns(document, 'descdef', { unique: false });
 
   // Try old recipes if we couldn't extract anything
   if ((Object.keys(res.properties).length === 0) &&
-      (Object.keys(res.descriptors).length === 0)) {
+      (Object.keys(descriptors).length === 0)) {
     res.properties = extractDlDfns(document, 'propdef', { unique: true });
-    res.descriptors = extractDlDfns(document, 'descdef', { unique: false });
+    descriptors = extractDlDfns(document, 'descdef', { unique: false });
+  }
+
+  // Move at-rules definitions from valuespaces to at-rules structure
+  for (const [name, dfn] of Object.entries(res.valuespaces)) {
+    if (name.startsWith('@')) {
+      if (!res.atrules[name]) {
+        res.atrules[name] = Object.assign(dfn, { descriptors: [] });
+      }
+      delete res.valuespaces[name];
+    }
+  }
+
+  // Move descriptors to at-rules structure
+  for (const [name, desclist] of Object.entries(descriptors)) {
+    for (const desc of desclist) {
+      const rule = desc.for;
+      if (!res.atrules[rule]) {
+        res.atrules[rule] = { descriptors: [] };
+      }
+      res.atrules[rule].descriptors.push(desc);
+    }
   }
 
   return res;
@@ -198,6 +220,9 @@ const extractDlDfns = (doc, className, options) =>
  * Definitions with `data-dfn-type` attribute set to `value` are not extracted
  * on purpose as they are typically namespaced to another construct (through a
  * `data-dfn-for` attribute.
+ *
+ * The function also extracts syntax of at-rules (name starts with '@') defined
+ * in "pre.prod" blocks.
  */
 const extractValueSpaces = doc => {
   let res = {};
@@ -213,16 +238,27 @@ const extractValueSpaces = doc => {
       .replace(/\/\*[^]*?\*\//gm, '')  // Drop comments
       .split(/\s?=\s/)
       .map(s => s.trim().replace(/\s+/g, ' '));
-    if (nameAndValue[0].match(/^<.*>$|^.*\(\)$/)) {
-      const name = nameAndValue[0].replace(/^(.*\(\))$/, '<$1>');
+
+    function addValuespace(name, value) {
       if (!(name in res)) {
         res[name] = {};
       }
       if (!res[name].value || (pureSyntax && !res[name].pureSyntax)) {
-        res[name].value = normalize(nameAndValue[1]);
+        res[name].value = normalize(value);
         res[name].pureSyntax = pureSyntax;
       }
     }
+
+    if (nameAndValue[0].match(/^<.*>$|^.*\(\)$/)) {
+      // Regular valuespace
+      addValuespace(
+        nameAndValue[0].replace(/^(.*\(\))$/, '<$1>'),
+        nameAndValue[1]);
+    }
+    else if (nameAndValue[0].match(/^@[a-z\-]+$/)) {
+      // At-rule syntax
+      addValuespace(nameAndValue[0], nameAndValue[1]);
+    }
   };
 
   // Regular expression to use to split production rules:
@@ -351,8 +387,16 @@ const extractValueSpaces = doc => {
     .map(val => val.replace(/\/\*[^]*?\*\//gm, ''))  // Drop comments
     .map(val => val.split(reSplitRules))             // Separate definitions
     .flat()
-    .filter(text => text.match(/\s?=\s/))
-    .map(text => parseProductionRule(text, { pureSyntax: true }));
+    .map(text => text.trim())
+    .map(text => {
+      if (text.match(/\s?=\s/)) {
+        return parseProductionRule(text, { pureSyntax: true });
+      }
+      else if (text.startsWith('@')) {
+        const name = text.split(' ')[0];
+        return parseProductionRule(`${name} = ${text}`, { pureSyntax: true });
+      }
+    });
 
   // Don't keep the info on whether value comes from a pure syntax section
   Object.values(res).map(value => delete value.pureSyntax);
diff --git a/tests/crawl-test.json b/tests/crawl-test.json
index c9869494..0846b21a 100644
--- a/tests/crawl-test.json
+++ b/tests/crawl-test.json
@@ -19,8 +19,8 @@
     },
     "title": "WOFF2",
     "css": {
+      "atrules": {},
       "properties": {},
-      "descriptors": {},
       "valuespaces": {}
     },
     "dfns": [
@@ -81,8 +81,8 @@
     "title": "No Title",
     "generator": "respec",
     "css": {
+      "atrules": {},
       "properties": {},
-      "descriptors": {},
       "valuespaces": {}
     },
     "dfns": [
@@ -201,8 +201,8 @@
     },
     "title": "[No title found for https://w3c.github.io/accelerometer/]",
     "css": {
+      "atrules": {},
       "properties": {},
-      "descriptors": {},
       "valuespaces": {}
     },
     "dfns": [],
diff --git a/tests/extract-css.js b/tests/extract-css.js
index 7c0e0bc0..7d019aff 100644
--- a/tests/extract-css.js
+++ b/tests/extract-css.js
@@ -267,6 +267,64 @@ const tests = [
     css: {}
   },
 
+  {
+    title: "extracts an at-rule syntax",
+    html: `
+      
+        @layer <layer-name>? {
+          <stylesheet>
+        }
+      
+ `, + propertyName: "atrules", + css: { + "@layer": { + "value": "@layer ? { }", + "descriptors": [] + } + } + }, + + { + title: "combines an at-rule syntax with descriptor", + html: ` +
+        @font-face {
+          <declaration-list>
+        }
+      
+ + + + + + +
Name: + font-display +
For: + @font-face +
Value: + auto | block | swap | fallback | optional +
Initial: + auto +
+ `, + propertyName: "atrules", + css: { + "@font-face": { + "value": "@font-face { }", + "descriptors": [ + { + for: "@font-face", + initial: "auto", + name: "font-display", + value: "auto | block | swap | fallback | optional" + } + ] + } + } + }, + { title: "extracts multiple descriptors with the same name", @@ -301,22 +359,28 @@ const tests = [ Initial: auto `, - propertyName: "descriptors", + propertyName: "atrules", css: { - "font-display": [ - { - for: "@font-face", - initial: "auto", - name: "font-display", - value: "auto | block | swap | fallback | optional" - }, - { - for: "@font-feature-values", - initial: "auto", - name: "font-display", - value: "auto | block | swap | fallback | optional" - } - ] + "@font-face": { + "descriptors": [ + { + for: "@font-face", + initial: "auto", + name: "font-display", + value: "auto | block | swap | fallback | optional" + } + ] + }, + "@font-feature-values": { + "descriptors": [ + { + for: "@font-feature-values", + initial: "auto", + name: "font-display", + value: "auto | block | swap | fallback | optional" + } + ] + } } },