LibWeb/CSS: Parse <url> as a new CSS::URL type

Our previous approach to `<url>` had a couple of issues:
- We'd complete the URL during parsing, when we should actually keep it
  as the original string until it's used.
- There's nowhere for us to store `<url-modifier>`s on a `URL::URL`.

So, `CSS::URL` is a solution to this. It holds the original URL string,
and later will also hold any modifiers. This commit parses all `<url>`s
as `CSS::URL`, but then converts it into a `URL::URL`, so no user code
is changed. These will be modified in subsequent commits.

For `@namespace`, we were never supposed to complete the URL at all, so
this makes that more correct already. However, in practice all
`@namespace`s are absolute URLs already, so this should have no
observable effects.
This commit is contained in:
Sam Atkins 2025-04-08 14:33:01 +01:00
parent 3b45ca3a76
commit b91feeee4f
7 changed files with 101 additions and 27 deletions

View file

@ -195,6 +195,7 @@ set(SOURCES
CSS/Time.cpp
CSS/Transformation.cpp
CSS/TransitionEvent.cpp
CSS/URL.cpp
CSS/VisualViewport.cpp
Cookie/Cookie.cpp
Cookie/ParsedCookie.cpp

View file

@ -32,6 +32,7 @@
#include <LibWeb/CSS/StyleValues/BasicShapeStyleValue.h>
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
#include <LibWeb/CSS/Supports.h>
#include <LibWeb/CSS/URL.h>
#include <LibWeb/Forward.h>
namespace Web::CSS::Parser {
@ -276,7 +277,7 @@ private:
Optional<GridRepeat> parse_repeat(Vector<ComponentValue> const&);
Optional<ExplicitGridTrack> parse_track_sizing_function(ComponentValue const&);
Optional<::URL::URL> parse_url_function(TokenStream<ComponentValue>&);
Optional<URL> parse_url_function(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_url_value(TokenStream<ComponentValue>&);
Optional<ShapeRadius> parse_shape_radius(TokenStream<ComponentValue>&);

View file

@ -153,15 +153,22 @@ GC::Ptr<CSSImportRule> Parser::convert_to_import_rule(AtRule const& rule)
TokenStream tokens { rule.prelude };
tokens.discard_whitespace();
Optional<::URL::URL> url = parse_url_function(tokens);
Optional<URL> url = parse_url_function(tokens);
if (!url.has_value() && tokens.next_token().is(Token::Type::String))
url = complete_url(tokens.consume_a_token().token().string());
url = URL { tokens.consume_a_token().token().string().to_string() };
if (!url.has_value()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
return {};
}
// FIXME: Stop completing the URL here
auto resolved_url = complete_url(url->url());
if (!resolved_url.has_value()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to complete `{}` as URL.", url->url());
return {};
}
tokens.discard_whitespace();
// FIXME: Implement layer support.
RefPtr<Supports> supports {};
@ -191,7 +198,7 @@ GC::Ptr<CSSImportRule> Parser::convert_to_import_rule(AtRule const& rule)
return {};
}
return CSSImportRule::create(url.value(), const_cast<DOM::Document&>(*document()), supports, move(media_query_list));
return CSSImportRule::create(resolved_url.release_value(), const_cast<DOM::Document&>(*document()), supports, move(media_query_list));
}
Optional<FlyString> Parser::parse_layer_name(TokenStream<ComponentValue>& tokens, AllowBlankLayerName allow_blank_layer_name)
@ -435,7 +442,10 @@ GC::Ptr<CSSNamespaceRule> Parser::convert_to_namespace_rule(AtRule const& rule)
FlyString namespace_uri;
if (auto url = parse_url_function(tokens); url.has_value()) {
namespace_uri = url.value().to_string();
// "A URI string parsed from the URI syntax must be treated as a literal string: as with the STRING syntax, no
// URI-specific normalization is applied."
// https://drafts.csswg.org/css-namespaces/#syntax
namespace_uri = url->url();
} else if (auto& url_token = tokens.consume_a_token(); url_token.is(Token::Type::String)) {
namespace_uri = url_token.token().string();
} else {

View file

@ -15,7 +15,6 @@
#include <AK/Debug.h>
#include <AK/GenericLexer.h>
#include <AK/TemporaryChange.h>
#include <LibURL/URL.h>
#include <LibWeb/CSS/FontFace.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/PropertyName.h>
@ -2014,9 +2013,13 @@ RefPtr<AbstractImageStyleValue> Parser::parse_image_value(TokenStream<ComponentV
if (url.has_value()) {
// If the value is a 'url(..)' parse as image, but if it is just a reference 'url(#xx)', leave it alone,
// so we can parse as URL further on. These URLs are used as references inside SVG documents for masks.
if (!url.value().equals(m_url, ::URL::ExcludeFragment::Yes)) {
tokens.discard_a_mark();
return ImageStyleValue::create(url.value());
if (!url->url().starts_with('#')) {
// FIXME: Stop completing the URL here
auto completed_url = complete_url(url->url());
if (completed_url.has_value()) {
tokens.discard_a_mark();
return ImageStyleValue::create(completed_url.release_value());
}
}
tokens.restore_a_mark();
return nullptr;
@ -2562,24 +2565,16 @@ RefPtr<CSSStyleValue> Parser::parse_easing_value(TokenStream<ComponentValue>& to
return nullptr;
}
Optional<::URL::URL> Parser::parse_url_function(TokenStream<ComponentValue>& tokens)
Optional<URL> Parser::parse_url_function(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
auto& component_value = tokens.consume_a_token();
auto convert_string_to_url = [&](StringView url_string) -> Optional<::URL::URL> {
auto url = complete_url(url_string);
if (url.has_value()) {
transaction.commit();
return url;
}
return {};
};
auto const& component_value = tokens.consume_a_token();
if (component_value.is(Token::Type::Url)) {
auto url_string = component_value.token().url();
return convert_string_to_url(url_string);
transaction.commit();
return URL { component_value.token().url().to_string() };
}
if (component_value.is_function("url"sv)) {
auto const& function_values = component_value.function().value;
// FIXME: Handle url-modifiers. https://www.w3.org/TR/css-values-4/#url-modifiers
@ -2588,8 +2583,8 @@ Optional<::URL::URL> Parser::parse_url_function(TokenStream<ComponentValue>& tok
if (value.is(Token::Type::Whitespace))
continue;
if (value.is(Token::Type::String)) {
auto url_string = value.token().string();
return convert_string_to_url(url_string);
transaction.commit();
return URL { value.token().string().to_string() };
}
break;
}
@ -2603,7 +2598,11 @@ RefPtr<CSSStyleValue> Parser::parse_url_value(TokenStream<ComponentValue>& token
auto url = parse_url_function(tokens);
if (!url.has_value())
return nullptr;
return URLStyleValue::create(*url);
// FIXME: Stop completing the URL here
auto completed_url = complete_url(url->url());
if (!completed_url.has_value())
return nullptr;
return URLStyleValue::create(completed_url.release_value());
}
// https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius
@ -3681,7 +3680,11 @@ RefPtr<FontSourceStyleValue> Parser::parse_font_source_value(TokenStream<Compone
// <url> [ format(<font-format>)]? [ tech( <font-tech>#)]?
auto url = parse_url_function(tokens);
if (!url.has_value() || !url->is_valid())
if (!url.has_value())
return nullptr;
// FIXME: Stop completing the URL here
auto completed_url = complete_url(url->url());
if (!completed_url.has_value())
return nullptr;
Optional<FlyString> format;
@ -3719,7 +3722,7 @@ RefPtr<FontSourceStyleValue> Parser::parse_font_source_value(TokenStream<Compone
// FIXME: [ tech( <font-tech>#)]?
transaction.commit();
return FontSourceStyleValue::create(url.release_value(), move(format));
return FontSourceStyleValue::create(completed_url.release_value(), move(format));
}
NonnullRefPtr<CSSStyleValue> Parser::resolve_unresolved_style_value(ParsingParams const& context, DOM::Element& element, Optional<PseudoElement> pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved)

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/URL.h>
namespace Web::CSS {
URL::URL(String url)
: m_url(move(url))
{
}
// https://drafts.csswg.org/cssom-1/#serialize-a-url
String URL::to_string() const
{
// To serialize a URL means to create a string represented by "url(", followed by the serialization of the URL as a string, followed by ")".
StringBuilder builder;
builder.append("url("sv);
serialize_a_string(builder, m_url);
builder.append(')');
return builder.to_string_without_validation();
}
bool URL::operator==(URL const&) const = default;
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
namespace Web::CSS {
// https://drafts.csswg.org/css-values-4/#urls
class URL {
public:
URL(String url);
String const& url() const { return m_url; }
String to_string() const;
bool operator==(URL const&) const;
private:
String m_url;
};
}

View file

@ -269,6 +269,7 @@ class TransformationStyleValue;
class TransitionStyleValue;
class UnicodeRangeStyleValue;
class UnresolvedStyleValue;
class URL;
class URLStyleValue;
class VisualViewport;