Skip to content
Permalink
Browse files
:has(:lang(~)) doesn't get invalidated
https://bugs.webkit.org/show_bug.cgi?id=243172

Reviewed by Antti Koivistso.

Make container query work with :lang pseudo class.

This patch introduces ElementRareData::effectiveLang to store the language of each element,
and uses Style::PseudoClassChangeInvalidation to invalidate styles for :lang pseudo class.

As an optimization, this patch also introduces Document::effectiveDocumentElementLanguage
which is the effective language of the document element as many websites specify a language there.
This avoids creating ElementRareData on all elements in the vast majority of cases.

* LayoutTests/fast/css/lang-pseudo-container-query-document-element-invalidation-expected.html: Added.
* LayoutTests/fast/css/lang-pseudo-container-query-document-element-invalidation.html: Added.pee
* LayoutTests/fast/css/lang-pseudo-container-query-invalidation-expected.html: Added.
* LayoutTests/fast/css/lang-pseudo-container-query-invalidation-xhtml-expected.html: Added.
* LayoutTests/fast/css/lang-pseudo-container-query-invalidation-xhtml.xhtml: Added.
* LayoutTests/fast/css/lang-pseudo-container-query-invalidation.html: Added.
* LayoutTests/fast/css/lang-pseudo-container-query-multiple-document-elements-invalidation-expected.html: Added.
* LayoutTests/fast/css/lang-pseudo-container-query-multiple-document-elements-invalidation.html: Added.

* Source/WebCore/css/SelectorCheckerTestFunctions.h:
(WebCore::matchesLangPseudoClass): Use newly introduced effectiveLang.

* Source/WebCore/dom/Document.cpp:
(WebCore::Document::childrenChanged):
(WebCore::Document::effectiveDocumentElementLanguage): Added.
(WebCore::Document::setDocumentElementLanguage): Added.
* Source/WebCore/dom/Document.h:

* Source/WebCore/dom/Element.cpp:
(WebCore::effectiveLangFromAttribute):
(WebCore::Element::attributeChanged): Added the style invalidation logic for :lang.
(WebCore::Element::insertedIntoAncestor): Propagate the lang attribute from the parent.
(WebCore::Element::removedFromAncestor): Clear the effective lang when appropriate.
(WebCore::Element::computeInheritedLanguage const): Deleted.
(WebCore::Element::effectiveLang const): Added.
(WebCore::Element::langFromAttribute const): Added.
(WebCore::Element::locale const):

* Source/WebCore/dom/Element.h:

* Source/WebCore/dom/ElementRareData.cpp:
(WebCore::SameSizeAsElementRareData):

* Source/WebCore/dom/ElementRareData.h:
(WebCore::ElementRareData::effectiveLang const): Added.
(WebCore::ElementRareData::setEffectiveLang): Added.
(WebCore::ElementRareData::useTypes const):

* Source/WebCore/dom/NodeRareData.h:
* Source/WebCore/style/PseudoClassChangeInvalidation.cpp:
(WebCore::Style::PseudoClassChangeInvalidation::computeInvalidation):
(WebCore::Style::PseudoClassChangeInvalidation::collectRuleSets):
* Source/WebCore/style/PseudoClassChangeInvalidation.h:
(WebCore::Style::PseudoClassChangeInvalidation::Value): Added.
(WebCore::Style::PseudoClassChangeInvalidation::PseudoClassChangeInvalidation): Added a new variant
which takes AnyValueTag as an argument.

* Source/WebCore/html/BaseDateAndTimeInputType.cpp:
(WebCore::BaseDateAndTimeInputType::localeIdentifier const):
(WebCore::BaseDateAndTimeInputType::setupDateTimeChooserParameters):

* Tools/TestWebKitAPI/Tests/WebCore/DocumentOrder.cpp:
(TestWebKitAPI::createDocument): Initialize XMLNames via ProcessWarming.

Canonical link: https://commits.webkit.org/253764@main
  • Loading branch information
rniwa committed Aug 25, 2022
1 parent 5f7ece2 commit 0c6665bbc95114525dc42754787d5e2527a51b94
Show file tree
Hide file tree
Showing 20 changed files with 268 additions and 26 deletions.
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<div style="width: 100px; height: 100px; background-color: green;"></div>
</body>
</html>
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html class="reftest-wait">
<head>
<meta http-equiv="content-language" content="fr">
</head>
<body>
<div><span></span></div>
<style>
div { width: 100px; height: 100px; }
div:has(*:lang(fr)) { background: red; }
div:has(*:lang(en)) { background: green; }
</style>
<script>
requestAnimationFrame(() => {
setTimeout(() => {
document.documentElement.lang = 'en';
document.documentElement.className = '';
}, 0);
});
</script>
</body>
</html>
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<div style="width: 100px; height: 100px; background-color: green;"></div>
</body>
</html>
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<div style="width: 100px; height: 100px; background-color: green;"></div>
</body>
</html>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html class="reftest-wait" xmlns="http://www.w3.org/1999/xhtml">
<body>
<section class="lang"><div xml:lang="zh" lang="ja"></div></section>
<section class="lang"><div id="ja" xml:lang="ja" lang="ja"></div></section>
<div><section class="lang" id="fr" lang="fr" style="-webkit-locale: 'en'"><div></div></section></div>
<div><section class="lang" id="es" xml:lang="es"><div></div></section></div>
<section class="lang"><div id="kr" xml:lang="kr" lang="en"></div></section>
<style>
body > * { background: red; }
section, div { width: 100px; height: 20px; }
.lang:has(:lang(zh)) { background: green; }
.lang:has(:lang(ja)) { background: red; }
div:has(.lang:lang(fr)) { background: red; }
div:has(.lang:lang(en)) { background: green; }
div:has(#es:lang(es)) { background: red; }
div:has(#es:lang(en)) { background: green; }
.lang:has(#kr:lang(kr)) { background: red; }
.lang:has(#kr:lang(kr)) { background: green; }
div:has(.lang > :lang(pt)) { background: red; }
</style>
<script>
requestAnimationFrame(() => {
setTimeout(() => {
document.getElementById('ja').setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:lang', 'zh');
document.getElementById('fr').setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:lang', 'en');
document.getElementById('es').setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:lang', 'en');
document.getElementById('kr').removeAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:lang');
document.documentElement.className = '';
}, 0);
});
</script>
</body>
</html>
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html class="reftest-wait">
<body>
<section class="lang"><div lang="en"></div></section>
<section class="lang" id="fr" lang="fr"><div></div></section>
<section class="lang" id="ja" lang="ja" style="-webkit-locale: 'en'"><div></div></section>
<div><section class="lang" id="zh" lang="zh" style="-webkit-locale: 'en'"><span></span></section></div>
<div id="kr" class="lang" lang="kr"></div>
<style>
div { width: 100px; height: 20px; }
.lang div:lang(en) { background: green; }
.lang div:lang(fr) { background: red; }
.lang div:lang(ja) { background: red; }
div:has(.lang :lang(zh)) { background: red; }
div:has(.lang :lang(en)) { background: green; }
.lang:lang(kr) { background: red; }
.lang div:lang(kr) { background: green; }
</style>
<script>
requestAnimationFrame(() => {
setTimeout(() => {
fr.lang = 'en';
ja.lang = 'en';
zh.lang = 'en';
kr.appendChild(document.createElement('div'));
document.documentElement.className = '';
}, 0);
});
</script>
</body>
</html>
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<div style="width: 100px; height: 100px; background-color: green;"></div>
</body>
</html>
@@ -0,0 +1,25 @@
<html lang="fr" class="reftest-wait">
<div><span></span></div>
<script>

const test = document.querySelector('div').matches('*:has(:lang(fr))') ? 'PASS' : 'FAIL';

const newDocumentElement = document.createElement('html');
newDocumentElement.lang = 'en';
document.replaceChild(newDocumentElement, document.documentElement);
document.documentElement.innerHTML = `
<head>
<style>
div { width: 100px; height: 100px; }
div:has(*:lang(fr)) { background: red; }
div:has(*:lang(en)) { background: green; }
span.pass { color: green; }
span.fail { color: red; }
</style>
</head>
<body>
<div><span class="${test}">${test}</span></div>
</body>`;

</script>
</html>
@@ -204,7 +204,7 @@ ALWAYS_INLINE bool matchesLangPseudoClass(const Element& element, const Vector<A
language = downcast<WebVTTElement>(element).language();
else
#endif
language = element.computeInheritedLanguage();
language = element.effectiveLang();

if (language.isEmpty())
return false;
@@ -980,6 +980,7 @@ void Document::childrenChanged(const ChildChange& change)
if (newDocumentElement == m_documentElement)
return;
m_documentElement = newDocumentElement;
setDocumentElementLanguage(m_documentElement ? m_documentElement->langFromAttribute() : nullAtom());
// The root style used for media query matching depends on the document element.
styleScope().clearResolver();
}
@@ -1507,6 +1508,26 @@ void Document::setContentLanguage(const AtomString& language)
m_styleScope->didChangeStyleSheetEnvironment();
}

const AtomString& Document::effectiveDocumentElementLanguage() const
{
if (!m_documentElementLanguage.isNull())
return m_documentElementLanguage;
return m_contentLanguage;
}

void Document::setDocumentElementLanguage(const AtomString& language)
{
if (m_documentElementLanguage == language)
return;
m_documentElementLanguage = language;

if (m_contentLanguage == language)
return;

// Recalculate style so language is used when selecting the initial font.
m_styleScope->didChangeStyleSheetEnvironment();
}

ExceptionOr<void> Document::setXMLVersion(const String& version)
{
if (!XMLDocumentParser::supportsXMLVersion(version))
@@ -473,6 +473,9 @@ class Document
const AtomString& contentLanguage() const { return m_contentLanguage; }
void setContentLanguage(const AtomString&);

const AtomString& effectiveDocumentElementLanguage() const;
void setDocumentElementLanguage(const AtomString&);

String xmlEncoding() const { return m_xmlEncoding; }
String xmlVersion() const { return m_xmlVersion; }
enum class StandaloneStatus : uint8_t { Unspecified, Standalone, NotStandalone };
@@ -1928,6 +1931,7 @@ class Document
bool m_hasXMLDeclaration { false };

AtomString m_contentLanguage;
AtomString m_documentElementLanguage;

RefPtr<TextResourceDecoder> m_decoder;

@@ -1986,6 +1986,32 @@ void Element::attributeChanged(const QualifiedName& name, const AtomString& oldV
shadowRoot->invalidatePartMappings();
Style::Invalidator::invalidateShadowParts(*shadowRoot);
}
} else if (name == HTMLNames::langAttr || name.matches(XMLNames::langAttr)) {
if (document().documentElement() == this)
document().setDocumentElementLanguage(newValue);
else {
Style::PseudoClassChangeInvalidation styleInvalidation(*this, CSSSelector::PseudoClassLang, Style::PseudoClassChangeInvalidation::AnyValue);
AtomString newValue = langFromAttribute();
auto setEffectiveLang = [&](Element& element) {
if (!newValue.isNull())
element.ensureElementRareData().setEffectiveLang(newValue);
else if (hasRareData())
element.elementRareData()->setEffectiveLang(nullAtom());
};
setEffectiveLang(*this);
for (auto it = descendantsOfType<Element>(*this).begin(); it;) {
auto& element = *it;
if (auto* elementData = element.elementData()) {
if (auto* attribute = elementData->findLanguageAttribute()) {
it.traverseNextSkippingChildren();
continue;
}
}
Style::PseudoClassChangeInvalidation styleInvalidation(element, CSSSelector::PseudoClassLang, Style::PseudoClassChangeInvalidation::AnyValue);
setEffectiveLang(element);
it.traverseNext();
}
}
}
}

@@ -2509,6 +2535,20 @@ Node::InsertedIntoAncestorResult Element::insertedIntoAncestor(InsertionType ins
CustomElementReactionQueue::enqueueConnectedCallbackIfNeeded(*this);
}

[&]() {
if (auto* parent = parentOrShadowHostElement(); parent && parent != document().documentElement() && UNLIKELY(parent->hasRareData())) {
auto lang = parent->elementRareData()->effectiveLang();
if (!lang.isNull() && langFromAttribute().isNull()) {
ensureElementRareData().setEffectiveLang(lang);
return;
}
}
if (UNLIKELY(hasRareData())) {
if (!elementRareData()->effectiveLang().isNull() && langFromAttribute().isNull())
ensureElementRareData().setEffectiveLang(nullAtom());
}
}();

if (shouldAutofocus(*this))
document().topDocument().appendAutofocusCandidate(*this);

@@ -2573,6 +2613,11 @@ void Element::removedFromAncestor(RemovalType removalType, ContainerNode& oldPar

ContainerNode::removedFromAncestor(removalType, oldParentOfRemovedTree);

if (UNLIKELY(hasRareData()) && !elementRareData()->effectiveLang().isNull()) {
if (langFromAttribute().isNull())
elementRareData()->setEffectiveLang(nullAtom());
}

Styleable::fromElement(*this).elementWasRemoved();

#if ENABLE(WHEEL_EVENT_LATCHING)
@@ -3836,21 +3881,28 @@ unsigned Element::rareDataChildIndex() const
return elementRareData()->childIndex();
}

AtomString Element::computeInheritedLanguage() const
AtomString Element::effectiveLang() const
{
// The language property is inherited, so we iterate over the parents to find the first language.
for (auto* element = this; element; element = element->parentOrShadowHostElement()) {
if (auto* elementData = element->elementData()) {
if (auto* attribute = elementData->findLanguageAttribute())
return attribute->value();
}
if (hasRareData()) {
auto lang = elementRareData()->effectiveLang();
if (!lang.isNull())
return lang;
}
return isConnected() ? document().effectiveDocumentElementLanguage() : nullAtom();
}

AtomString Element::langFromAttribute() const
{
if (auto* data = elementData()) {
if (auto* attribute = data->findLanguageAttribute())
return attribute->value();
}
return document().contentLanguage();
return nullAtom();
}

Locale& Element::locale() const
{
return document().getCachedLocale(computeInheritedLanguage());
return document().getCachedLocale(effectiveLang());
}

void Element::normalizeAttributes()
@@ -419,7 +419,8 @@ class Element : public ContainerNode {
void setStyleIsAffectedByPreviousSibling() { setStyleFlag(NodeStyleFlag::StyleIsAffectedByPreviousSibling); }
void setChildIndex(unsigned);

WEBCORE_EXPORT AtomString computeInheritedLanguage() const;
AtomString effectiveLang() const;
AtomString langFromAttribute() const;
Locale& locale() const;

virtual bool accessKeyAction(bool /*sendToAnyEvent*/) { return false; }
@@ -36,7 +36,7 @@ namespace WebCore {
struct SameSizeAsElementRareData : NodeRareData {
IntPoint savedLayerScrollPosition;
Vector<std::unique_ptr<ElementAnimationRareData>> animationRareData;
void* pointers[11];
void* pointers[12];
void* intersectionObserverData;
#if ENABLE(CSS_TYPED_OM)
void* typedOMData[2];
@@ -72,6 +72,9 @@ class ElementRareData : public NodeRareData {
RenderStyle* computedStyle() const { return m_computedStyle.get(); }
void setComputedStyle(std::unique_ptr<RenderStyle> computedStyle) { m_computedStyle = WTFMove(computedStyle); }

const AtomString& effectiveLang() const { return m_effectiveLang; }
void setEffectiveLang(const AtomString& lang) { m_effectiveLang = lang; }

DOMTokenList* classList() const { return m_classList.get(); }
void setClassList(std::unique_ptr<DOMTokenList> classList) { m_classList = WTFMove(classList); }

@@ -119,10 +122,12 @@ class ElementRareData : public NodeRareData {
result.add(UseType::ScrollingPosition);
if (m_computedStyle)
result.add(UseType::ComputedStyle);
if (m_dataset)
result.add(UseType::Dataset);
if (m_effectiveLang)
result.add(UseType::LangEffective);
if (m_classList)
result.add(UseType::ClassList);
if (m_dataset)
result.add(UseType::Dataset);
if (m_shadowRoot)
result.add(UseType::ShadowRoot);
if (m_customElementReactionQueue)
@@ -159,6 +164,7 @@ class ElementRareData : public NodeRareData {
IntPoint m_savedLayerScrollPosition;
std::unique_ptr<RenderStyle> m_computedStyle;

AtomString m_effectiveLang;
std::unique_ptr<DatasetDOMStringMap> m_dataset;
std::unique_ptr<DOMTokenList> m_classList;
RefPtr<ShadowRoot> m_shadowRoot;
@@ -269,6 +269,7 @@ class NodeRareData {
Nonce = 1 << 18,
ComputedStyleMap = 1 << 19,
ExplicitlySetAttrElementsMap = 1 << 20,
EffectiveLang = 1 << 21,
};
#endif

@@ -498,7 +498,7 @@ bool BaseDateAndTimeInputType::isEditControlOwnerReadOnly() const
AtomString BaseDateAndTimeInputType::localeIdentifier() const
{
ASSERT(element());
return element()->computeInheritedLanguage();
return element()->effectiveLang();
}

void BaseDateAndTimeInputType::didChooseValue(StringView value)
@@ -530,7 +530,7 @@ bool BaseDateAndTimeInputType::setupDateTimeChooserParameters(DateTimeChooserPar
if (!document.settings().langAttributeAwareFormControlUIEnabled())
parameters.locale = AtomString { defaultLanguage() };
else {
AtomString computedLocale = element.computeInheritedLanguage();
AtomString computedLocale = element.effectiveLang();
parameters.locale = computedLocale.isEmpty() ? AtomString(defaultLanguage()) : computedLocale;
}

0 comments on commit 0c6665b

Please sign in to comment.