Skip to content

Commit 68e73e3

Browse files
committed
Web Inspector: Pull CSS property keyword completions from the backend instead of hardcoding them
https://bugs.webkit.org/show_bug.cgi?id=303627 rdar://problem/165914089 Reviewed by Tim Nguyen and Devin Rousso. The Web Inspector's CSS autocomplete system previously hardcoded all valid keyword values for CSS properties in CSSKeywordCompletions.js. This meant that when new CSS values were added to WebCore, they had to be manually duplicated in the Inspector frontend, and there was no way to filter values based on runtime settings (like feature flags). This change extends getSupportedCSSProperties() to return the valid keyword values for each property, pulled directly from CSSProperties.json. The frontend now uses these backend-provided values instead of the hardcoded ones, ensuring completions stay in sync with WebCore and respect feature flags. Key changes: - Added validKeywordsForProperty() to return keyword values from CSSProperties.json - Added isKeywordValidForPropertyValues() to filter keywords by settings flags - Added CSSParserContext(const Settings&) constructor for easy context creation - Updated getSupportedCSSProperties() to include filtered values in the response - Added settings-flag to grid-lanes/inline-grid-lanes values in CSSProperties.json * LayoutTests/inspector/css/getSupportedCSSProperties-expected.txt: * LayoutTests/inspector/css/getSupportedCSSProperties.html: * Source/WebCore/css/CSSProperties.json: * Source/WebCore/css/CSSProperty.h: * Source/WebCore/css/parser/CSSParserContext.cpp: (WebCore::CSSParserContext::CSSParserContext): * Source/WebCore/css/parser/CSSParserContext.h: * Source/WebCore/css/scripts/process-css-properties.py: (GenerateCSSPropertyNames._generate_css_property_names_gperf_prelude): (GenerateCSSPropertyNames): * Source/WebCore/inspector/agents/InspectorCSSAgent.cpp: (WebCore::InspectorCSSAgent::getSupportedCSSProperties): Canonical link: https://commits.webkit.org/304195@main
1 parent f6a83ee commit 68e73e3

File tree

8 files changed

+269
-42
lines changed

8 files changed

+269
-42
lines changed

LayoutTests/inspector/css/getSupportedCSSProperties-expected.txt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
"background-repeat" is supported
2+
"background-repeat" has keyword values:
3+
- "repeat-x"
4+
- "repeat-y"
5+
- "repeat"
6+
- "space"
7+
- "round"
8+
- "no-repeat"
29

310
"box-sizing" is supported
411
"box-sizing" has aliases:
@@ -7,12 +14,49 @@
714
- "border-box"
815
- "content-box"
916

17+
"display" is supported
18+
"display" has keyword values:
19+
- "inline"
20+
- "block"
21+
- "flow"
22+
- "flow-root"
23+
- "list-item"
24+
- "inline-block"
25+
- "table"
26+
- "inline-table"
27+
- "table-row-group"
28+
- "table-header-group"
29+
- "table-footer-group"
30+
- "table-row"
31+
- "table-column-group"
32+
- "table-column"
33+
- "table-cell"
34+
- "table-caption"
35+
- "flex"
36+
- "inline-flex"
37+
- "grid"
38+
- "inline-grid"
39+
- "grid-lanes"
40+
- "inline-grid-lanes"
41+
- "contents"
42+
- "none"
43+
- "-webkit-box"
44+
- "-webkit-inline-box"
45+
- "-webkit-flex"
46+
- "-webkit-inline-flex"
47+
1048
"filter" is supported
1149
"filter" has aliases:
1250
- "-webkit-filter"
51+
"filter" has keyword values:
52+
- "none"
1353

1454
"font-style" is supported
1555
"font-style" is inherited
56+
"font-style" has keyword values:
57+
- "normal"
58+
- "italic"
59+
- "oblique"
1660

1761
"margin" is supported
1862
"margin" has longhands:
@@ -27,5 +71,6 @@
2771
"text-transform" is inherited
2872
"text-transform" has keyword values:
2973
- "none"
74+
- "math-auto"
3075

3176

LayoutTests/inspector/css/getSupportedCSSProperties.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
const expectedProperties = [
1414
"background-repeat",
1515
"box-sizing",
16+
"display",
1617
"filter",
1718
"font-style",
1819
"margin",

Source/WebCore/css/CSSProperties.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,14 @@
483483
"inline-flex",
484484
"grid",
485485
"inline-grid",
486-
"grid-lanes",
487-
"inline-grid-lanes",
486+
{
487+
"value": "grid-lanes",
488+
"settings-flag": "gridLanesEnabled"
489+
},
490+
{
491+
"value": "inline-grid-lanes",
492+
"settings-flag": "gridLanesEnabled"
493+
},
488494
{
489495
"value": "ruby",
490496
"status": "unimplemented"

Source/WebCore/css/CSSProperty.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#include <WebCore/CSSPropertyNames.h>
2525
#include <WebCore/CSSValue.h>
26+
#include <WebCore/CSSValueKeywords.h>
2627
#include <WebCore/IsImportant.h>
2728
#include <WebCore/WritingMode.h>
2829
#include <wtf/BitSet.h>
@@ -32,6 +33,7 @@ namespace WebCore {
3233

3334
class CSSValueList;
3435
class Settings;
36+
struct CSSParserContext;
3537

3638
enum class IsImplicit : bool { No, Yes };
3739

@@ -140,6 +142,15 @@ class CSSProperty {
140142

141143
static bool disablesNativeAppearance(CSSPropertyID);
142144

145+
// Returns the valid keyword values for a property from CSSProperties.json.
146+
// This is used by the Inspector to provide completions for properties
147+
// that aren't keyword-fast-path eligible but still have enumerated values.
148+
static std::span<const CSSValueID> validKeywordsForProperty(CSSPropertyID);
149+
150+
// Checks if a keyword is valid for a property, taking settings flags into account.
151+
// This is used by the Inspector to filter keywords based on enabled settings.
152+
static bool isKeywordValidForPropertyValues(CSSPropertyID, CSSValueID, const CSSParserContext&);
153+
143154
const StylePropertyMetadata& metadata() const { return m_metadata; }
144155
static bool isColorProperty(CSSPropertyID propertyId)
145156
{

Source/WebCore/css/parser/CSSParserContext.cpp

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -78,46 +78,52 @@ CSSParserContext::CSSParserContext(const Document& document)
7878
}
7979

8080
CSSParserContext::CSSParserContext(const Document& document, const URL& sheetBaseURL, ASCIILiteral charset)
81-
: baseURL { sheetBaseURL.isNull() ? document.baseURL() : sheetBaseURL }
82-
, charset { charset }
83-
, mode { document.inQuirksMode() ? HTMLQuirksMode : HTMLStandardMode }
84-
, isHTMLDocument { document.isHTMLDocument() }
85-
, hasDocumentSecurityOrigin { sheetBaseURL.isNull() || document.protectedSecurityOrigin()->canRequest(baseURL, OriginAccessPatternsForWebProcess::singleton()) }
86-
, useSystemAppearance { document.settings().useSystemAppearance() }
87-
, counterStyleAtRuleImageSymbolsEnabled { document.settings().cssCounterStyleAtRuleImageSymbolsEnabled() }
88-
, springTimingFunctionEnabled { document.settings().springTimingFunctionEnabled() }
81+
: CSSParserContext(document.settings())
82+
{
83+
baseURL = sheetBaseURL.isNull() ? document.baseURL() : sheetBaseURL;
84+
this->charset = charset;
85+
mode = document.inQuirksMode() ? HTMLQuirksMode : HTMLStandardMode;
86+
isHTMLDocument = document.isHTMLDocument();
87+
hasDocumentSecurityOrigin = sheetBaseURL.isNull() || document.protectedSecurityOrigin()->canRequest(baseURL, OriginAccessPatternsForWebProcess::singleton());
88+
webkitMediaTextTrackDisplayQuirkEnabled = document.quirks().needsWebKitMediaTextTrackDisplayQuirk();
89+
}
90+
91+
CSSParserContext::CSSParserContext(const Settings& settings)
92+
: mode { HTMLStandardMode }
93+
, useSystemAppearance { settings.useSystemAppearance() }
94+
, counterStyleAtRuleImageSymbolsEnabled { settings.cssCounterStyleAtRuleImageSymbolsEnabled() }
95+
, springTimingFunctionEnabled { settings.springTimingFunctionEnabled() }
8996
#if HAVE(CORE_ANIMATION_SEPARATED_LAYERS)
90-
, cssTransformStyleSeparatedEnabled { document.settings().cssTransformStyleSeparatedEnabled() }
97+
, cssTransformStyleSeparatedEnabled { settings.cssTransformStyleSeparatedEnabled() }
9198
#endif
92-
, gridLanesEnabled { document.settings().gridLanesEnabled() }
93-
, cssAppearanceBaseEnabled { document.settings().cssAppearanceBaseEnabled() }
94-
, cssPaintingAPIEnabled { document.settings().cssPaintingAPIEnabled() }
95-
, cssShapeFunctionEnabled { document.settings().cssShapeFunctionEnabled() }
96-
, cssTextDecorationLineErrorValues { document.settings().cssTextDecorationLineErrorValues() }
97-
, cssBackgroundClipBorderAreaEnabled { document.settings().cssBackgroundClipBorderAreaEnabled() }
98-
, cssWordBreakAutoPhraseEnabled { document.settings().cssWordBreakAutoPhraseEnabled() }
99-
, popoverAttributeEnabled { document.settings().popoverAttributeEnabled() }
100-
, sidewaysWritingModesEnabled { document.settings().sidewaysWritingModesEnabled() }
101-
, cssTextWrapPrettyEnabled { document.settings().cssTextWrapPrettyEnabled() }
102-
, thumbAndTrackPseudoElementsEnabled { document.settings().thumbAndTrackPseudoElementsEnabled() }
99+
, gridLanesEnabled { settings.gridLanesEnabled() }
100+
, cssAppearanceBaseEnabled { settings.cssAppearanceBaseEnabled() }
101+
, cssPaintingAPIEnabled { settings.cssPaintingAPIEnabled() }
102+
, cssShapeFunctionEnabled { settings.cssShapeFunctionEnabled() }
103+
, cssTextDecorationLineErrorValues { settings.cssTextDecorationLineErrorValues() }
104+
, cssBackgroundClipBorderAreaEnabled { settings.cssBackgroundClipBorderAreaEnabled() }
105+
, cssWordBreakAutoPhraseEnabled { settings.cssWordBreakAutoPhraseEnabled() }
106+
, popoverAttributeEnabled { settings.popoverAttributeEnabled() }
107+
, sidewaysWritingModesEnabled { settings.sidewaysWritingModesEnabled() }
108+
, cssTextWrapPrettyEnabled { settings.cssTextWrapPrettyEnabled() }
109+
, thumbAndTrackPseudoElementsEnabled { settings.thumbAndTrackPseudoElementsEnabled() }
103110
#if ENABLE(SERVICE_CONTROLS)
104-
, imageControlsEnabled { document.settings().imageControlsEnabled() }
111+
, imageControlsEnabled { settings.imageControlsEnabled() }
105112
#endif
106-
, colorLayersEnabled { document.settings().cssColorLayersEnabled() }
107-
, contrastColorEnabled { document.settings().cssContrastColorEnabled() }
108-
, targetTextPseudoElementEnabled { document.settings().targetTextPseudoElementEnabled() }
109-
, cssProgressFunctionEnabled { document.settings().cssProgressFunctionEnabled() }
110-
, cssRandomFunctionEnabled { document.settings().cssRandomFunctionEnabled() }
111-
, cssTreeCountingFunctionsEnabled { document.settings().cssTreeCountingFunctionsEnabled() }
112-
, cssURLModifiersEnabled { document.settings().cssURLModifiersEnabled() }
113-
, cssURLIntegrityModifierEnabled { document.settings().cssURLIntegrityModifierEnabled() }
114-
, cssAxisRelativePositionKeywordsEnabled { document.settings().cssAxisRelativePositionKeywordsEnabled() }
115-
, cssDynamicRangeLimitMixEnabled { document.settings().cssDynamicRangeLimitMixEnabled() }
116-
, cssConstrainedDynamicRangeLimitEnabled { document.settings().cssConstrainedDynamicRangeLimitEnabled() }
117-
, cssTextTransformMathAutoEnabled { document.settings().cssTextTransformMathAutoEnabled() }
118-
, cssInternalAutoBaseParsingEnabled { document.settings().cssInternalAutoBaseParsingEnabled() }
119-
, webkitMediaTextTrackDisplayQuirkEnabled { document.quirks().needsWebKitMediaTextTrackDisplayQuirk() }
120-
, propertySettings { CSSPropertySettings { document.settings() } }
113+
, colorLayersEnabled { settings.cssColorLayersEnabled() }
114+
, contrastColorEnabled { settings.cssContrastColorEnabled() }
115+
, targetTextPseudoElementEnabled { settings.targetTextPseudoElementEnabled() }
116+
, cssProgressFunctionEnabled { settings.cssProgressFunctionEnabled() }
117+
, cssRandomFunctionEnabled { settings.cssRandomFunctionEnabled() }
118+
, cssTreeCountingFunctionsEnabled { settings.cssTreeCountingFunctionsEnabled() }
119+
, cssURLModifiersEnabled { settings.cssURLModifiersEnabled() }
120+
, cssURLIntegrityModifierEnabled { settings.cssURLIntegrityModifierEnabled() }
121+
, cssAxisRelativePositionKeywordsEnabled { settings.cssAxisRelativePositionKeywordsEnabled() }
122+
, cssDynamicRangeLimitMixEnabled { settings.cssDynamicRangeLimitMixEnabled() }
123+
, cssConstrainedDynamicRangeLimitEnabled { settings.cssConstrainedDynamicRangeLimitEnabled() }
124+
, cssTextTransformMathAutoEnabled { settings.cssTextTransformMathAutoEnabled() }
125+
, cssInternalAutoBaseParsingEnabled { settings.cssInternalAutoBaseParsingEnabled() }
126+
, propertySettings { CSSPropertySettings { settings } }
121127
{
122128
}
123129

Source/WebCore/css/parser/CSSParserContext.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
namespace WebCore {
3838

3939
class Document;
40+
class Settings;
4041

4142
struct CSSParserContext {
4243
WTF_DEPRECATED_MAKE_STRUCT_FAST_ALLOCATED(CSSParserContext);
@@ -95,6 +96,7 @@ struct CSSParserContext {
9596
CSSParserContext(CSSParserMode, const URL& baseURL = URL());
9697
WEBCORE_EXPORT CSSParserContext(const Document&);
9798
CSSParserContext(const Document&, const URL& baseURL, ASCIILiteral charset = ""_s);
99+
CSSParserContext(const Settings&);
98100

99101
void setUASheetMode();
100102

Source/WebCore/css/scripts/process-css-properties.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2967,7 +2967,10 @@ def _generate_css_property_names_gperf_prelude(self, *, to):
29672967
to=to,
29682968
headers=[
29692969
"BoxSides.h",
2970+
"CSSParserContext.h",
29702971
"CSSProperty.h",
2972+
"CSSValueKeywords.h",
2973+
"DeprecatedGlobalSettings.h",
29712974
"Settings.h",
29722975
],
29732976
system_headers=[
@@ -3359,6 +3362,138 @@ def _generate_css_property_id_text_stream(self, *, to):
33593362
}
33603363
""")
33613364

3365+
def _generate_valid_keywords_for_property(self, *, to):
3366+
# Generate static arrays of valid keyword CSSValueIDs for each property
3367+
# that has a 'values' array in CSSProperties.json. This is used by the
3368+
# Inspector to provide completions for properties that aren't keyword-fast-path
3369+
# eligible but still have enumerated values.
3370+
3371+
# First, collect all properties with values and generate static arrays for them
3372+
properties_with_values = []
3373+
seen_array_names = set()
3374+
for prop in self.properties_and_descriptors.style_properties.all:
3375+
if hasattr(prop, 'values') and prop.values:
3376+
# Filter to only include values that have a valid keyword_term (actual keywords)
3377+
keyword_values = [value for value in prop.values if hasattr(value, 'value_keyword_name') and value.value_keyword_name]
3378+
if keyword_values:
3379+
array_name = f"validKeywordsFor{prop.property_name.name_for_methods}"
3380+
# Skip if we've already generated an array with this name (handles aliases)
3381+
if array_name not in seen_array_names:
3382+
seen_array_names.add(array_name)
3383+
properties_with_values.append((prop, keyword_values, array_name))
3384+
3385+
# Generate static arrays for each property with values
3386+
for prop, keywordValues, array_name in properties_with_values:
3387+
value_ids = [value.value_keyword_name.id for value in keywordValues]
3388+
to.write(f"static constexpr std::array {array_name} {{")
3389+
with to.indent():
3390+
for value_id in value_ids:
3391+
to.write(f"{value_id},")
3392+
to.write("};")
3393+
to.newline()
3394+
3395+
# Generate the switch function - include all properties, even those with duplicate array names
3396+
all_properties_with_values = []
3397+
for prop in self.properties_and_descriptors.style_properties.all:
3398+
if hasattr(prop, 'values') and prop.values:
3399+
keyword_values = [value for value in prop.values if hasattr(value, 'value_keyword_name') and value.value_keyword_name]
3400+
if keyword_values:
3401+
array_name = f"validKeywordsFor{prop.property_name.name_for_methods}"
3402+
all_properties_with_values.append((prop, keyword_values, array_name))
3403+
3404+
to.write("std::span<const CSSValueID> CSSProperty::validKeywordsForProperty(CSSPropertyID id)")
3405+
to.write("{")
3406+
with to.indent():
3407+
to.write("switch (id) {")
3408+
for prop, keyword_values, array_name in all_properties_with_values:
3409+
to.write(f"case {prop.id}:")
3410+
with to.indent():
3411+
to.write(f"return std::span<const CSSValueID> {{ {array_name} }};")
3412+
to.write("default:")
3413+
with to.indent():
3414+
to.write("return { };")
3415+
to.write("}")
3416+
to.write("}")
3417+
to.newline()
3418+
3419+
# Generate isKeywordValidForPropertyValues function to check settings flags.
3420+
# This is used by the Inspector to filter keywords based on enabled settings.
3421+
3422+
# Collect properties that have any keywords with settings-flags
3423+
properties_with_settings_flags = []
3424+
for prop, keyword_values, array_name in all_properties_with_values:
3425+
# Check if any keyword has a settings_flag
3426+
keywords_with_flags = [(value, value.settings_flag) for value in keyword_values if value.settings_flag]
3427+
if keywords_with_flags:
3428+
properties_with_settings_flags.append((prop, keyword_values))
3429+
3430+
# Generate helper functions for properties with settings-flagged keywords
3431+
for prop, keyword_values in properties_with_settings_flags:
3432+
func_name = f"isKeywordValidFor{prop.property_name.name_for_methods}Values"
3433+
to.write(f"static bool {func_name}(CSSValueID keyword, const CSSParserContext& context)")
3434+
to.write("{")
3435+
with to.indent():
3436+
to.write("switch (keyword) {")
3437+
3438+
# Group keywords by their settings_flag (or lack thereof)
3439+
# Keywords without settings_flag always return true
3440+
keywords_without_flag = [value for value in keyword_values if not value.settings_flag]
3441+
keywords_with_flag = [value for value in keyword_values if value.settings_flag]
3442+
3443+
if keywords_without_flag:
3444+
for value in keywords_without_flag:
3445+
to.write(f"case {value.value_keyword_name.id}:")
3446+
with to.indent():
3447+
to.write("return true;")
3448+
3449+
# Group keywords by their settings_flag for efficient switch generation
3450+
from collections import defaultdict
3451+
flag_to_keywords = defaultdict(list)
3452+
for value in keywords_with_flag:
3453+
flag_to_keywords[value.settings_flag].append(value)
3454+
3455+
for flag, keywordValues in flag_to_keywords.items():
3456+
for value in keywordValues:
3457+
to.write(f"case {value.value_keyword_name.id}:")
3458+
with to.indent():
3459+
# Check if this is a function call (e.g., DeprecatedGlobalSettings::attachmentElementEnabled())
3460+
if "::" in flag or "(" in flag:
3461+
to.write(f"return {flag};")
3462+
else:
3463+
to.write(f"return context.{flag};")
3464+
3465+
to.write("default:")
3466+
with to.indent():
3467+
to.write("return false;")
3468+
to.write("}")
3469+
to.write("}")
3470+
to.newline()
3471+
3472+
# Generate the main isKeywordValidForPropertyValues switch function
3473+
to.write("bool CSSProperty::isKeywordValidForPropertyValues(CSSPropertyID id, CSSValueID keyword, const CSSParserContext& context)")
3474+
to.write("{")
3475+
with to.indent():
3476+
to.write("switch (id) {")
3477+
3478+
for prop, keyword_values, array_name in all_properties_with_values:
3479+
to.write(f"case {prop.id}:")
3480+
with to.indent():
3481+
# Check if this property has any keywords with settings flags
3482+
has_settings_flags = any(value.settings_flag for value in keyword_values)
3483+
if has_settings_flags:
3484+
func_name = f"isKeywordValidFor{prop.property_name.name_for_methods}Values"
3485+
to.write(f"return {func_name}(keyword, context);")
3486+
else:
3487+
# No settings flags, just check if keyword is in the valid set
3488+
to.write(f"return std::ranges::find({array_name}, keyword) != {array_name}.end();")
3489+
3490+
to.write("default:")
3491+
with to.indent():
3492+
to.write("return false;")
3493+
to.write("}")
3494+
to.write("}")
3495+
to.newline()
3496+
33623497
def _term_matches_number_or_integer(self, term):
33633498
if isinstance(term, MatchOneTerm):
33643499
return any(self._term_matches_number_or_integer(inner_term) for inner_term in term.subterms)
@@ -3584,6 +3719,10 @@ def generate_css_property_names_gperf(self):
35843719
to=writer
35853720
)
35863721

3722+
self._generate_valid_keywords_for_property(
3723+
to=writer
3724+
)
3725+
35873726
self._generate_css_property_names_gperf_footing(
35883727
to=writer
35893728
)

0 commit comments

Comments
 (0)