Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/data/baseline-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,12 +589,11 @@ export const functions = new Map([
["sign", "5:2025"],
["anchor", "5:2026"],
["anchor-size", "5:2026"],
["color", "10:2023"],
["image", "10:2015"],
["attr", "10:2015"],
["calc", "10:2015"],
["calc-size", "0:"],
["rect", "5:2024"],
["color", "10:2023"],
["color-mix", "10:2023"],
["conic-gradient", "10:2020"],
["repeating-conic-gradient", "10:2020"],
Expand All @@ -620,12 +619,14 @@ export const functions = new Map([
["opacity", "10:2016"],
["saturate", "10:2016"],
["sepia", "10:2016"],
["fit-content", "0:"],
["linear-gradient", "10:2015"],
["radial-gradient", "10:2015"],
["repeating-linear-gradient", "10:2015"],
["repeating-radial-gradient", "10:2015"],
["hsl", "10:2015"],
["hwb", "10:2022"],
["image", "0:"],
["image-set", "5:2023"],
["lab", "10:2023"],
["lch", "10:2023"],
Expand Down
16 changes: 16 additions & 0 deletions tests/rules/use-baseline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,22 @@ ruleTester.run("use-baseline", rule, {
},
],
},
{
code: 'a { background-image: image("foo.png", red); }',
errors: [
{
messageId: "notBaselineFunction",
data: {
function: "image",
availability: "widely",
},
line: 1,
column: 23,
endLine: 1,
endColumn: 44,
},
],
},
{
code: "@media (inverted-colors: inverted) { a { color: red; } }",
errors: [
Expand Down
218 changes: 113 additions & 105 deletions tools/generate-baseline.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import prettier from "prettier";
import fs from "node:fs";

//------------------------------------------------------------------------------
// Helpers
// Constants
//------------------------------------------------------------------------------

/*
Expand All @@ -25,6 +25,13 @@ import fs from "node:fs";

const WIDE_SUPPORT_PROPERTIES = new Set(["cursor"]);

/*
* Some bare `css.types.<name>` compat keys refer to CSS data types
* instead of functions. For example, `css.types.image` refers to `<image>`,
* not `image()`.
*/
const UNGROUPED_NON_FUNCTION_TYPES = new Set(["color", "image"]);

const BASELINE_HIGH = 10;
const BASELINE_LOW = 5;
const BASELINE_FALSE = 0;
Expand All @@ -34,6 +41,34 @@ const baselineIds = new Map([
[false, BASELINE_FALSE],
]);

/*
* The following regular expressions are used to match the keys in the
* features object. The regular expressions are used to extract the
* property name, value, at-rule, type, or selector from the key.
*
* For example, the key "css.properties.color" would match the
* property regular expression and the "color" property would be
* extracted.
*
* Note that these values cannot contain underscores as underscores are
* only used in feature names to provide descriptions rather than syntax.
* Example: css.properties.align-self.position_absolute_context
*/
const PATTERNS = {
property: /^css\.properties\.(?<property>[a-zA-Z$\d-]+)$/u,
propertyValue:
/^css\.properties\.(?<property>[a-zA-Z$\d-]+)\.(?<value>[a-zA-Z$\d-]+)$/u,
atRule: /^css\.at-rules\.(?<atRule>[a-zA-Z$\d-]+)$/u,
mediaCondition: /^css\.at-rules\.media\.(?<condition>[a-zA-Z$\d-]+)$/u,
selector: /^css\.selectors\.(?<selector>[a-zA-Z$\d-]+)$/u,
type: /^css\.types\.(?:(?<group>[a-zA-Z\d-]+)\.)?(?<type>[a-zA-Z\d-]+)$/u,
functionName: /^(?<name>[a-zA-Z\d-]+)\(\)$/u,
};

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
* Encodes the baseline status and year fields into a single string.
* @param {string} status The feature's baseline status.
Expand Down Expand Up @@ -61,19 +96,26 @@ function mapFeatureStatus(status) {
}

/**
* Flattens the compat features into an object where the key is the feature
* name and the value is the baseline.
* @param {[string, {compat_features?: string[]}]} featureEntry The entry to flatten.
* @returns {Object} The flattened entry.
* Determines if a name matches a known CSS function in MDN data.
* @param {string} functionName The function name without trailing parentheses.
* @returns {boolean} True if the function exists in MDN data.
*/
function isKnownCSSFunction(functionName) {
return `${functionName}()` in mdnData.css.functions;
}

/**
* Determines if a compat-derived CSS type key actually represents a function.
* @param {string|undefined} group The optional CSS type group.
* @param {string} typeName The parsed type name to check (e.g., "color" or "calc").
* @returns {boolean} True if the compat key represents a CSS function.
*/
function flattenCompatFeatures([featureId, entry]) {
if (!entry.compat_features) {
return {};
function isCompatTypeAFunction(group, typeName) {
if (!group && UNGROUPED_NON_FUNCTION_TYPES.has(typeName)) {
return false;
}

return Object.fromEntries(
entry.compat_features.map(feature => [feature, featureId]),
);
return isKnownCSSFunction(typeName);
}

/**
Expand All @@ -82,120 +124,86 @@ function flattenCompatFeatures([featureId, entry]) {
* @returns {Object} The extracted CSS features.
*/
function extractCSSFeatures(features) {
/*
* The following regular expressions are used to match the keys in the
* features object. The regular expressions are used to extract the
* property name, value, at-rule, type, or selector from the key.
*
* For example, the key "css.properties.color" would match the
* cssPropertyPattern regular expression and the "color" property would be
* extracted.
*
* Note that these values cannot contain underscores as underscores are
* only used in feature names to provide descriptions rather than syntax.
* Example: css.properties.align-self.position_absolute_context
*/
const cssPropertyPattern = /^css\.properties\.(?<property>[a-zA-Z$\d-]+)$/u;
const cssPropertyValuePattern =
/^css\.properties\.(?<property>[a-zA-Z$\d-]+)\.(?<value>[a-zA-Z$\d-]+)$/u;
const cssAtRulePattern = /^css\.at-rules\.(?<atRule>[a-zA-Z$\d-]+)$/u;
const cssMediaConditionPattern =
/^css\.at-rules\.media\.(?<condition>[a-zA-Z$\d-]+)$/u;
const cssTypePattern = /^css\.types\.(?:.*?\.)?(?<type>[a-zA-Z\d-]+)$/u;
const cssSelectorPattern = /^css\.selectors\.(?<selector>[a-zA-Z$\d-]+)$/u;

const properties = {};
const propertyValues = {};
const atRules = {};
const mediaConditions = {};
const functions = {};
const selectors = {};

for (const [key, featureId] of Object.entries(features)) {
const feature = webFeatures[featureId];
const status = feature.status.by_compat_key[key];
let match;

// property names
if (
(match = cssPropertyPattern.exec(key)) !== null &&
!WIDE_SUPPORT_PROPERTIES.has(match.groups.property)
) {
properties[match.groups.property] = mapFeatureStatus(status);
continue;
}

// property values
if ((match = cssPropertyValuePattern.exec(key)) !== null) {
// don't include values for these properties
if (WIDE_SUPPORT_PROPERTIES.has(match.groups.property)) {
continue;
}

if (!propertyValues[match.groups.property]) {
propertyValues[match.groups.property] = {};
}
propertyValues[match.groups.property][match.groups.value] =
mapFeatureStatus(status);
continue;
}
const output = {
properties: {},
propertyValues: {},
atRules: {},
mediaConditions: {},
selectors: {},
functions: {},
};

// at-rules
if ((match = cssAtRulePattern.exec(key)) !== null) {
atRules[match.groups.atRule] = mapFeatureStatus(status);
continue;
for (const feature of Object.values(features)) {
// Check if the feature name itself represents a function
const nameMatch = PATTERNS.functionName.exec(feature.name);
if (nameMatch && isKnownCSSFunction(nameMatch.groups.name)) {
output.functions[nameMatch.groups.name] = mapFeatureStatus(
feature.status,
);
}

// Media conditions (@media features)
if ((match = cssMediaConditionPattern.exec(key)) !== null) {
mediaConditions[match.groups.condition] = mapFeatureStatus(status);
if (!feature.compat_features) {
continue;
}

// functions
if ((match = cssTypePattern.exec(key)) !== null) {
const type = match.groups.type;
if (!(`${type}()` in mdnData.css.functions)) {
for (const key of feature.compat_features) {
if (!key.startsWith("css.")) {
continue;
}
functions[type] = mapFeatureStatus(status);
continue;
}

// selectors
if ((match = cssSelectorPattern.exec(key)) !== null) {
selectors[match.groups.selector] = mapFeatureStatus(status);
continue;
const status = feature.status.by_compat_key[key];
let match;

// properties
if ((match = PATTERNS.property.exec(key))) {
if (!WIDE_SUPPORT_PROPERTIES.has(match.groups.property)) {
output.properties[match.groups.property] =
mapFeatureStatus(status);
}
}
// property values
else if ((match = PATTERNS.propertyValue.exec(key))) {
if (!WIDE_SUPPORT_PROPERTIES.has(match.groups.property)) {
output.propertyValues[match.groups.property] ??= {};
output.propertyValues[match.groups.property][
match.groups.value
] = mapFeatureStatus(status);
}
}
// at-rules
else if ((match = PATTERNS.atRule.exec(key))) {
output.atRules[match.groups.atRule] = mapFeatureStatus(status);
}
// media conditions (@media features)
else if ((match = PATTERNS.mediaCondition.exec(key))) {
output.mediaConditions[match.groups.condition] =
mapFeatureStatus(status);
}
// functions
else if ((match = PATTERNS.type.exec(key))) {
const { group, type } = match.groups;
if (isCompatTypeAFunction(group, type)) {
output.functions[type] = mapFeatureStatus(status);
}
}
// selectors
else if ((match = PATTERNS.selector.exec(key))) {
output.selectors[match.groups.selector] =
mapFeatureStatus(status);
}
}
}

return {
properties,
propertyValues,
atRules,
mediaConditions,
functions,
selectors,
};
return output;
}

//------------------------------------------------------------------------------
// Main
//------------------------------------------------------------------------------

// create one object with all features then filter just on the css ones
const allFeatures = Object.entries(webFeatures).reduce(
(acc, entry) => Object.assign(acc, flattenCompatFeatures(entry)),
{},
);
const cssFeatures = extractCSSFeatures(
Object.fromEntries(
Object.entries(allFeatures).filter(([key]) => key.startsWith("css.")),
),
);
const cssFeatures = extractCSSFeatures(webFeatures);
const featuresPath = "./src/data/baseline-data.js";

// export each group separately as a Set, such as highProperties, lowProperties, etc.
const code = `/**
* @fileoverview CSS features extracted from the web-features package.
* @author tools/generate-baseline.js
Expand Down