blob: 372bdfe4e348a970f2b6d4dfcc2c242c9f3cf85a [file] [log] [blame]
/*
* Copyright (C) 2025 Samuel Weinig <[email protected]>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "CSSStyleProperties.h"
#include "CSSPropertyNames.h"
#include "CSSPropertyParser.h"
#include "CSSRule.h"
#include "CSSSerializationContext.h"
#include "CSSStyleSheet.h"
#include "DeprecatedCSSOMValue.h"
#include "Document.h"
#include "HTMLNames.h"
#include "InspectorInstrumentation.h"
#include "JSDOMGlobalObject.h"
#include "JSDOMWindowBase.h"
#include "LocalDOMWindow.h"
#include "MutableStyleProperties.h"
#include "Quirks.h"
#include "Settings.h"
#include "StyleAttributeMutationScope.h"
#include "StyleProperties.h"
#include "StyleSheetContents.h"
#include "StyledElement.h"
#include <wtf/StdLibExtras.h>
#include <wtf/TZoneMallocInlines.h>
#include <wtf/text/ParsingUtilities.h>
#include <wtf/text/StringParsingBuffer.h>
namespace WebCore {
WTF_MAKE_TZONE_ALLOCATED_IMPL(CSSStyleProperties);
WTF_MAKE_TZONE_ALLOCATED_IMPL(PropertySetCSSStyleProperties);
WTF_MAKE_TZONE_ALLOCATED_IMPL(StyleRuleCSSStyleProperties);
WTF_MAKE_TZONE_ALLOCATED_IMPL(InlineCSSStyleProperties);
namespace {
enum class PropertyNamePrefix { None, Epub, WebKit };
static inline bool matchesCSSPropertyNamePrefix(const StringImpl& propertyName, ASCIILiteral prefix)
{
ASSERT(toASCIILower(propertyName[0]) == prefix[0zu]);
const size_t offset = 1;
#ifndef NDEBUG
for (auto character : prefix.span8())
ASSERT(isASCIILower(character));
ASSERT(propertyName.length());
#endif
// The prefix within the property name must be followed by a capital letter.
// Other characters in the prefix within the property name must be lowercase.
if (propertyName.length() < prefix.length() + 1)
return false;
for (size_t i = offset; i < prefix.length(); ++i) {
if (propertyName[i] != prefix[i])
return false;
}
if (!isASCIIUpper(propertyName[prefix.length()]))
return false;
return true;
}
static PropertyNamePrefix propertyNamePrefix(const StringImpl& propertyName)
{
ASSERT(propertyName.length());
// First character of the prefix within the property name may be upper or lowercase.
char16_t firstChar = toASCIILower(propertyName[0]);
switch (firstChar) {
case 'e':
if (matchesCSSPropertyNamePrefix(propertyName, "epub"_s))
return PropertyNamePrefix::Epub;
break;
case 'w':
if (matchesCSSPropertyNamePrefix(propertyName, "webkit"_s))
return PropertyNamePrefix::WebKit;
break;
default:
break;
}
return PropertyNamePrefix::None;
}
static inline void writeWebKitPrefix(std::span<char>& buffer)
{
memcpySpan(consumeSpan(buffer, 8), "-webkit-"_span);
}
static inline void writeEpubPrefix(std::span<char>& buffer)
{
memcpySpan(consumeSpan(buffer, 6), "-epub-"_span);
}
static CSSPropertyID parseJavaScriptCSSPropertyName(const AtomString& propertyName)
{
using CSSPropertyIDMap = HashMap<AtomString, CSSPropertyID>;
static NeverDestroyed<CSSPropertyIDMap> propertyIDCache;
auto* propertyNameString = propertyName.impl();
if (!propertyNameString)
return CSSPropertyInvalid;
unsigned length = propertyNameString->length();
if (!length)
return CSSPropertyInvalid;
if (auto id = propertyIDCache.get().get(propertyName))
return id;
constexpr size_t bufferSize = maxCSSPropertyNameLength;
std::array<char, bufferSize> buffer;
std::span<char> bufferSpan { buffer };
const char* name = buffer.data();
unsigned i = 0;
switch (propertyNamePrefix(*propertyNameString)) {
case PropertyNamePrefix::None:
if (isASCIIUpper((*propertyNameString)[0]))
return CSSPropertyInvalid;
break;
case PropertyNamePrefix::Epub:
writeEpubPrefix(bufferSpan);
i += 4;
break;
case PropertyNamePrefix::WebKit:
writeWebKitPrefix(bufferSpan);
i += 6;
break;
}
consume(bufferSpan) = toASCIILower((*propertyNameString)[i++]);
char* stringEnd = std::to_address(std::span { buffer }.first(buffer.size() - 1).end());
size_t bufferSizeLeft = stringEnd - bufferSpan.data();
size_t propertySizeLeft = length - i;
if (propertySizeLeft > bufferSizeLeft)
return CSSPropertyInvalid;
for (; i < length; ++i) {
char16_t c = (*propertyNameString)[i];
if (!c || !isASCII(c))
return CSSPropertyInvalid; // illegal character
if (isASCIIUpper(c)) {
size_t bufferSizeLeft = stringEnd - bufferSpan.data();
size_t propertySizeLeft = length - i + 1;
if (propertySizeLeft > bufferSizeLeft)
return CSSPropertyInvalid;
bufferSpan[0] = '-';
bufferSpan[1] = toASCIILowerUnchecked(c);
skip(bufferSpan, 2);
} else
consume(bufferSpan) = c;
ASSERT(!bufferSpan.empty());
}
ASSERT(!bufferSpan.empty());
unsigned outputLength = bufferSpan.data() - buffer.data();
auto id = findCSSProperty(name, outputLength);
// FIXME: Why aren't we memoizing CSS property names we fail to find?
if (id != CSSPropertyInvalid)
propertyIDCache.get().add(propertyName, id);
return id;
}
}
CSSPropertyID CSSStyleProperties::getCSSPropertyIDFromJavaScriptPropertyName(const AtomString& propertyName)
{
// FIXME: This exposes properties disabled by settings. Pass result of CSSStyleProperties::settings instead of null?
Settings* settings = nullptr;
auto property = parseJavaScriptCSSPropertyName(propertyName);
return isExposed(property, settings) ? property : CSSPropertyInvalid;
}
enum class CSSPropertyLookupMode { ConvertUsingDashPrefix, ConvertUsingNoDashPrefix, NoConversion };
template<CSSPropertyLookupMode mode> static CSSPropertyID lookupCSSPropertyFromIDLAttribute(const AtomString& attribute)
{
static NeverDestroyed<HashMap<AtomString, CSSPropertyID>> cache;
if (auto id = cache.get().get(attribute))
return id;
std::array<char, maxCSSPropertyNameLength> outputBuffer;
size_t outputIndex = 0;
if constexpr (mode == CSSPropertyLookupMode::ConvertUsingDashPrefix || mode == CSSPropertyLookupMode::ConvertUsingNoDashPrefix) {
// Conversion is implementing the "IDL attribute to CSS property algorithm"
// from https://drafts.csswg.org/cssom/#idl-attribute-to-css-property.
if constexpr (mode == CSSPropertyLookupMode::ConvertUsingDashPrefix)
outputBuffer[outputIndex++] = '-';
readCharactersForParsing(attribute, [&](auto buffer) {
while (buffer.hasCharactersRemaining()) {
auto c = *buffer++;
ASSERT_WITH_MESSAGE(isASCII(c), "Invalid property name: %s", attribute.string().utf8().data());
if (isASCIIUpper(c)) {
outputBuffer[outputIndex++] = '-';
outputBuffer[outputIndex++] = toASCIILowerUnchecked(c);
} else
outputBuffer[outputIndex++] = c;
}
});
} else {
readCharactersForParsing(attribute, [&](auto buffer) {
while (buffer.hasCharactersRemaining()) {
auto c = *buffer++;
ASSERT_WITH_MESSAGE(c == '-' || isASCIILower(c), "Invalid property name: %s", attribute.string().utf8().data());
outputBuffer[outputIndex++] = c;
}
});
}
auto id = findCSSProperty(outputBuffer.data(), outputIndex);
ASSERT_WITH_MESSAGE(id != CSSPropertyInvalid, "Invalid property name: %s", attribute.string().utf8().data());
cache.get().add(attribute, id);
return id;
}
String CSSStyleProperties::propertyValueForCamelCasedIDLAttribute(const AtomString& attribute)
{
auto propertyID = lookupCSSPropertyFromIDLAttribute<CSSPropertyLookupMode::ConvertUsingNoDashPrefix>(attribute);
ASSERT_WITH_MESSAGE(propertyID != CSSPropertyInvalid, "Invalid attribute: %s", attribute.string().utf8().data());
return getPropertyValueInternal(propertyID);
}
ExceptionOr<void> CSSStyleProperties::setPropertyValueForCamelCasedIDLAttribute(const AtomString& attribute, const String& value)
{
auto propertyID = lookupCSSPropertyFromIDLAttribute<CSSPropertyLookupMode::ConvertUsingNoDashPrefix>(attribute);
ASSERT_WITH_MESSAGE(propertyID != CSSPropertyInvalid, "Invalid attribute: %s", attribute.string().utf8().data());
return setPropertyInternal(propertyID, value, IsImportant::No);
}
String CSSStyleProperties::propertyValueForWebKitCasedIDLAttribute(const AtomString& attribute)
{
auto propertyID = lookupCSSPropertyFromIDLAttribute<CSSPropertyLookupMode::ConvertUsingDashPrefix>(attribute);
ASSERT_WITH_MESSAGE(propertyID != CSSPropertyInvalid, "Invalid attribute: %s", attribute.string().utf8().data());
return getPropertyValueInternal(propertyID);
}
ExceptionOr<void> CSSStyleProperties::setPropertyValueForWebKitCasedIDLAttribute(const AtomString& attribute, const String& value)
{
auto propertyID = lookupCSSPropertyFromIDLAttribute<CSSPropertyLookupMode::ConvertUsingDashPrefix>(attribute);
ASSERT_WITH_MESSAGE(propertyID != CSSPropertyInvalid, "Invalid attribute: %s", attribute.string().utf8().data());
return setPropertyInternal(propertyID, value, IsImportant::No);
}
String CSSStyleProperties::propertyValueForDashedIDLAttribute(const AtomString& attribute)
{
auto propertyID = lookupCSSPropertyFromIDLAttribute<CSSPropertyLookupMode::NoConversion>(attribute);
ASSERT_WITH_MESSAGE(propertyID != CSSPropertyInvalid, "Invalid attribute: %s", attribute.string().utf8().data());
return getPropertyValueInternal(propertyID);
}
ExceptionOr<void> CSSStyleProperties::setPropertyValueForDashedIDLAttribute(const AtomString& attribute, const String& value)
{
auto propertyID = lookupCSSPropertyFromIDLAttribute<CSSPropertyLookupMode::NoConversion>(attribute);
ASSERT_WITH_MESSAGE(propertyID != CSSPropertyInvalid, "Invalid attribute: %s", attribute.string().utf8().data());
return setPropertyInternal(propertyID, value, IsImportant::No);
}
String CSSStyleProperties::propertyValueForEpubCasedIDLAttribute(const AtomString& attribute)
{
auto propertyID = lookupCSSPropertyFromIDLAttribute<CSSPropertyLookupMode::ConvertUsingDashPrefix>(attribute);
ASSERT_WITH_MESSAGE(propertyID != CSSPropertyInvalid, "Invalid attribute: %s", attribute.string().utf8().data());
return getPropertyValueInternal(propertyID);
}
ExceptionOr<void> CSSStyleProperties::setPropertyValueForEpubCasedIDLAttribute(const AtomString& attribute, const String& value)
{
auto propertyID = lookupCSSPropertyFromIDLAttribute<CSSPropertyLookupMode::ConvertUsingDashPrefix>(attribute);
ASSERT_WITH_MESSAGE(propertyID != CSSPropertyInvalid, "Invalid attribute: %s", attribute.string().utf8().data());
return setPropertyInternal(propertyID, value, IsImportant::No);
}
String CSSStyleProperties::cssFloat()
{
return getPropertyValueInternal(CSSPropertyFloat);
}
ExceptionOr<void> CSSStyleProperties::setCssFloat(const String& value)
{
return setPropertyInternal(CSSPropertyFloat, value, IsImportant::No);
}
// MARK: - PropertySetCSSStyleProperties
void PropertySetCSSStyleProperties::ref() const
{
m_propertySet->ref();
}
void PropertySetCSSStyleProperties::deref() const
{
m_propertySet->deref();
}
unsigned PropertySetCSSStyleProperties::length() const
{
unsigned exposed = 0;
for (auto property : *m_propertySet) {
if (isExposed(property.id()))
exposed++;
}
return exposed;
}
String PropertySetCSSStyleProperties::item(unsigned i) const
{
for (unsigned j = 0; j <= i && j < m_propertySet->propertyCount(); j++) {
if (!isExposed(m_propertySet->propertyAt(j).id()))
i++;
}
if (i >= m_propertySet->propertyCount())
return String();
return m_propertySet->propertyAt(i).cssName();
}
String PropertySetCSSStyleProperties::cssText() const
{
return m_propertySet->asText(CSS::defaultSerializationContext());
}
ExceptionOr<void> PropertySetCSSStyleProperties::setCssText(const String& text)
{
StyleAttributeMutationScope mutationScope { parentElement() };
if (!willMutate())
return { };
bool changed = m_propertySet->parseDeclaration(text, cssParserContext());
didMutate(changed ? MutationType::PropertyChanged : MutationType::StyleAttributeChanged);
mutationScope.enqueueMutationRecord();
return { };
}
RefPtr<DeprecatedCSSOMValue> PropertySetCSSStyleProperties::getPropertyCSSValue(const String& propertyName)
{
if (isCustomPropertyName(propertyName)) {
RefPtr<CSSValue> value = m_propertySet->getCustomPropertyCSSValue(propertyName);
if (!value)
return nullptr;
return wrapForDeprecatedCSSOM(value.get());
}
CSSPropertyID propertyID = cssPropertyID(propertyName);
if (!isExposed(propertyID))
return nullptr;
return wrapForDeprecatedCSSOM(m_propertySet->getPropertyCSSValue(propertyID).get());
}
String PropertySetCSSStyleProperties::getPropertyValue(const String& propertyName)
{
if (isCustomPropertyName(propertyName))
return m_propertySet->getCustomPropertyValue(propertyName);
CSSPropertyID propertyID = cssPropertyID(propertyName);
if (!isExposed(propertyID))
return String();
return getPropertyValueInternal(propertyID);
}
String PropertySetCSSStyleProperties::getPropertyPriority(const String& propertyName)
{
if (isCustomPropertyName(propertyName))
return m_propertySet->customPropertyIsImportant(propertyName) ? "important"_s : emptyString();
CSSPropertyID propertyID = cssPropertyID(propertyName);
if (!isExposed(propertyID))
return emptyString();
return m_propertySet->propertyIsImportant(propertyID) ? "important"_s : emptyString();
}
String PropertySetCSSStyleProperties::getPropertyShorthand(const String& propertyName)
{
CSSPropertyID propertyID = cssPropertyID(propertyName);
if (!isExposed(propertyID))
return String();
return m_propertySet->getPropertyShorthand(propertyID);
}
bool PropertySetCSSStyleProperties::isPropertyImplicit(const String& propertyName)
{
return m_propertySet->isPropertyImplicit(cssPropertyID(propertyName));
}
ExceptionOr<void> PropertySetCSSStyleProperties::setProperty(const String& propertyName, const String& value, const String& priority)
{
StyleAttributeMutationScope mutationScope { parentElement() };
CSSPropertyID propertyID = cssPropertyID(propertyName);
if (isCustomPropertyName(propertyName))
propertyID = CSSPropertyCustom;
if (!isExposed(propertyID))
return { };
if (!willMutate())
return { };
bool important = equalLettersIgnoringASCIICase(priority, "important"_s);
if (!important && !priority.isEmpty())
return { };
bool changed;
if (propertyID == CSSPropertyCustom) [[unlikely]]
changed = m_propertySet->setCustomProperty(propertyName, value, cssParserContext(), important ? IsImportant::Yes : IsImportant::No);
else
changed = m_propertySet->setProperty(propertyID, value, cssParserContext(), important ? IsImportant::Yes : IsImportant::No);
didMutate(changed ? MutationType::PropertyChanged : MutationType::NoChanges);
if (changed) {
// CSS DOM requires raising SyntaxError of parsing failed, but this is too dangerous for compatibility,
// see <http://bugs.webkit.org/show_bug.cgi?id=7296>.
mutationScope.enqueueMutationRecord();
}
return { };
}
ExceptionOr<String> PropertySetCSSStyleProperties::removeProperty(const String& propertyName)
{
StyleAttributeMutationScope mutationScope { parentElement() };
CSSPropertyID propertyID = cssPropertyID(propertyName);
if (isCustomPropertyName(propertyName))
propertyID = CSSPropertyCustom;
if (!isExposed(propertyID))
return String();
if (!willMutate())
return String();
String result;
bool changed = propertyID != CSSPropertyCustom ? m_propertySet->removeProperty(propertyID, &result) : m_propertySet->removeCustomProperty(propertyName, &result);
didMutate(changed ? MutationType::PropertyChanged : MutationType::NoChanges);
if (changed)
mutationScope.enqueueMutationRecord();
return result;
}
String PropertySetCSSStyleProperties::getPropertyValueInternal(CSSPropertyID propertyID)
{
if (!isExposed(propertyID))
return { };
auto value = m_propertySet->getPropertyValue(propertyID);
if (!value.isEmpty())
return value;
return { };
}
ExceptionOr<void> PropertySetCSSStyleProperties::setPropertyInternal(CSSPropertyID propertyID, const String& value, IsImportant important)
{
StyleAttributeMutationScope mutationScope { parentElement() };
if (!willMutate())
return { };
if (!isExposed(propertyID))
return { };
if (m_propertySet->setProperty(propertyID, value, cssParserContext(), important)) {
didMutate(MutationType::PropertyChanged);
mutationScope.enqueueMutationRecord();
} else
didMutate(MutationType::NoChanges);
return { };
}
bool PropertySetCSSStyleProperties::isExposed(CSSPropertyID propertyID) const
{
if (propertyID == CSSPropertyInvalid)
return false;
auto parserContext = cssParserContext();
bool parsingDescriptor = parserContext->enclosingRuleType && *parserContext->enclosingRuleType != StyleRuleType::Style;
return WebCore::isExposed(propertyID, &parserContext->propertySettings)
&& (!CSSProperty::isDescriptorOnly(propertyID) || parsingDescriptor);
}
RefPtr<DeprecatedCSSOMValue> PropertySetCSSStyleProperties::wrapForDeprecatedCSSOM(CSSValue* internalValue)
{
if (!internalValue)
return nullptr;
// The map is here to maintain the object identity of the CSSValues over multiple invocations.
// FIXME: It is likely that the identity is not important for web compatibility and this code should be removed.
auto& clonedValue = m_cssomValueWrappers.add(internalValue, WeakPtr<DeprecatedCSSOMValue>()).iterator->value;
if (clonedValue)
return clonedValue.get();
auto wrapper = internalValue->createDeprecatedCSSOMWrapper(*this);
clonedValue = wrapper;
return wrapper;
}
StyleSheetContents* PropertySetCSSStyleProperties::contextStyleSheet() const
{
RefPtr cssStyleSheet = parentStyleSheet();
return cssStyleSheet ? &cssStyleSheet->contents() : nullptr;
}
OptionalOrReference<CSSParserContext> PropertySetCSSStyleProperties::cssParserContext() const
{
return CSSParserContext(m_propertySet->cssParserMode());
}
Ref<MutableStyleProperties> PropertySetCSSStyleProperties::copyProperties() const
{
return m_propertySet->mutableCopy();
}
// MARK: - StyleRuleCSSStyleProperties
StyleRuleCSSStyleProperties::StyleRuleCSSStyleProperties(MutableStyleProperties& propertySet, CSSRule& parentRule)
: PropertySetCSSStyleProperties(propertySet)
, m_parentRuleType(parentRule.styleRuleType())
, m_parentRule(&parentRule)
{
m_propertySet->ref();
}
StyleRuleCSSStyleProperties::~StyleRuleCSSStyleProperties()
{
m_propertySet->deref();
}
bool StyleRuleCSSStyleProperties::willMutate()
{
if (!m_parentRule || !m_parentRule->parentStyleSheet())
return false;
m_parentRule->parentStyleSheet()->willMutateRules();
return true;
}
void StyleRuleCSSStyleProperties::didMutate(MutationType type)
{
ASSERT(m_parentRule);
ASSERT(m_parentRule->parentStyleSheet());
if (type == MutationType::PropertyChanged)
m_cssomValueWrappers.clear();
// Style sheet mutation needs to be signaled even if the change failed. willMutate*/didMutate* must pair.
m_parentRule->parentStyleSheet()->didMutateRuleFromCSSStyleDeclaration();
}
CSSStyleSheet* StyleRuleCSSStyleProperties::parentStyleSheet() const
{
return m_parentRule ? m_parentRule->parentStyleSheet() : nullptr;
}
CSSRule* StyleRuleCSSStyleProperties::parentRule() const
{
return m_parentRule.get();
}
OptionalOrReference<CSSParserContext> StyleRuleCSSStyleProperties::cssParserContext() const
{
RefPtr styleSheet = contextStyleSheet();
if (!styleSheet)
return PropertySetCSSStyleProperties::cssParserContext();
auto context = styleSheet->parserContext();
context.enclosingRuleType = m_parentRuleType;
return context;
}
void StyleRuleCSSStyleProperties::reattach(MutableStyleProperties& propertySet)
{
m_propertySet->deref();
m_propertySet = &propertySet;
m_propertySet->ref();
}
// MARK: - InlineCSSStyleProperties
bool InlineCSSStyleProperties::willMutate()
{
if (m_parentElement)
InspectorInstrumentation::willInvalidateStyleAttr(*m_parentElement);
return true;
}
void InlineCSSStyleProperties::didMutate(MutationType type)
{
if (type == MutationType::NoChanges)
return;
if (type == MutationType::StyleAttributeChanged && m_parentElement) {
m_parentElement->dirtyStyleAttribute();
return;
}
m_cssomValueWrappers.clear();
if (!m_parentElement)
return;
// Inline style changes from JavaScript (e.g., element.style.color = 'red') need to set
// the mutation bit for innerHTML prefix cache invalidation, since they don't go through
// the normal attribute change notification path.
m_parentElement->setDidMutateSubtreeAfterSetInnerHTMLOnAncestors();
m_parentElement->invalidateStyleAttribute();
InspectorInstrumentation::didInvalidateStyleAttr(*m_parentElement);
}
CSSStyleSheet* InlineCSSStyleProperties::parentStyleSheet() const
{
return nullptr;
}
OptionalOrReference<CSSParserContext> InlineCSSStyleProperties::cssParserContext() const
{
if (!m_parentElement)
return PropertySetCSSStyleProperties::cssParserContext();
auto& documentContext = m_parentElement->document().cssParserContext();
if (documentContext.mode == m_propertySet->cssParserMode())
return documentContext;
CSSParserContext context(documentContext);
context.mode = m_propertySet->cssParserMode();
return context;
}
}