LibWeb: Add MathML Element presentational hints

This commit is contained in:
Lorenz A 2025-09-20 22:54:45 +02:00 committed by Jelle Raaijmakers
parent f1571c4217
commit 96b34ea744
13 changed files with 380 additions and 2 deletions

View File

@ -758,7 +758,10 @@ set(SOURCES
Loader/ProxyMappings.cpp
Loader/Resource.cpp
Loader/ResourceLoader.cpp
MathML/AttributeNames.cpp
MathML/MathMLElement.cpp
MathML/MathMLMiElement.cpp
MathML/MathMLMspaceElement.cpp
MathML/TagNames.cpp
MediaCapabilitiesAPI/MediaCapabilities.cpp
MediaSourceExtensions/BufferedChangeEvent.cpp

View File

@ -83,6 +83,8 @@
#include <LibWeb/HTML/WindowOrWorkerGlobalScope.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/MathML/MathMLElement.h>
#include <LibWeb/MathML/MathMLMiElement.h>
#include <LibWeb/MathML/MathMLMspaceElement.h>
#include <LibWeb/MathML/TagNames.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/SVG/SVGAElement.h>
@ -543,6 +545,12 @@ static GC::Ref<SVG::SVGElement> create_svg_element(JS::Realm& realm, Document& d
static GC::Ref<MathML::MathMLElement> create_mathml_element(JS::Realm& realm, Document& document, QualifiedName qualified_name)
{
auto const& local_name = qualified_name.local_name();
if (local_name == MathML::TagNames::mi)
return realm.create<MathML::MathMLMiElement>(document, move(qualified_name));
if (local_name == MathML::TagNames::mspace)
return realm.create<MathML::MathMLMspaceElement>(document, move(qualified_name));
// https://w3c.github.io/mathml-core/#dom-and-javascript
// All the nodes representing MathML elements in the DOM must implement, and expose to scripts,
// the following MathMLElement interface.

View File

@ -885,6 +885,8 @@ struct LayoutState;
namespace Web::MathML {
class MathMLElement;
class MathMLMiElement;
class MathMLMspaceElement;
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2025, Lorenz Ackermann, <me@lorenzackermann.xyz>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/MathML/AttributeNames.h>
namespace Web::MathML::AttributeNames {
#define __ENUMERATE_MATHML_ATTRIBUTE(name, attribute) \
FlyString name = attribute##_fly_string;
ENUMERATE_MATHML_ATTRIBUTES
#undef __ENUMERATE_MATHML_ATTRIBUTE
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2025, Lorenz Ackermann, <me@lorenzackermann.xyz>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <LibWeb/Export.h>
namespace Web::MathML::AttributeNames {
#define ENUMERATE_MATHML_ATTRIBUTES \
__ENUMERATE_MATHML_ATTRIBUTE(autofocus, "autofocus") \
__ENUMERATE_MATHML_ATTRIBUTE(depth, "depth") \
__ENUMERATE_MATHML_ATTRIBUTE(dir, "dir") \
__ENUMERATE_MATHML_ATTRIBUTE(displaystyle, "displaystyle") \
__ENUMERATE_MATHML_ATTRIBUTE(height, "height") \
__ENUMERATE_MATHML_ATTRIBUTE(mathbackground, "mathbackground") \
__ENUMERATE_MATHML_ATTRIBUTE(mathcolor, "mathcolor") \
__ENUMERATE_MATHML_ATTRIBUTE(mathsize, "mathsize") \
__ENUMERATE_MATHML_ATTRIBUTE(mathvariant, "mathvariant") \
__ENUMERATE_MATHML_ATTRIBUTE(scriptlevel, "scriptlevel") \
__ENUMERATE_MATHML_ATTRIBUTE(width, "width")
#define __ENUMERATE_MATHML_ATTRIBUTE(name, attribute) extern WEB_API FlyString name;
ENUMERATE_MATHML_ATTRIBUTES
#undef __ENUMERATE_MATHML_ATTRIBUTE
}

View File

@ -6,6 +6,13 @@
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/MathMLElementPrototype.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/MathDepthStyleValue.h>
#include <LibWeb/HTML/Numbers.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/MathML/AttributeNames.h>
#include <LibWeb/MathML/MathMLElement.h>
#include <LibWeb/MathML/TagNames.h>
@ -59,4 +66,68 @@ void MathMLElement::visit_edges(JS::Cell::Visitor& visitor)
HTMLOrSVGElement::visit_edges(visitor);
}
bool MathMLElement::is_presentational_hint(FlyString const& name) const
{
return first_is_one_of(name, AttributeNames::dir, AttributeNames::mathcolor, AttributeNames::mathbackground,
AttributeNames::mathsize, AttributeNames::displaystyle, AttributeNames::scriptlevel);
}
void MathMLElement::apply_presentational_hints(GC::Ref<CSS::CascadedProperties> cascaded_properties) const
{
for_each_attribute([&](auto& name, auto& value) {
if (name == AttributeNames::dir) {
// https://w3c.github.io/mathml-core/#attributes-common-to-html-and-mathml-elements
// The dir attribute, if present, must be an ASCII case-insensitive match to ltr or rtl. In that case, the
// user agent is expected to treat the attribute as a presentational hint setting the element's direction
// property to the corresponding value. More precisely, an ASCII case-insensitive match to rtl is mapped to
// rtl while an ASCII case-insensitive match to ltr is mapped to ltr.
if (value.equals_ignoring_ascii_case("ltr"sv))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::Direction, CSS::KeywordStyleValue::create(CSS::Keyword::Ltr));
else if (value.equals_ignoring_ascii_case("rtl"sv))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::Direction, CSS::KeywordStyleValue::create(CSS::Keyword::Rtl));
} else if (name == AttributeNames::mathcolor) {
// https://w3c.github.io/mathml-core/#legacy-mathml-style-attributes
// The mathcolor and mathbackground attributes, if present, must have a value that is a <color>. In that case,
// the user agent is expected to treat these attributes as a presentational hint setting the element's color
// and background-color properties to the corresponding values.
if (auto parsed_value = parse_css_value(CSS::Parser::ParsingParams { document() }, value, CSS::PropertyID::Color))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::Color, parsed_value.release_nonnull());
} else if (name == AttributeNames::mathbackground) {
if (auto parsed_value = parse_css_value(CSS::Parser::ParsingParams { document() }, value, CSS::PropertyID::BackgroundColor))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BackgroundColor, parsed_value.release_nonnull());
} else if (name == AttributeNames::mathsize) {
// https://w3c.github.io/mathml-core/#dfn-mathsize
// The mathsize attribute, if present, must have a value that is a valid <length-percentage>.
// In that case, the user agent is expected to treat the attribute as a presentational hint setting the
// element's font-size property to the corresponding value.
if (auto parsed_value = HTML::parse_dimension_value(value))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::FontSize, parsed_value.release_nonnull());
} else if (name == AttributeNames::displaystyle) {
// https://w3c.github.io/mathml-core/#dfn-displaystyle
// The displaystyle attribute, if present, must have a value that is a boolean. In that case, the user agent
// is expected to treat the attribute as a presentational hint setting the element's math-style property to
// the corresponding value. More precisely, an ASCII case-insensitive match to true is mapped to normal while
// an ASCII case-insensitive match to false is mapped to compact.
if (value.equals_ignoring_ascii_case("true"sv))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::MathStyle, CSS::KeywordStyleValue::create(CSS::Keyword::Normal));
else if (value.equals_ignoring_ascii_case("false"sv))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::MathStyle, CSS::KeywordStyleValue::create(CSS::Keyword::Compact));
} else if (name == AttributeNames::scriptlevel) {
// https://w3c.github.io/mathml-core/#dfn-scriptlevel
// The scriptlevel attribute, if present, must have value +<U>, -<U> or <U> where <U> is an unsigned-integer.
// In that case the user agent is expected to treat the scriptlevel attribute as a presentational hint
// setting the element's math-depth property to the corresponding value. More precisely, +<U>, -<U> and <U>
// are respectively mapped to add(<U>) add(<-U>) and <U>.
if (Optional<StringView> parsed_value = HTML::parse_integer_digits(value); parsed_value.has_value()) {
auto string_value = parsed_value.value();
if (auto value = parsed_value->to_number<i32>(TrimWhitespace::No); value.has_value()) {
auto style_value = string_value[0] == '+' || string_value[0] == '-' ? CSS::MathDepthStyleValue::create_add(CSS::IntegerStyleValue::create(value.release_value()))
: CSS::MathDepthStyleValue::create_integer(CSS::IntegerStyleValue::create(value.release_value()));
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::MathDepth, style_value);
}
}
}
});
}
}

View File

@ -24,14 +24,15 @@ public:
virtual Optional<ARIA::Role> default_role() const override;
protected:
MathMLElement(DOM::Document&, DOM::QualifiedName);
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
virtual WebIDL::ExceptionOr<void> cloned(DOM::Node&, bool) const override;
virtual void inserted() override;
virtual GC::Ptr<DOM::EventTarget> global_event_handlers_to_event_target(FlyString const&) override { return *this; }
virtual bool is_presentational_hint(FlyString const&) const override;
virtual void apply_presentational_hints(GC::Ref<CSS::CascadedProperties>) const override;
private:
MathMLElement(DOM::Document&, DOM::QualifiedName);
virtual void visit_edges(Visitor&) override;
virtual void initialize(JS::Realm&) override;

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2025, Lorenz Ackermann, <me@lorenzackermann.xyz>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/MathML/AttributeNames.h>
#include <LibWeb/MathML/MathMLMiElement.h>
namespace Web::MathML {
GC_DEFINE_ALLOCATOR(MathMLMiElement);
MathMLMiElement::MathMLMiElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: MathMLElement(document, move(qualified_name))
{
}
bool MathMLMiElement::is_presentational_hint(FlyString const& name) const
{
if (Base::is_presentational_hint(name))
return true;
return first_is_one_of(name, AttributeNames::mathvariant);
}
void MathMLMiElement::apply_presentational_hints(GC::Ref<CSS::CascadedProperties> cascaded_properties) const
{
Base::apply_presentational_hints(cascaded_properties);
// https://w3c.github.io/mathml-core/#dfn-mathvariant
// The mathvariant attribute, if present, must be an ASCII case-insensitive match of normal. In that case, the user
// agent is expected to treat the attribute as a presentational hint setting the element's text-transform property
// to none. Otherwise it has no effects.
if (auto mathvariant = attribute(AttributeNames::mathvariant); mathvariant.has_value() && mathvariant.value().equals_ignoring_ascii_case("normal"sv)) {
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::TextTransform, CSS::KeywordStyleValue::create(CSS::Keyword::None));
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2025, Lorenz Ackermann, <me@lorenzackermann.xyz>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/MathML/MathMLElement.h>
namespace Web::MathML {
class MathMLMiElement final : public MathMLElement {
WEB_PLATFORM_OBJECT(MathMLMiElement, MathMLElement);
GC_DECLARE_ALLOCATOR(MathMLMiElement);
public:
virtual ~MathMLMiElement() override = default;
private:
MathMLMiElement(DOM::Document&, DOM::QualifiedName);
virtual bool is_presentational_hint(FlyString const&) const override;
virtual void apply_presentational_hints(GC::Ref<CSS::CascadedProperties>) const override;
};
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2025, Lorenz Ackermann, <me@lorenzackermann.xyz>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/MathML/AttributeNames.h>
#include <LibWeb/MathML/MathMLMspaceElement.h>
namespace Web::MathML {
GC_DEFINE_ALLOCATOR(MathMLMspaceElement);
MathMLMspaceElement::MathMLMspaceElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: MathMLElement(document, move(qualified_name))
{
}
bool MathMLMspaceElement::is_presentational_hint(FlyString const& name) const
{
if (Base::is_presentational_hint(name))
return true;
return first_is_one_of(name, AttributeNames::width, AttributeNames::height, AttributeNames::depth);
}
void MathMLMspaceElement::apply_presentational_hints(GC::Ref<CSS::CascadedProperties> cascaded_properties) const
{
Base::apply_presentational_hints(cascaded_properties);
// https://w3c.github.io/mathml-core/#attribute-mspace-width
// The width, height, depth, if present, must have a value that is a valid <length-percentage>.
auto parse_non_percentage_value = [&](FlyString const& attribute_name) -> RefPtr<CSS::StyleValue const> {
if (auto attribute = this->attribute(attribute_name); attribute.has_value()) {
if (auto value = HTML::parse_dimension_value(attribute.value()); value && !value->is_percentage()) {
return value;
}
}
return nullptr;
};
// If the width attribute is present, valid and not a percentage then that attribute is used as a presentational hint
// setting the element's width property to the corresponding value.
if (auto width_value = parse_non_percentage_value(AttributeNames::width)) {
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::Width, width_value.release_nonnull());
}
// https://w3c.github.io/mathml-core/#attribute-mspace-height
// If the height attribute is absent, invalid or a percentage then the requested line-ascent is 0. Otherwise the
// requested line-ascent is the resolved value of the height attribute, clamping negative values to 0.
auto height_value = parse_non_percentage_value(AttributeNames::height);
// FIXME set the line-ascent
// If both the height and depth attributes are present, valid and not a percentage then they are used as a
// presentational hint setting the element's height property to the concatenation of the
// strings "calc(", the height attribute value, " + ", the depth attribute value, and ")". If only one of these
// attributes is present, valid and not a percentage then it is treated as a presentational hint setting the
// element's height property to the corresponding value.
auto depth_value = parse_non_percentage_value(AttributeNames::depth);
if (height_value && depth_value) {
// FIXME set the presentational hint to calculate height + depth
} else if (height_value) {
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::Height, height_value.release_nonnull());
} else if (depth_value) {
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::Height, depth_value.release_nonnull());
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2025, Lorenz Ackermann, <me@lorenzackermann.xyz>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/MathML/MathMLElement.h>
namespace Web::MathML {
class MathMLMspaceElement final : public MathMLElement {
WEB_PLATFORM_OBJECT(MathMLMspaceElement, MathMLElement);
GC_DECLARE_ALLOCATOR(MathMLMspaceElement);
public:
virtual ~MathMLMspaceElement() override = default;
private:
MathMLMspaceElement(DOM::Document&, DOM::QualifiedName);
virtual bool is_presentational_hint(FlyString const&) const override;
virtual void apply_presentational_hints(GC::Ref<CSS::CascadedProperties>) const override;
};
}

View File

@ -0,0 +1,24 @@
direction: ltr
direction: ltr
direction: rtl
color: red
color: rgb(255, 0, 0)
background-color: blue
background-color: rgb(0, 0, 255)
font-size: 10px
math-style: compact
math-style: normal
math-style: normal
math-style: compact
math-style: compact
math-depth: 1
math-depth: 2
math-depth: 3
math-depth: -2
text-transform: math-auto
text-transform: none
width: 10px
width: auto
height: 20px
height: 30px
height: auto

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<body>
<math data-prop="direction">
<mi></mi>
<mi dir="LTR"></mi>
<mi dir="rtl"></mi>
</math>
<math data-prop="color">
<mi mathcolor="red"></mi>
<mi mathcolor="#f00"></mi>
</math>
<math data-prop="background-color">
<mi mathbackground="blue"></mi>
<mi mathbackground="#0000ff"></mi>
</math>
<math data-prop="font-size">
<mi mathsize="10px"></mi>
</math>
<math data-prop="math-style">
<mi displaystyle=""></mi>
<mi displaystyle="true"></mi>
<mi displaystyle="TRuE"></mi>
<mi displaystyle="false"></mi>
<mi displaystyle="faLse"></mi>
</math>
<math data-prop="math-depth" style="math-depth:1">
<mi></mi>
<mi scriptlevel="2"></mi>
<mi scriptlevel="+2"></mi>
<mi scriptlevel="-3"></mi>
</math>
<math data-prop="text-transform">
<mi></mi>
<mi mathvariant="normal"></mi>
</math>
<math data-prop="width">
<mspace width="10px"></mspace>
<mspace width="10%"></mspace>
</math>
<math data-prop="height">
<mspace height="20px"></mspace>
<mspace depth="30px"></mspace>
<mspace height="10%"></mspace>
</math>
</body>
<script>
test(() => {
[...document.querySelectorAll('[data-prop]')].forEach(propItem => {
const prop = propItem.dataset['prop'];
[...propItem.querySelectorAll('& > *')].forEach(item =>
println(`${prop}: ${item.computedStyleMap().get(prop).toString()}`)
);
});
});
</script>