LibWeb/CSS: Serialize :heading(...) pseudo-class properly

We originally had special handling for `:host()` as that had been the
only pseudo-class that could be both an identifier or a function.
However, this meant duplicating the serialization logic, and also we
had to manually remember to add the same hack for any other
identifier-and-function cases. Which I forgot to do with `:heading()`!

So instead, for these cases, detect if they actually have arguments
specified and use that to determine which form to serialize as. We do
still have to write a check for each one of these pseudo-classes, but
the VERIFY should make it easier to remember.
This commit is contained in:
Sam Atkins 2025-08-28 10:25:44 +01:00 committed by Jelle Raaijmakers
commit 9ffc15ba3f
Notes: github-actions[bot] 2025-08-28 10:41:37 +00:00
3 changed files with 88 additions and 14 deletions

View file

@ -427,22 +427,24 @@ String Selector::SimpleSelector::serialize() const
auto& pseudo_class = this->pseudo_class();
auto metadata = pseudo_class_metadata(pseudo_class.type);
// HACK: `:host()` has both a function and a non-function form, so handle that first.
// It's also not in the spec.
if (pseudo_class.type == PseudoClass::Host) {
if (pseudo_class.argument_selector_list.is_empty()) {
s.append(':');
s.append(pseudo_class_name(pseudo_class.type));
} else {
s.append(':');
s.append(pseudo_class_name(pseudo_class.type));
s.append('(');
s.append(serialize_a_group_of_selectors(pseudo_class.argument_selector_list));
s.append(')');
bool accepts_arguments = [&]() {
if (!metadata.is_valid_as_function)
return false;
if (!metadata.is_valid_as_identifier)
return true;
// For pseudo-classes with both a function and identifier form, see if they have arguments.
switch (pseudo_class.type) {
case PseudoClass::Heading:
return !pseudo_class.an_plus_b_patterns.is_empty();
case PseudoClass::Host:
return !pseudo_class.argument_selector_list.is_empty();
default:
VERIFY_NOT_REACHED();
}
}
}();
// If the pseudo-class does not accept arguments append ":" (U+003A), followed by the name of the pseudo-class, to s.
else if (metadata.is_valid_as_identifier) {
if (!accepts_arguments) {
s.append(':');
s.append(pseudo_class_name(pseudo_class.type));
}

View file

@ -0,0 +1,34 @@
Harness status: OK
Found 28 tests
17 Pass
11 Fail
Pass ":heading" should be a valid selector
Pass ":heading(2)" should be a valid selector
Pass ":heading(99999)" should be a valid selector
Pass ":heading(0)" should be a valid selector
Pass ":heading(0, 1, 2)" should be a valid selector
Pass ":heading(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)" should be a valid selector
Pass ":heading(-1)" should be a valid selector
Pass "h1:heading" should be a valid selector
Pass "h1:heading(1)" should be a valid selector
Pass "h1:heading(2)" should be a valid selector
Pass ":heading()" should be an invalid selector
Pass ":heading(1.0)" should be an invalid selector
Pass ":heading(1.4)" should be an invalid selector
Fail ":heading(n)" should be an invalid selector
Fail ":heading(odd)" should be an invalid selector
Fail ":heading(even)" should be an invalid selector
Fail ":heading(2n)" should be an invalid selector
Fail ":heading(2n+1)" should be an invalid selector
Fail ":heading(2n+2)" should be an invalid selector
Fail ":heading(-n+3)" should be an invalid selector
Fail ":heading(2n, 3n)" should be an invalid selector
Fail ":heading(2, 3n)" should be an invalid selector
Fail ":heading(2 of .foo)" should be an invalid selector
Fail ":heading(2n of .foo)" should be an invalid selector
Pass ":heading(calc(1))" should be an invalid selector
Pass ":heading(max(1, 2))" should be an invalid selector
Pass ":heading(min(1, 2)" should be an invalid selector
Pass ":heading(var(--level))" should be an invalid selector

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Selectors: The heading pseudo-classes</title>
<link rel="help" href="https://drafts.csswg.org/selectors-5/#headings">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/parsing-testcommon.js"></script>
<script>
test_valid_selector(':heading');
test_valid_selector(':heading(2)');
test_valid_selector(':heading(99999)');
test_valid_selector(':heading(0)');
test_valid_selector(':heading(0, 1, 2)');
test_valid_selector(':heading(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)');
test_valid_selector(':heading(-1)');
test_valid_selector('h1:heading');
test_valid_selector('h1:heading(1)');
test_valid_selector('h1:heading(2)');
test_invalid_selector(':heading()');
test_invalid_selector(':heading(1.0)');
test_invalid_selector(':heading(1.4)');
test_invalid_selector(':heading(n)');
test_invalid_selector(':heading(odd)');
test_invalid_selector(':heading(even)');
test_invalid_selector(':heading(2n)');
test_invalid_selector(':heading(2n+1)');
test_invalid_selector(':heading(2n+2)');
test_invalid_selector(':heading(-n+3)');
test_invalid_selector(':heading(2n, 3n)');
test_invalid_selector(':heading(2, 3n)');
test_invalid_selector(':heading(2 of .foo)');
test_invalid_selector(':heading(2n of .foo)');
test_invalid_selector(':heading(calc(1))');
test_invalid_selector(':heading(max(1, 2))');
test_invalid_selector(':heading(min(1, 2)');
test_invalid_selector(':heading(var(--level))');
</script>