1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-11 18:20:43 +09:00

LibWeb/CSS: Bring :lang() matching closer to spec

With this, we pass the 8 ref tests in css/selectors/selectors-4/ which
previously failed. This is not technically a full implementation, as we
are supposed to first canonicalize the language range and tag, but that
will require downloading and processing the IANA language subtag
registry:

https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry

That's significantly more work, and WPT doesn't seem to test any cases
that require that, so we can leave it for now.
This commit is contained in:
Sam Atkins 2025-05-15 14:47:05 +01:00 committed by Tim Ledbetter
parent e0e513e9fc
commit 1fe29ac642
Notes: github-actions[bot] 2025-05-15 15:41:56 +00:00
28 changed files with 449 additions and 14 deletions

View file

@ -53,6 +53,67 @@ static inline GC::Ptr<DOM::Node const> traverse_up(GC::Ptr<DOM::Node const> node
return node->parent();
}
// https://www.rfc-editor.org/rfc/rfc4647.html#section-3.3.2
// NB: Language tags only use ASCII characters, so we can get away with using StringView.
static bool language_range_matches_tag(StringView language_range, StringView language_tag)
{
// 1. Split both the extended language range and the language tag being compared into a list of subtags by
// dividing on the hyphen (%x2D) character.
auto range_subtags = language_range.split_view('-', SplitBehavior::KeepEmpty);
auto tag_subtags = language_tag.split_view('-', SplitBehavior::KeepEmpty);
// Two subtags match if either they are the same when compared case-insensitively or the language range's subtag
// is the wildcard '*'.
auto subtags_match = [](StringView language_range_subtag, StringView language_subtag) {
return language_range_subtag == "*"sv
|| Infra::is_ascii_case_insensitive_match(language_range_subtag, language_subtag);
};
// 2. Begin with the first subtag in each list. If the first subtag in the range does not match the first
// subtag in the tag, the overall match fails. Otherwise, move to the next subtag in both the range and the
// tag.
auto tag_subtag = tag_subtags.begin();
auto range_subtag = range_subtags.begin();
if (!subtags_match(*range_subtag, *tag_subtag))
return false;
++tag_subtag;
++range_subtag;
// 3. While there are more subtags left in the language range's list:
while (!range_subtag.is_end()) {
// A. If the subtag currently being examined in the range is the wildcard ('*'), move to the next subtag in
// the range and continue with the loop.
if (*range_subtag == "*"sv) {
++range_subtag;
continue;
}
// B. Else, if there are no more subtags in the language tag's list, the match fails.
if (tag_subtag.is_end())
return false;
// C. Else, if the current subtag in the range's list matches the current subtag in the language tag's
// list, move to the next subtag in both lists and continue with the loop.
if (subtags_match(*range_subtag, *tag_subtag)) {
++range_subtag;
++tag_subtag;
continue;
}
// D. Else, if the language tag's subtag is a "singleton" (a single letter or digit, which includes the
// private-use subtag 'x') the match fails.
if (tag_subtag->length() == 1 && is_ascii_alphanumeric((*tag_subtag)[0])) {
return false;
}
// E. Else, move to the next subtag in the language tag's list and continue with the loop.
++tag_subtag;
}
// 4. When the language range's list has no more subtags, the match succeeds.
return true;
}
// https://drafts.csswg.org/selectors-4/#the-lang-pseudo
static inline bool matches_lang_pseudo_class(DOM::Element const& element, Vector<FlyString> const& languages)
{
@ -62,21 +123,17 @@ static inline bool matches_lang_pseudo_class(DOM::Element const& element, Vector
auto element_language = maybe_element_language.release_value();
// FIXME: This is ad-hoc. Implement a proper language range matching algorithm as recommended by BCP47.
for (auto const& language : languages) {
if (language.is_empty())
continue;
if (language == "*"sv)
// The elements content language matches a language range if its content language, as represented in BCP 47
// syntax, matches the given language range in an extended filtering operation per [RFC4647] Matching of Language
// Tags (section 3.3.2). Both the content language and the language range must be canonicalized and converted to
// extlang form as per section 4.5 of [RFC5646] prior to the extended filtering operation. The matching is
// performed case-insensitively within the ASCII range.
// FIXME: Canonicalize both as extlang.
for (auto const& language_range : languages) {
if (language_range_matches_tag(language_range, element_language))
return true;
auto element_language_length = element_language.bytes_as_string_view().length();
auto language_length = language.bytes_as_string_view().length();
if (element_language_length == language_length) {
if (Infra::is_ascii_case_insensitive_match(element_language, language))
return true;
} else if (element_language_length > language_length) {
if (element_language.starts_with_bytes(language, CaseSensitivity::CaseInsensitive) && element_language.bytes_as_string_view()[language_length] == '-')
return true;
}
}
return false;
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching reference</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<style>
div.test { color: green; }
</style>
<div class="test">This should be green</div>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
:lang(en-US) { color: green; }
</style>
<div class="test">This should be green</div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang(fr) { color: green; }
</style>
<div class="test"><span lang="fr">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("FR") { color: green; }
</style>
<div class="test"><span lang="fr">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("Fr") { color: green; }
</style>
<div class="test"><span lang="fR">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("fr") { color: green; }
</style>
<div class="test"><span lang="fr-CH">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: green; }
:lang("fr-CH") { color: red; }
</style>
<div class="test"><span lang="fr">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: green; }
:lang("fr-CH") { color: red; }
</style>
<div class="test"><span lang="fr-FR">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("*-CH") { color: green; }
</style>
<div class="test"><span lang="fr-CH">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("*-Latn") { color: green; }
</style>
<div class="test"><span lang="fr-Latn-FR">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("fr-FR") { color: green; }
</style>
<div class="test"><span lang="fr-Latn-FR">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("*-FR") { color: green; }
</style>
<div class="test"><span lang="fr-Latn-FR">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("fr", "nl", "de") { color: green; }
</style>
<div class="test"><span lang="fr-Latn-FR">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang(de, nl, fr) { color: green; }
</style>
<div class="test"><span lang="fr-Latn-FR">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: green; }
:lang(de, nl, 0, fr) { color: red; }
</style>
<div class="test"><span lang="fr">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: green; }
:lang(0) { color: red; }
</style>
<div class="test"><span lang="0">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang(\*-FR) { color: green; }
</style>
<div class="test"><span lang="fr-Latn-FR">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang(fr) { color: green; }
</style>
<div class="test"><span lang="fr-FR-x-foobar">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("fr-x-foobar") { color: green; }
</style>
<div class="test"><span lang="fr-Latn-FR-x-foobar">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("*-x-foobar") { color: green; }
</style>
<div class="test"><span lang="fr-Latn-FR-x-foobar">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: green; }
:lang("fr-x-foobar") { color: red; }
</style>
<div class="test"><span lang="fr">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("iw") { color: green; }
</style>
<div class="test"><span lang="iw-ase-jpan-basiceng">This should be green</span></div>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
span:lang("en-gb-oed") { color: magenta; }
span span:lang("*-gb") { color: green; }
</style>
<div class="test" lang="en-GB-oed"><span><span>This should be green</span></span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("i-navajo") { color: green; }
</style>
<div class="test"><span lang="i-navajo">This should be green</span></div>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("x") { color: green; } /* not a well-formed lang tag, but matches per
the Extended Filtering algorithm */
</style>
<div class="test"><span lang="x-lojban">This should be green</span></div>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("art") { color: green; }
</style>
<div class="test"><span lang="art-lojban">This should be green</span></div>

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>CSS Selectors 4 - :lang matching</title>
<link rel="author" title="Jonathan Kew" href="mailto:jkew@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-lang-pseudo">
<link rel="match" href="../../../../../expected/wpt-import/css/selectors/selectors-4/lang-000-ref.html">
<style>
div.test { color: red; }
:lang("art") { color: green; }
</style>
<!-- This can match :lang("art"), because "-x-lojban" is a private subtag,
so this is *not* the grandfathered "art-lojban" tag. -->
<div class="test"><span lang="art-x-lojban">This should be green</span></div>