diff --git a/kumascript/macros/CSSSyntax.ejs b/kumascript/macros/CSSSyntax.ejs index 730e7a21b434..3bfd42d2d2f2 100644 --- a/kumascript/macros/CSSSyntax.ejs +++ b/kumascript/macros/CSSSyntax.ejs @@ -1,442 +1,339 @@ <% /* - Displays the syntax of a CSS property or descriptor - - $0 - property/descriptor name - defaults to the slug name - $1 - @-rule - defaults to the @-rule within the URL -*/ - -const mdnDataCSS = require('mdn-data/css'); -// List types for which we want to link to a description instead of -// including it in the formal syntax block -let s_named_color_link = mdn.localString({ - "en-US": "/docs/Web/CSS/color_value#Color_keywords", - "fr" : "/docs/Web/CSS/Type_color#Les_mots-clés", - "de" : "/docs/Web/CSS/Farben#Farbschlüsselwörter", - "es" : "/docs/Web/CSS/color_value#Color_keywords", - "ja" : "/docs/Web/CSS/color_value#Color_keywords", - "nl" : "/docs/Web/CSS/kleur_waarde#Color_keywords", - "pt-BR": "/docs/Web/CSS/color_value#Palavras-chave_de_cores", - "zh-CN": "/docs/Web/CSS/color_value#色彩关键字" -}); -let s_system_color_link = mdn.localString({ - "en-US": "/docs/Web/CSS/color_value#Color_keywords", - "fr" : "/docs/Web/CSS/Type_color#Couleurs_système", - "de" : "/docs/Web/CSS/Farben#Systemfarben", - "es" : "/docs/Web/CSS/color_value#Colores_de_Sistema", - "ja" : "/docs/Web/CSS/color_value#System_Colors", - "nl" : "/docs/Web/CSS/kleur_waarde#System_Colors", - "pt-BR": "/docs/Web/CSS/color_value#System_Colors", - "zh-CN": "/docs/Web/CSS/color_value#系统颜色" -}); -let externallyDescribedTypesData = { - "named-color": { - link: s_named_color_link + * Displays the formal syntax for a CSS property, using the values found in + * the https://github.com/w3c/webref package. + * + * The property name is taken from the page slug. + * + * The syntax is pretty-printed and syntax-highlighted. + */ + +const webRefData = require('@webref/css'); +const { definitionSyntax } = require('css-tree'); + +const locale = env.locale; + +// URL where we describe value definition syntax +const valueDefinitionUrl = `/${locale}/docs/Web/CSS/Value_definition_syntax`; + +// Used for building links and tooltips for parts of the value definition syntax +const syntaxDescriptions = { + '*': { + fragment: 'asterisk', + tooltip: mdn.localString({ 'en-US': 'Asterisk: the entity may occur zero, one or several times' }) }, - "deprecated-system-color": { - link: s_system_color_link + '+': { + fragment: 'plus', + tooltip: mdn.localString({ 'en-US': 'Plus: the entity may occur one or several times' }) + }, + '?': { + fragment: 'question_mark', + tooltip: mdn.localString({ 'en-US': 'Question mark: the entity is optional' }) + }, + '{}': { + fragment: 'curly_braces', + tooltip: mdn.localString({ 'en-US': 'Curly braces: encloses two integers defining the minimal and maximal numbers of occurrences of the entity' }) + }, + '#': { + fragment: 'hash_mark', + tooltip: mdn.localString({ 'en-US': 'Hash mark: the entity is repeated one or several times, each occurence separated by a comma' }) + }, + '!': { + fragment: 'exclamation_point_!', + tooltip: mdn.localString({ 'en-US': 'Exclamation point: the group must produce at least one value' }) + }, + '[]': { + fragment: 'brackets', + tooltip: mdn.localString({ 'en-US': 'Brackets: enclose several entities, combinators, and multipliers to transform them as a single component' }) + }, + '|': { + fragment: 'single_bar', + tooltip: mdn.localString({ 'en-US': 'Single bar: exactly one of the entities must be present' }) + }, + '||': { + fragment: 'double_bar', + tooltip: mdn.localString({ 'en-US': 'Double bar: one or several of the entities must be present, in any order' }) + }, + '&&': { + fragment: 'double_ampersand', + tooltip: mdn.localString({ 'en-US': 'Double ampersand: all of the entities must be present, in any order' }) } -}; -let data = { - ...mdnDataCSS, - externallyDescribedTypes: externallyDescribedTypesData, -}; -let slug = env.slug; -let locale = env.locale; -let name = $0 || (slug ? slug.split("/").pop().toLowerCase() : "preview-wiki-content"); -// "Conflicting" documents have an MD5 postfix to deal with multiple conflicting documents. -// In here we need a clean name so remove the MD5 part. -name = name.replace(/(.*)_[a-f0-9]{32}$/, (_, x) => x); -let atRule = $1; -let rawSyntax = ""; -let formattedSyntax = ""; -let localize = mdn.getLocalString; - -let localStrings = web.getJSONData("L10n-CSS"); - -let s_where = mdn.localString({ - "en-US": "where ", - "de" : "wobei ", - "fr" : "où ", - "ja" : "ここで", - "ru" : "где " -}); - -let s_syntax_value_definition = mdn.localString({ - "en-US": "Value_definition_syntax", - "de" : "Wertdefinitionssyntax", - "fr" : "Syntaxe_de_d%C3%A9finition_des_valeurs" -}); - -let s_or = mdn.localString({ - "en-US": "or", - "de" : "oder", - "ru" : "или" -}); - -let s_possible_values = mdn.localString({ - "en-US": "Possible values: ", - "de" : "Mögliche Werte: ", - "ja" : "使用可能な値: ", - "ru" : "Возможные значения: " -}); - -let s_number_followed_by = mdn.localString({ - "en-US": "a number followed by", - "de" : "eine Nummer gefolgt von", -}); - -let s_like = mdn.localString({ - "en-US": "like", - "de" : "wie", - "ru" : "как" -}); - -let typeInfo = { - "angle": { - "title": s_possible_values + s_number_followed_by + "'deg', 'grad', 'rad' " + s_or + " 'turn', " + s_like + " 2turn, 1.3rad, -60deg " + s_or + " 0grad." - }, - "blend-mode": { - "title": s_possible_values + "normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, luminosity" - }, - "color": { - "typeLinkName": "color_value" - }, - "custom-ident": {}, - "filter-function": { - "title": s_possible_values + "blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), saturate(), sepia()" - }, - "flex": { - "typeLinkName": "flex_value" - }, - "frequency": {}, - "ident": {}, - "image": {}, - "integer": {}, - "length": { - "title": s_possible_values + s_number_followed_by + "'em', 'ex', 'ch', 'rem', 'px', 'cm', 'mm', 'in', 'vh', 'vw', 'vmin', 'vmax', 'pt', 'pc' " + s_or + " 'px', " + s_like + " 3px, 1.5cm, -0.5em " + s_or + " 0" - }, - "number": {}, - "percentage": {}, - "position": { - "typeLinkName": "position_value" - }, - "resolution": {}, - "single-transition-timing-function": { - "title": s_possible_values + "cubic-bezier(), steps(), linear, ease, ease-in, ease-out, east-in-out, step-start-step-end" - }, - "string": {}, - "time": { - "title": s_possible_values + s_number_followed_by + "'s' " + s_or + " 'ms', " + s_like + " 3s, -2.5ms " + s_or + " 0s." - }, - "timing-function": { - "title": s_possible_values + "cubic-bezier(), steps(), linear, ease, ease-in, ease-out, east-in-out, step-start-step-end" - }, - "url": {}, - "uri": {}, - "transform-function": { - "title": s_possible_values + "matrix(), matrix3d(), rotate(), rotate3d(), rotateX(), rotateY(), rotateZ(), scale(), scale3d(), scaleX(), scaleY(), scaleZ(), skewX(), skewY(), translate(), translate3d(), translateX(), translateY(), translateZ()" - } -}; - -let operators = [ - { - regexp: /#/g, - title: mdn.localString({ - "en-US": "Hash mark: the entity is repeated one or several times, each occurence separated by a comma", - "de": "Rautensymbol: Die Entität wird einmal oder mehrmals wiederholt, wobei jedes Vorkommen durch ein Komma getrennt wird" - }), - anchor: mdn.localString({ - "en-US": "Hash_mark", - "de" : "Rautensymbol_()" - }) - }, - { - regexp: /\|\|/g, - title: mdn.localString({ - "en-US": "Double bar: one or several of the entities must be present, in any order", - "de" : "Doppelter Balken: Eine oder mehrere der Entitäten muss angegeben werden, in beliebiger Reihenfolge" - }), - anchor: mdn.localString({ - "en-US": "Double_bar", - "de" : "Doppelter_Balken" - }) - }, - { - regexp: /[^|](\|)[^|]/g, - title: mdn.localString({ - "en-US": "Single bar: exactly one of the entities must be present", - "de" : "Einfacher Balken: Genau eine der Entitäten muss angegeben werden" - }), - anchor: mdn.localString({ - "en-US": "Single_bar", - "de" : "Einfacher_Balken" - }) - }, - { - regexp: /&&/g, - title: mdn.localString({ - "en-US": "Double ampersand: all of the entities must be present, in any order", - "de" : "Doppeltes Und-Zeichen: Alle Entitäten müssen angegeben werden, in beliebiger Reihenfolge" - }), - anchor: mdn.localString({ - "en-US": "Double_ampersand", - "de" : "Doppeltes_Und-Zeichen" - }) - }, - { - regexp: /\?/g, - title: mdn.localString({ - "en-US": "Question mark: the entity is optional", - "de" : "Fragezeichen: Die Entität ist optional" - }), - anchor: mdn.localString({ - "en-US": "Question_mark", - "de" : "Fragezeichen_()" - }) - }, - { - regexp: /([^/])\*(?!\/)/g, - title: mdn.localString({ - "en-US": "Asterisk: the entity may occur zero, one or several times", - "de" : "Asterisk: Die Entität kann keinmal, einmal oder mehrmals vorkommen" - }), - anchor: mdn.localString({ - "en-US": "Asterisk", - "de" : "Asterisk_(*)" - }) - }, - { - regexp: /\+/g, - title: mdn.localString({ - "en-US": "Plus: the entity may occur one or several times", - "de" : "Plus: Die Entität kann einmal oder mehrmals vorkommen" - }), - anchor: mdn.localString({ - "en-US": "Plus", - "de" : "Plus_()" - }) - }, - { - regexp: /(\[|\])(?![^<]*>)/g, - title: mdn.localString({ - "en-US": "Brackets: enclose several entities, combinators, and multipliers to transform them as a single component", - "de" : "Eckige Klammern: umschließen mehrere Entitäten, Kombinatoren und Multiplikatoren, um diese als eine gesamte Komponente zu behandeln" - }), - anchor: mdn.localString({ - "en-US": "Brackets", - "de" : "Eckige_Klammern" - }) - }, - { - regexp: /\{(?!\s)|(\d)\}/g, - title: mdn.localString({ - "en-US": "Curly braces: encloses two integers defining the minimal and maximal numbers of occurrences of the entity", - "de" : "Geschweifte Klammern: umschließen zwei Ganzzahlen, die die Minimal- und Maximalanzahl an Vorkommen der Entität angeben" - }), - anchor: mdn.localString({ - "en-US": "Curly_braces", - "de" : "Geschweifte_Klammern_(_)" - }) - }, - { - regexp: /!/g, - title: mdn.localString({ - "en-US": "Exclamation point: the group must produce at least one value", - "de" : "Ausrufezeichen: die Gruppe muss mindestens einen Wert erzeugen" - }), - anchor: mdn.localString({ - "en-US": "Exclamation_point", - "de" : "Ausrufezeichen_(!)" - }) - }, -]; - -async function buildLink(match, type) { - var link = "/" + locale + "/docs/Web/CSS/"; - var title = ""; - var propertyName = type.match(/'(.+?)'/); - - // Handle property references like <'color'> - if (propertyName) { - if (data.properties[propertyName[1]]) { - title = data.properties[propertyName[1]].syntax; - } - return "<" + propertyName[0] + ">"; - - // Handle basic types - } else if (Object.prototype.hasOwnProperty.call(typeInfo, type)) { - var typeLinkName = typeInfo[type].typeLinkName || type; - - link += typeLinkName; - - if (typeInfo[type].title) { - title = typeInfo[type].title; - } else { - title = ""; - } - - // Handle types which we want to describe using a link to a different page - } else if (Object.prototype.hasOwnProperty.call(data.externallyDescribedTypes, type)) { - return "<" + type + ">"; - - // Handle advanced types having their syntax defined within the 'CSSData' template - } else if (data.syntaxes[type]) { - return "<" + type + ">"; - - // Handled advanced types having their syntax not defined - } else { - return "<" + type + ">"; - } +} + +// get the contents of webref +const parsedWebRef = await webRefData.listAll(); - return "<" + type + ">"; +// get all the value syntaxes +let valuespaces = {}; +for (const spec of Object.values(parsedWebRef)) { + valuespaces = {...valuespaces, ...spec.valuespaces}; } -function addBrackets(str) { - return str.match(/\(\)$/) ? str : str + "()"; +// get the property name from the page slug +const propertyName = $0 || env.slug.split('/').pop().toLowerCase(); + +/** + * Get the formal syntax for a property from the webref data, given: + * @param {string} propertyName - the name of the property + * @param {object} parsedWebRef - the webref data + */ +function getPropertySyntax(propertyName) { + // 1) get all specs which list this property + let specsForProp = []; + for (const [shortname, data] of Object.entries(parsedWebRef)) { + const propNames = Object.keys(data.properties); + if (propNames.includes(propertyName)) { + specsForProp.push(shortname); + } + } + // 2) If we have more than one spec, filter out specs that end "-n" where n is a number + if (specsForProp.length > 1) { + specsForProp = specsForProp.filter( specName => !(/-\d+$/.test(specName)) ); + } + // 3) If we now have only one spec, return the syntax it lists + if (specsForProp.length === 1) { + return parsedWebRef[specsForProp[0]].properties[propertyName].value; + } + // 4) If we still have > 1 spec, assume that: + // - one of them is the base spec, which defines `values`, + // - the others define incremental additions as `newValues` + let syntax = ''; + let newSyntaxes = ''; + for (const specName of specsForProp) { + const baseValue = parsedWebRef[specName].properties[propertyName].value; + if (baseValue) { + syntax = baseValue; + } + const newValues = parsedWebRef[specName].properties[propertyName].newValues; + if (newValues) { + newSyntaxes += ` | ${newValues}`; + } + } + // Concatenate new values on to values to return a single syntax string + if (newSyntaxes) { + syntax += newSyntaxes; + } + return syntax; } -async function formatSyntax(rawSyntax) { - if (rawSyntax === "") { - return ""; +/** + * Determines the markup to generate for a single node in the AST + * generated by css-tree. + */ +function renderNode(name, node) { + switch (node.type) { + case 'Property': { + return `${name}`; + } + case 'Type': { + // encode < and > + let encoded = name.replaceAll('<', '<'); + encoded = encoded.replaceAll('>', '>'); + // add CSS class: we use "property" because there isn't one for types + const span = `${encoded}`; + // if this type is not included in the syntax, link to its dedicated page + if (valuespaces[name] && valuespaces[name].value) { + return span; + } else { + // the slug does not include the angle brackets + let slug = name.replaceAll('<', ''); + slug = slug.replaceAll('>', ''); + // also remove the range in square brackets, as in "" + 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==