"
+ slug = slug.replace(/\[.*\]/, '')
+ return `${span}`;
+ }
+ }
+ case 'Multiplier': {
+ // link to the Value Definition Symtax and provide a tooltip
+ let key = name;
+ if (name.startsWith('{')) {
+ key = '{}';
+ }
+ const info = syntaxDescriptions[key];
+ return `${name}`;
+ }
+ case 'Keyword': {
+ return `${name}`;
}
+ case 'Function': {
+ return `${name}`;
+ }
+ case 'Token': {
+ if (name === ')') {
+ // this is a closing bracket
+ return `${name}`;
+ }
+ }
+ case 'Group': {
+ // link from brackets to the value definition syntax docs
+ const info = syntaxDescriptions['[]'];
+ name = name.replace(/^\[/, `[`);
+ name = name.replace(/\]$/, `]`);
+
+ // link from combinators (except " ") to the value definition syntax docs
+ if (node.combinator && (node.combinator !== ' ')) {
+ const info = syntaxDescriptions[node.combinator];
+ // note that we are replacing the combinator surrounded by spaces, like " | "
+ name = name.replaceAll(` ${node.combinator} `, ` ${node.combinator} `);
+ }
+
+ return name;
+ }
+ default:
+ return name;
+ }
+}
- var formattedSyntax = rawSyntax;
- operators.forEach(function(operator) {
- if (formattedSyntax.indexOf("\n") !== -1 &&
- operator.title === "Curly braces") {
- return "";
- }
-
- formattedSyntax = formattedSyntax.replace(operator.regexp,
- function(match, text) {
- var linkText = match;
- if (match.indexOf("*") !== -1) {
- linkText = "*";
- } else if (match.indexOf("}") !== -1) {
- linkText = "}";
- } else if (typeof text === "string") {
- linkText = text;
- }
-
- var output = "" + linkText + "";
- if (linkText === "|") {
- output = " " + output + " ";
- } else if (linkText === "*" || linkText === "}") {
- output = text + output;
- }
-
- return output;
- });
+/**
+ * Generate the markup for every term in a syntax definition,
+ * ensuring that the terms are visually aligned
+ */
+function renderTerms(terms, combinator) {
+ let output = '';
+ const renderedTerms = [];
+
+ for (const term of terms) {
+ // figure out the lengths of the translated terms, without markup
+ // this is just so we can align the terms properly
+ const termTextLength = definitionSyntax.generate(term).length;
+ // get the translated terms, with markup
+ const termText = definitionSyntax.generate(term, { decorate: renderNode});
+ renderedTerms.push({
+ text: termText,
+ length: termTextLength
});
- formattedSyntax = await string.asyncReplace(
- formattedSyntax,
- /<([a-z0-9()'-]+?( \[-?(\d+|∞),(\d+|∞)\])?)>/gi,
- buildLink
- );
+ }
- return formattedSyntax;
+ const maxTermLength = Math.max(...renderedTerms.map(t => t.length));
+
+ // write out the translated terms, padding with spaces for alignment
+ // and separating terms using their combinator symbol
+ for (let i = 0; i < renderedTerms.length; i++) {
+ const termText = renderedTerms[i].text;
+ const spaceCount = (maxTermLength + 2) - renderedTerms[i].length;
+ let combinatorText = '';
+ if (combinator && combinator !== " ") {
+ const info = syntaxDescriptions[combinator];
+ // link from combinators (except " ") to the value definition syntax docs
+ combinatorText = `${combinator}`;
+ }
+ // omit the combinator for the final term
+ combinatorText = (i < renderedTerms.length-1 ? combinatorText : '');
+ output += ` ${termText}${' '.repeat(spaceCount)}${combinatorText}
`;
+ }
+
+ return output;
}
-async function formatTypesSyntax(formattedSyntax, describedTypes) {
- var formattedTypesSyntax = "";
- var types = [];
- var typeAnchorAttributes = formattedSyntax.match(/href=".+?"/g);
- if (typeAnchorAttributes) {
- typeAnchorAttributes.forEach(function(typeAnchorAttribute) {
- var type = typeAnchorAttribute.match(/href="(?:#|.*\/)(.+?)"/);
- if (types.indexOf(type[1]) === -1 &&
- type[1].indexOf(s_syntax_value_definition) === -1) {
- // Some data type page names have '_value' appended,
- // which needs to be removed in order to find the syntax
- var subType = type[1].replace("_value", "");
- // Describe this type if it exists in "syntaxes" and
- // it *does not* exist in externallyDescribedTypes
- if ((Object.prototype.hasOwnProperty.call(data.syntaxes, subType)) &&
- (!Object.prototype.hasOwnProperty.call(data.externallyDescribedTypes, subType))) {
- types.push(subType);
- }
- }
- });
-
- var typesSyntax = "";
- if (types.length > 0) {
- for(let index = 0; index < types.length; index++) {
- let type = types[index];
- // Avoid recursions by checking whether a type was already
- // described before
- // check whether https://github.com/mdn/data/pull/66 was merged
- var syntax = data.syntaxes[type].syntax || data.syntaxes[type];
- if (describedTypes.indexOf(type) === -1) {
- typesSyntax += "<" + type +
- "> = " + await formatSyntax(syntax);
- if (index < types.length - 1) {
- typesSyntax += "
";
- }
- describedTypes.push(type);
- }
- }
-
- if (typesSyntax !== "") {
- formattedTypesSyntax += "" + s_where +
- "
" + typesSyntax +
- "
";
- }
- }
-
- return formattedTypesSyntax + await formatTypesSyntax(typesSyntax, describedTypes);
+/**
+ * Render the syntax for a single type.
+ */
+function renderSyntax(type, syntax) {
+ // write out the name of this type
+ let output = `${type} =
`;
+
+ const ast = definitionSyntax.parse(syntax);
+ // if the combinator is ' ', write the complete type syntax in a single line
+ if (ast.combinator === ' ') {
+ output += renderTerms([ast], ast.combinator);
+ } else {
+ // otherwise write out each direct child in its own line
+ output += renderTerms(ast.terms, ast.combinator);
+ }
+
+ return output;
+}
+
+/**
+ * Get names of all the types in a given set of syntaxes
+ */
+function getTypesForSyntaxes(syntaxes, constituents) {
+
+ function processNode(node) {
+ if (node.type === 'Type' &&
+ (!constituents.includes(node.name))) {
+ constituents.push(node.name);
}
+ }
+
+ for (const syntax of syntaxes) {
+ let ast = definitionSyntax.parse(syntax);
+ definitionSyntax.walk(ast, processNode);
+ }
- return formattedTypesSyntax;
}
-if (!atRule) {
- var matches = null;
- if (slug) {
- matches = slug.match(/\/CSS\/(@.+?)(?=\/)/);
+/**
+ * Given an item (such as a CSS property), fetch all the types that participate
+ * in its formal syntax definition, either directly or transitively.
+ */
+function getConstituentTypes(propertySyntax) {
+ const allConstituents = [];
+ let oldConstituentsLength = 0;
+ // get all the types in the top-level syntax
+ let constituentSyntaxes = [propertySyntax];
+
+ // while an iteration added more types...
+ while (true) {
+ oldConstituentsLength = allConstituents.length;
+ getTypesForSyntaxes(constituentSyntaxes, allConstituents);
+
+ if (allConstituents.length <= oldConstituentsLength) {
+ break;
}
+ // get the syntaxes for all newly added constituents,
+ // and then get the types in those syntaxes
+ constituentSyntaxes = [];
+ for (let constituent of allConstituents.slice(oldConstituentsLength)) {
+
+ let constituentSyntaxEntry = valuespaces[`<${constituent}>`];
- if (matches) {
- atRule = matches[1];
+ if (constituentSyntaxEntry && constituentSyntaxEntry.value) {
+ constituentSyntaxes.push(constituentSyntaxEntry.value);
+ }
}
+ }
+ return allConstituents;
}
-if (name === "preview-wiki-content") {
- formattedSyntax = "" +
- localize(localStrings, "info_in_preview_not_available") + "";
-} else {
- if (atRule) {
- if (data.atRules[atRule] && data.atRules[atRule].descriptors && data.atRules[atRule].descriptors[name]) {
- rawSyntax = data.atRules[atRule].descriptors[name].syntax;
- }
- } else if (name[0] === "@") {
- if (data.atRules[name] && data.atRules[name].syntax) {
- rawSyntax = data.atRules[name].syntax;
- }
- } else if (name[0] === ":" && typeof data.selectors[name] !== 'undefined') {
- rawSyntax = data.selectors[name].syntax;
- } else if (data.properties[name]) {
- rawSyntax = data.properties[name].syntax;
- } else if (data.syntaxes[addBrackets(name)]) {
- rawSyntax = data.syntaxes[addBrackets(name)].syntax;
+/**
+ * Write out the complete formal syntax for a property.
+ *
+ * This includes the property's own syntax, described in `propertySyntax`,
+ * and also the syntax for any types that participate in the definition of
+ * the property.
+ */
+function writeFormalSyntax(propertySyntax) {
+ let output = '';
+ output += '';
+ // write the syntax for the property
+ output += renderSyntax(propertyName, propertySyntax);
+ output += '
';
+ // collect all the constituent types for the property
+ const types = getConstituentTypes(propertySyntax);
+
+ // and write each one out
+ for (const type of types) {
+ if (valuespaces[`<${type}>`] && valuespaces[`<${type}>`].value) {
+ output += renderSyntax(`<${type}>`, valuespaces[`<${type}>`].value);
+ output += '
';
}
+ }
- formattedSyntax = await formatSyntax(rawSyntax);
- formattedSyntax += await formatTypesSyntax(formattedSyntax, []);
+ output += '
';
+ return output;
}
-let out = '';
+let output = '';
-if (!formattedSyntax) {
- out = "No syntax available
No value found in the database.
";
+// get the syntax for this property
+const propertySyntax = getPropertySyntax(propertyName, parsedWebRef);
+
+if (!propertySyntax) {
+ output = 'Error: could not find syntax for this item';
} else {
- const rtlLocales = ['ar', 'he', 'fa'];
- const rtl = rtlLocales.includes(env.locale);
- out = `${formattedSyntax}
`
+ // write it out
+ output = writeFormalSyntax(propertySyntax);
}
-
-%><%- out %>
+%>
+<%-output%>
diff --git a/package.json b/package.json
index c2a490f8d741..0b67ff71a0af 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"@fast-csv/parse": "^4.3.6",
"@mdn/browser-compat-data": "^5.1.2",
"@use-it/interval": "^1.0.0",
+ "@webref/css": "^4.1.1",
"accept-language-parser": "^1.5.0",
"browser-specs": "^3.13.0",
"chalk": "^4.1.2",
@@ -75,6 +76,7 @@
"compression": "^1.7.4",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
+ "css-tree": "^2.1.0",
"dayjs": "^1.11.3",
"dexie": "^3.2.2",
"dotenv": "^16.0.1",
diff --git a/testing/tests/developing.spec.ts b/testing/tests/developing.spec.ts
index 3fc247d7a97c..208b852bbdfe 100644
--- a/testing/tests/developing.spec.ts
+++ b/testing/tests/developing.spec.ts
@@ -72,7 +72,7 @@ test.describe("Testing the kitchensink page", () => {
).json();
expect(doc.title).toBe("The MDN Content Kitchensink");
- expect(Object.keys(doc.flaws).length).toBe(0);
+ expect(doc.flaws).toEqual({});
});
// XXX Do more advanced tasks that test the server and document "CRUD operations"
diff --git a/testing/tests/index.test.ts b/testing/tests/index.test.ts
index e5a44cdecf91..e813a3b4bf25 100644
--- a/testing/tests/index.test.ts
+++ b/testing/tests/index.test.ts
@@ -1467,7 +1467,7 @@ test("img tags without 'src' should not crash", () => {
const { doc } = JSON.parse(fs.readFileSync(jsonFile, "utf-8")) as {
doc: Doc;
};
- expect(Object.keys(doc.flaws).length).toBe(0);
+ expect(doc.flaws).toEqual({});
});
test("/Web/Embeddable should have 3 valid live samples", () => {
@@ -1487,7 +1487,7 @@ test("/Web/Embeddable should have 3 valid live samples", () => {
const { doc } = JSON.parse(fs.readFileSync(jsonFile, "utf-8")) as {
doc: Doc;
};
- expect(Object.keys(doc.flaws).length).toBe(0);
+ expect(doc.flaws).toEqual({});
const builtFiles = fs.readdirSync(path.join(builtFolder));
expect(
diff --git a/yarn.lock b/yarn.lock
index 137513f556a3..580917de99e9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2586,6 +2586,11 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1"
integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==
+"@webref/css@^4.1.1":
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/@webref/css/-/css-4.1.1.tgz#ed67ed325a31b400937c28944f1f26029c27d231"
+ integrity sha512-HDviqRnmuv2qfnx8SDP7EYNERy7Q9OP7YDo1RUgOmonhGhL/Z6hCGFwgY9AZSvnQFF4xXTk52Z8UKW+fFg8PUg==
+
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -4137,6 +4142,14 @@ css-tree@^1.1.2, css-tree@^1.1.3:
mdn-data "2.0.14"
source-map "^0.6.1"
+css-tree@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.1.0.tgz#170e27ccf94e7c5facb183765c25898be843d1d2"
+ integrity sha512-PcysZRzToBbrpoUrZ9qfblRIRf8zbEAkU0AIpQFtgkFK0vSbzOmBCvdSAx2Zg7Xx5wiYJKUKk0NMP7kxevie/A==
+ dependencies:
+ mdn-data "2.0.27"
+ source-map-js "^1.0.1"
+
css-what@^6.0.1, css-what@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
@@ -8403,7 +8416,7 @@ mdn-data@2.0.14:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
-mdn-data@^2.0.27:
+mdn-data@2.0.27, mdn-data@^2.0.27:
version "2.0.27"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.27.tgz#1710baa7b0db8176d3b3d565ccb7915fc69525ab"
integrity sha512-kwqO0I0jtWr25KcfLm9pia8vLZ8qoAKhWZuZMbneJq3jjBD3gl5nZs8l8Tu3ZBlBAHVQtDur9rdDGyvtfVraHQ==