LibWeb/CSS: Parse @page bleed, marks, page-orientation descriptors

These don't have WPT tests so I've added some myself.
This commit is contained in:
Sam Atkins 2025-05-14 16:05:53 +01:00
commit 3a235e9050
Notes: github-actions[bot] 2025-05-15 08:54:25 +00:00
10 changed files with 190 additions and 1 deletions

View file

@ -108,6 +108,13 @@
"spec": "https://drafts.csswg.org/css-page-3/#at-page-rule", "spec": "https://drafts.csswg.org/css-page-3/#at-page-rule",
"FIXME": "There are a lot more properties that are valid, see https://drafts.csswg.org/css-page-3/#properties-list", "FIXME": "There are a lot more properties that are valid, see https://drafts.csswg.org/css-page-3/#properties-list",
"descriptors": { "descriptors": {
"bleed": {
"initial": "auto",
"syntax": [
"auto",
"<length>"
]
},
"margin": { "margin": {
"initial": "0", "initial": "0",
"syntax": [ "syntax": [
@ -138,6 +145,21 @@
"<'margin-top'>" "<'margin-top'>"
] ]
}, },
"marks": {
"initial": "none",
"syntax": [
"none",
"crop || cross"
]
},
"page-orientation": {
"initial": "upright",
"syntax": [
"upright",
"rotate-left",
"rotate-right"
]
},
"size": { "size": {
"initial": "auto", "initial": "auto",
"FIXME": "Replace with actual syntax once we parse grammar properly", "FIXME": "Replace with actual syntax once we parse grammar properly",

View file

@ -141,6 +141,8 @@
"copy", "copy",
"cover", "cover",
"crisp-edges", "crisp-edges",
"crop",
"cross",
"crosshair", "crosshair",
"currentcolor", "currentcolor",
"cursive", "cursive",
@ -391,6 +393,8 @@
"revert-layer", "revert-layer",
"ridge", "ridge",
"right", "right",
"rotate-left",
"rotate-right",
"round", "round",
"row", "row",
"row-resize", "row-resize",
@ -506,6 +510,7 @@
"upper-latin", "upper-latin",
"upper-roman", "upper-roman",
"uppercase", "uppercase",
"upright",
"use-credentials", "use-credentials",
"vertical-lr", "vertical-lr",
"vertical-rl", "vertical-rl",

View file

@ -61,6 +61,36 @@ Parser::ParseErrorOr<NonnullRefPtr<CSSStyleValue const>> Parser::parse_descripto
}, },
[&](DescriptorMetadata::ValueType value_type) -> RefPtr<CSSStyleValue const> { [&](DescriptorMetadata::ValueType value_type) -> RefPtr<CSSStyleValue const> {
switch (value_type) { switch (value_type) {
case DescriptorMetadata::ValueType::CropOrCross: {
// crop || cross
auto first = parse_keyword_value(tokens);
auto second = parse_keyword_value(tokens);
if (!first)
return nullptr;
RefPtr<CSSStyleValue const> crop;
RefPtr<CSSStyleValue const> cross;
if (first->to_keyword() == Keyword::Crop)
crop = first;
else if (first->to_keyword() == Keyword::Cross)
cross = first;
else
return nullptr;
if (!second)
return first.release_nonnull();
if (crop.is_null() && second->to_keyword() == Keyword::Crop)
crop = second.release_nonnull();
else if (cross.is_null() && second->to_keyword() == Keyword::Cross)
cross = second.release_nonnull();
else
return nullptr;
return StyleValueList::create(StyleValueVector { crop.release_nonnull(), cross.release_nonnull() }, StyleValueList::Separator::Space);
}
case DescriptorMetadata::ValueType::FamilyName: case DescriptorMetadata::ValueType::FamilyName:
return parse_family_name_value(tokens); return parse_family_name_value(tokens);
case DescriptorMetadata::ValueType::FontSrcList: { case DescriptorMetadata::ValueType::FontSrcList: {
@ -84,6 +114,8 @@ Parser::ParseErrorOr<NonnullRefPtr<CSSStyleValue const>> Parser::parse_descripto
return nullptr; return nullptr;
return StyleValueList::create(move(valid_sources), StyleValueList::Separator::Comma); return StyleValueList::create(move(valid_sources), StyleValueList::Separator::Comma);
} }
case DescriptorMetadata::ValueType::Length:
return parse_length_value(tokens);
case DescriptorMetadata::ValueType::OptionalDeclarationValue: { case DescriptorMetadata::ValueType::OptionalDeclarationValue: {
// `component_values` already has what we want. Just skip through its tokens so code below knows we consumed them. // `component_values` already has what we want. Just skip through its tokens so code below knows we consumed them.
while (tokens.has_next_token()) while (tokens.has_next_token())

View file

@ -118,8 +118,10 @@ RefPtr<CSSStyleValue const> descriptor_initial_value(AtRuleID, DescriptorID);
struct DescriptorMetadata { struct DescriptorMetadata {
enum class ValueType { enum class ValueType {
// FIXME: Parse the grammar instead of hard-coding all the options! // FIXME: Parse the grammar instead of hard-coding all the options!
CropOrCross,
FamilyName, FamilyName,
FontSrcList, FontSrcList,
Length,
OptionalDeclarationValue, OptionalDeclarationValue,
PageSize, PageSize,
PositivePercentage, PositivePercentage,
@ -387,6 +389,8 @@ DescriptorMetadata get_descriptor_metadata(AtRuleID at_rule_id, DescriptorID des
return "FontSrcList"_string; return "FontSrcList"_string;
if (syntax_string == "<declaration-value>?"sv) if (syntax_string == "<declaration-value>?"sv)
return "OptionalDeclarationValue"_string; return "OptionalDeclarationValue"_string;
if (syntax_string == "<length>"sv)
return "Length"_string;
if (syntax_string == "<page-size>"sv) if (syntax_string == "<page-size>"sv)
return "PageSize"_string; return "PageSize"_string;
if (syntax_string == "<percentage [0,∞]>"sv) if (syntax_string == "<percentage [0,∞]>"sv)
@ -395,13 +399,18 @@ DescriptorMetadata get_descriptor_metadata(AtRuleID at_rule_id, DescriptorID des
return "String"_string; return "String"_string;
if (syntax_string == "<unicode-range-token>#"sv) if (syntax_string == "<unicode-range-token>#"sv)
return "UnicodeRangeTokens"_string; return "UnicodeRangeTokens"_string;
dbgln("Unrecognized value type: `{}`", syntax_string);
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
}(); }();
option_generator.set("value_type"sv, value_type); option_generator.set("value_type"sv, value_type);
option_generator.append(R"~~~( option_generator.append(R"~~~(
metadata.syntax.empend(DescriptorMetadata::ValueType::@value_type@); metadata.syntax.empend(DescriptorMetadata::ValueType::@value_type@);
)~~~"); )~~~");
} else if (syntax_string == "crop || cross"sv) {
// FIXME: This is extra hacky.
option_generator.append(R"~~~(
metadata.syntax.empend(DescriptorMetadata::ValueType::CropOrCross);
)~~~");
} else { } else {
// Keyword // Keyword
option_generator.set("keyword:titlecase"sv, title_casify(syntax_string)); option_generator.set("keyword:titlecase"sv, title_casify(syntax_string));

View file

@ -0,0 +1,8 @@
"bleed: auto": parsed as "auto"
"bleed: 1in": parsed as "1in"
"bleed: -2cm": parsed as "-2cm"
"bleed: auto 1in": INVALID
"bleed: 3cm auto": INVALID
"bleed: 1cm 2cm": INVALID
"bleed: orange": INVALID
"bleed: none": INVALID

View file

@ -0,0 +1,11 @@
"marks: none": parsed as "none"
"marks: crop": parsed as "crop"
"marks: cross": parsed as "cross"
"marks: crop cross": parsed as "crop cross"
"marks: cross crop": parsed as "crop cross"
"marks: none crop": INVALID
"marks: crop crop": INVALID
"marks: cross cross": INVALID
"marks: cross crop cross": INVALID
"marks: orange": INVALID
"marks: auto": INVALID

View file

@ -0,0 +1,7 @@
"page-orientation: upright": parsed as "upright"
"page-orientation: rotate-left": parsed as "rotate-left"
"page-orientation: rotate-right": parsed as "rotate-right"
"page-orientation: auto": INVALID
"page-orientation: none": INVALID
"page-orientation: rotate-left rotate-right": INVALID
"page-orientation: 90deg": INVALID

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<style>
@page {}
</style>
<script src="../include.js"></script>
<script>
test(() => {
let page = document.styleSheets[0].cssRules[0];
const testCases = [
"auto",
"1in",
"-2cm",
// Invalid
"auto 1in",
"3cm auto",
"1cm 2cm",
"orange",
"none"
];
for (let testCase of testCases) {
page.style.removeProperty("bleed");
page.style.bleed = testCase;
let parsed = page.style.bleed;
if (parsed.length)
println(`"bleed: ${testCase}": parsed as "${parsed}"`);
else
println(`"bleed: ${testCase}": INVALID`);
}
});
</script>

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<style>
@page {}
</style>
<script src="../include.js"></script>
<script>
test(() => {
let page = document.styleSheets[0].cssRules[0];
const testCases = [
"none",
"crop",
"cross",
"crop cross",
"cross crop",
// Invalid
"none crop",
"crop crop",
"cross cross",
"cross crop cross",
"orange",
"auto"
];
for (let testCase of testCases) {
page.style.removeProperty("marks");
page.style.marks = testCase;
let parsed = page.style.marks;
if (parsed.length)
println(`"marks: ${testCase}": parsed as "${parsed}"`);
else
println(`"marks: ${testCase}": INVALID`);
}
});
</script>

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<style>
@page {}
</style>
<script src="../include.js"></script>
<script>
test(() => {
let page = document.styleSheets[0].cssRules[0];
const testCases = [
"upright",
"rotate-left",
"rotate-right",
// Invalid
"auto",
"none",
"rotate-left rotate-right",
"90deg"
];
for (let testCase of testCases) {
page.style.removeProperty("page-orientation");
page.style.pageOrientation = testCase;
let parsed = page.style.pageOrientation;
if (parsed.length)
println(`"page-orientation: ${testCase}": parsed as "${parsed}"`);
else
println(`"page-orientation: ${testCase}": INVALID`);
}
});
</script>