mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-27 18:17:22 +02:00
LibWeb: Map logical aliases at cascade time
Previously we would incorrectly map these in `CSSStyleProperties::convert_declarations_to_specified_order`, aside from being too early (as it meant we didn't maintain them as distinct from their physical counterparts in CSSStyleProperties), this meant that we didn't yet have the required context to map them correctly. We now map them as part of the cascade process. To compute the mapping context we do a cascade without mapping, and extract the relevant properties (writing-direction and direction).
This commit is contained in:
Notes:
github-actions[bot]
2025-06-23 14:20:40 +00:00
Author: https://github.com/Calme1709 Commit: https://github.com/LadybirdBrowser/ladybird/commit/cfc8d3031b8 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5138 Reviewed-by: https://github.com/AtkinsSJ ✅
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<title>CSS Logical Properties: Flow-Relative Offsets</title>
|
||||
<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com" />
|
||||
<link rel="help" href="https://drafts.csswg.org/css-logical/#inset-properties">
|
||||
<meta name="assert" content="This test checks the interaction of the flow-relative inset-* properties with the physical ones in different writing modes." />
|
||||
<script src="../../resources/testharness.js"></script>
|
||||
<script src="../../resources/testharnessreport.js"></script>
|
||||
|
||||
<div id="log"></div>
|
||||
|
||||
<script type="module">
|
||||
import {runTests, createBoxPropertyGroup} from "./resources/test-box-properties.js";
|
||||
runTests(createBoxPropertyGroup("inset-*", {
|
||||
type: "length",
|
||||
prerequisites: {"position": "relative"},
|
||||
}));
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<title>CSS Logical Properties: Flow-Relative Margins</title>
|
||||
<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com" />
|
||||
<link rel="help" href="https://drafts.csswg.org/css-logical/#margin-properties">
|
||||
<meta name="assert" content="This test checks the interaction of the flow-relative margin-* properties with the physical ones in different writing modes." />
|
||||
<script src="../../resources/testharness.js"></script>
|
||||
<script src="../../resources/testharnessreport.js"></script>
|
||||
|
||||
<div id="log"></div>
|
||||
|
||||
<script type="module">
|
||||
import {runTests, createBoxPropertyGroup} from "./resources/test-box-properties.js";
|
||||
runTests(createBoxPropertyGroup("margin-*", {type: "length"}));
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<title>CSS Logical Properties: Flow-Relative Padding</title>
|
||||
<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com" />
|
||||
<link rel="help" href="https://drafts.csswg.org/css-logical/#padding-properties">
|
||||
<meta name="assert" content="This test checks the interaction of the flow-relative padding-* properties with the physical ones in different writing modes." />
|
||||
<script src="../../resources/testharness.js"></script>
|
||||
<script src="../../resources/testharnessreport.js"></script>
|
||||
|
||||
<div id="log"></div>
|
||||
|
||||
<script type="module">
|
||||
import {runTests, createBoxPropertyGroup} from "./resources/test-box-properties.js";
|
||||
runTests(createBoxPropertyGroup("padding-*", {type: "length"}));
|
||||
</script>
|
||||
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
testElement,
|
||||
writingModes,
|
||||
testCSSValues,
|
||||
testComputedValues,
|
||||
makeDeclaration
|
||||
} from "./test-shared.js";
|
||||
|
||||
// Values to use while testing
|
||||
const testValues = {
|
||||
"length": ["1px", "2px", "3px", "4px", "5px"],
|
||||
"color": ["rgb(1, 1, 1)", "rgb(2, 2, 2)", "rgb(3, 3, 3)", "rgb(4, 4, 4)", "rgb(5, 5, 5)"],
|
||||
"border-style": ["solid", "dashed", "dotted", "double", "groove"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a group of physical and logical box properties, such as
|
||||
*
|
||||
* { physical: {
|
||||
* left: "margin-left", right: "margin-right",
|
||||
* top: "margin-top", bottom: "margin-bottom",
|
||||
* }, logical: {
|
||||
* inlineStart: "margin-inline-start", inlineEnd: "margin-inline-end",
|
||||
* blockStart: "margin-block-start", blockEnd: "margin-block-end",
|
||||
* }, shorthands: {
|
||||
* "margin": ["margin-top", "margin-right", "margin-bottom", "margin-left"],
|
||||
* "margin-inline": ["margin-inline-start", "margin-inline-end"],
|
||||
* "margin-block": ["margin-block-start", "margin-block-end"],
|
||||
* }, type: ["length"], prerequisites: "...", property: "margin-*" }
|
||||
*
|
||||
* @param {string} property
|
||||
* A string representing the property names, like "margin-*".
|
||||
* @param {Object} descriptor
|
||||
* @param {string|string[]} descriptor.type
|
||||
* Describes the kind of values accepted by the property, like "length".
|
||||
* Must be a key or a collection of keys from the `testValues` object.
|
||||
* @param {Object={}} descriptor.prerequisites
|
||||
* Represents property declarations that are needed by `property` to work.
|
||||
* For example, border-width properties require a border style.
|
||||
*/
|
||||
export function createBoxPropertyGroup(property, descriptor) {
|
||||
const logical = {};
|
||||
const physical = {};
|
||||
const shorthands = {};
|
||||
for (const axis of ["inline", "block"]) {
|
||||
const shorthand = property.replace("*", axis);
|
||||
const longhands = [];
|
||||
shorthands[shorthand] = longhands;
|
||||
for (const side of ["start", "end"]) {
|
||||
const logicalSide = axis + "-" + side;
|
||||
const camelCase = logicalSide.replace(/-(.)/g, (match, $1) => $1.toUpperCase());
|
||||
const longhand = property.replace("*", logicalSide);
|
||||
logical[camelCase] = longhand;
|
||||
longhands.push(longhand);
|
||||
}
|
||||
}
|
||||
const isInset = property === "inset-*";
|
||||
let prerequisites = "";
|
||||
for (const physicalSide of ["left", "right", "top", "bottom"]) {
|
||||
physical[physicalSide] = isInset ? physicalSide : property.replace("*", physicalSide);
|
||||
prerequisites += makeDeclaration(descriptor.prerequisites, physicalSide);
|
||||
}
|
||||
shorthands[property.replace("-*", "")] =
|
||||
["top", "right", "bottom", "left"].map(physicalSide => physical[physicalSide]);
|
||||
const type = [].concat(descriptor.type);
|
||||
return { logical, physical, shorthands, type, prerequisites, property };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a group physical and logical box-corner properties.
|
||||
*
|
||||
* @param {string} property
|
||||
* A string representing the property names, like "border-*-radius".
|
||||
* @param {Object} descriptor
|
||||
* @param {string|string[]} descriptor.type
|
||||
* Describes the kind of values accepted by the property, like "length".
|
||||
* Must be a key or a collection of keys from the `testValues` object.
|
||||
* @param {Object={}} descriptor.prerequisites
|
||||
* Represents property declarations that are needed by `property` to work.
|
||||
* For example, border-width properties require a border style.
|
||||
*/
|
||||
export function createCornerPropertyGroup(property, descriptor) {
|
||||
const logical = {};
|
||||
const physical = {};
|
||||
const shorthands = {};
|
||||
for (const logicalCorner of ["start-start", "start-end", "end-start", "end-end"]) {
|
||||
const prop = property.replace("*", logicalCorner);
|
||||
const [block_side, inline_side] = logicalCorner.split("-");
|
||||
const b = "block" + block_side.charAt(0).toUpperCase() + block_side.slice(1);
|
||||
const i = "inline" + inline_side.charAt(0).toUpperCase() + inline_side.slice(1);
|
||||
const index = b + "-" + i; // e.g. "blockStart-inlineEnd"
|
||||
logical[index] = prop;
|
||||
}
|
||||
let prerequisites = "";
|
||||
for (const physicalCorner of ["top-left", "top-right", "bottom-left", "bottom-right"]) {
|
||||
const prop = property.replace("*", physicalCorner);
|
||||
physical[physicalCorner] = prop;
|
||||
prerequisites += makeDeclaration(descriptor.prerequisites, physicalCorner);
|
||||
}
|
||||
const type = [].concat(descriptor.type);
|
||||
return { logical, physical, shorthands, type, prerequisites, property };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a group of physical and logical sizing properties.
|
||||
*
|
||||
* @param {string} prefix
|
||||
* One of "", "max-" or "min-".
|
||||
*/
|
||||
export function createSizingPropertyGroup(prefix) {
|
||||
return {
|
||||
logical: {
|
||||
inline: `${prefix}inline-size`,
|
||||
block: `${prefix}block-size`,
|
||||
},
|
||||
physical: {
|
||||
horizontal: `${prefix}width`,
|
||||
vertical: `${prefix}height`,
|
||||
},
|
||||
type: ["length"],
|
||||
prerequisites: makeDeclaration({ display: "block" }),
|
||||
property: (prefix ? prefix.slice(0, -1) + " " : "") + "sizing",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a grup of logical and physical properties in different writing modes.
|
||||
*
|
||||
* @param {Object} group
|
||||
* An object returned by createBoxPropertyGroup or createSizingPropertyGroup.
|
||||
*/
|
||||
export function runTests(group) {
|
||||
const values = testValues[group.type[0]].map(function (_, i) {
|
||||
return group.type.map(type => testValues[type][i]).join(" ");
|
||||
});
|
||||
const logicals = Object.values(group.logical);
|
||||
const physicals = Object.values(group.physical);
|
||||
const shorthands = group.shorthands ? Object.entries(group.shorthands) : null;
|
||||
const is_corner = group.property == "border-*-radius";
|
||||
|
||||
test(function () {
|
||||
const expected = [];
|
||||
for (const [i, logicalProp] of logicals.entries()) {
|
||||
testElement.style.setProperty(logicalProp, values[i]);
|
||||
expected.push([logicalProp, values[i]]);
|
||||
}
|
||||
testCSSValues("logical properties in inline style", testElement.style, expected);
|
||||
}, `Test that logical ${group.property} properties are supported.`);
|
||||
testElement.style.cssText = "";
|
||||
|
||||
const shorthandValues = {};
|
||||
for (const [shorthand, longhands] of shorthands || []) {
|
||||
let valueArray;
|
||||
if (group.type.length > 1) {
|
||||
valueArray = [values[0]];
|
||||
} else {
|
||||
valueArray = testValues[group.type].slice(0, longhands.length);
|
||||
}
|
||||
shorthandValues[shorthand] = valueArray;
|
||||
const value = valueArray.join(" ");
|
||||
const expected = [[shorthand, value]];
|
||||
for (let [i, longhand] of longhands.entries()) {
|
||||
expected.push([longhand, valueArray[group.type.length > 1 ? 0 : i]]);
|
||||
}
|
||||
test(function () {
|
||||
testElement.style.setProperty(shorthand, value);
|
||||
testCSSValues("shorthand in inline style", testElement.style, expected);
|
||||
const stylesheet = `.test { ${group.prerequisites} }`;
|
||||
testComputedValues("shorthand in computed style", stylesheet, expected);
|
||||
}, `Test that ${shorthand} shorthand sets longhands and serializes correctly.`);
|
||||
testElement.style.cssText = "";
|
||||
}
|
||||
|
||||
for (const writingMode of writingModes) {
|
||||
for (const style of writingMode.styles) {
|
||||
const writingModeDecl = makeDeclaration(style);
|
||||
|
||||
const associated = {};
|
||||
for (const [logicalSide, logicalProp] of Object.entries(group.logical)) {
|
||||
let physicalProp;
|
||||
if (is_corner) {
|
||||
const [block_side, inline_side] = logicalSide.split("-");
|
||||
const physicalSide1 = writingMode[block_side];
|
||||
const physicalSide2 = writingMode[inline_side];
|
||||
let physicalCorner;
|
||||
// mirror "left-top" to "top-left" etc
|
||||
if (["top", "bottom"].includes(physicalSide1)) {
|
||||
physicalCorner = physicalSide1 + "-" + physicalSide2;
|
||||
} else {
|
||||
physicalCorner = physicalSide2 + "-" + physicalSide1;
|
||||
}
|
||||
physicalProp = group.physical[physicalCorner];
|
||||
} else {
|
||||
physicalProp = group.physical[writingMode[logicalSide]];
|
||||
}
|
||||
associated[logicalProp] = physicalProp;
|
||||
associated[physicalProp] = logicalProp;
|
||||
}
|
||||
|
||||
// Test that logical properties are converted to their physical
|
||||
// equivalent correctly when all in the group are present on a single
|
||||
// declaration, with no overwriting of previous properties and
|
||||
// no physical properties present. We put the writing mode properties
|
||||
// on a separate declaration to test that the computed values of these
|
||||
// properties are used, rather than those on the same declaration.
|
||||
test(function () {
|
||||
let decl = group.prerequisites;
|
||||
const expected = [];
|
||||
for (const [i, logicalProp] of logicals.entries()) {
|
||||
decl += `${logicalProp}: ${values[i]}; `;
|
||||
expected.push([logicalProp, values[i]]);
|
||||
expected.push([associated[logicalProp], values[i]]);
|
||||
}
|
||||
testComputedValues("logical properties on one declaration, writing " +
|
||||
`mode properties on another, '${writingModeDecl}'`,
|
||||
`.test { ${writingModeDecl} } .test { ${decl} }`,
|
||||
expected);
|
||||
}, `Test that logical ${group.property} properties share computed values `
|
||||
+ `with their physical associates, with '${writingModeDecl}'.`);
|
||||
|
||||
// Test logical shorthand properties.
|
||||
if (shorthands) {
|
||||
test(function () {
|
||||
for (const [shorthand, longhands] of shorthands) {
|
||||
let valueArray = shorthandValues[shorthand];
|
||||
const decl = group.prerequisites + `${shorthand}: ${valueArray.join(" ")}; `;
|
||||
const expected = [];
|
||||
for (let [i, longhand] of longhands.entries()) {
|
||||
const longhandValue = valueArray[group.type.length > 1 ? 0 : i];
|
||||
expected.push([longhand, longhandValue]);
|
||||
expected.push([associated[longhand], longhandValue]);
|
||||
}
|
||||
testComputedValues("shorthand properties on one declaration, writing " +
|
||||
`mode properties on another, '${writingModeDecl}'`,
|
||||
`.test { ${writingModeDecl} } .test { ${decl} }`,
|
||||
expected);
|
||||
}
|
||||
}, `Test that ${group.property} shorthands set the computed value of both `
|
||||
+ `logical and physical longhands, with '${writingModeDecl}'.`);
|
||||
}
|
||||
|
||||
// Test that logical and physical properties are cascaded together,
|
||||
// honoring their relative order on a single declaration
|
||||
// (a) with a single logical property after the physical ones
|
||||
// (b) with a single physical property after the logical ones
|
||||
test(function () {
|
||||
for (const lastIsLogical of [true, false]) {
|
||||
const lasts = lastIsLogical ? logicals : physicals;
|
||||
const others = lastIsLogical ? physicals : logicals;
|
||||
for (const lastProp of lasts) {
|
||||
let decl = writingModeDecl + group.prerequisites;
|
||||
const expected = [];
|
||||
for (const [i, prop] of others.entries()) {
|
||||
decl += `${prop}: ${values[i]}; `;
|
||||
const valueIdx = associated[prop] === lastProp ? others.length : i;
|
||||
expected.push([prop, values[valueIdx]]);
|
||||
expected.push([associated[prop], values[valueIdx]]);
|
||||
}
|
||||
decl += `${lastProp}: ${values[others.length]}; `;
|
||||
testComputedValues(`'${lastProp}' last on single declaration, '${writingModeDecl}'`,
|
||||
`.test { ${decl} }`,
|
||||
expected);
|
||||
}
|
||||
}
|
||||
}, `Test that ${group.property} properties honor order of appearance when both `
|
||||
+ `logical and physical associates are declared, with '${writingModeDecl}'.`);
|
||||
|
||||
// Test that logical and physical properties are cascaded properly when
|
||||
// on different declarations
|
||||
// (a) with a logical property in the high specificity rule
|
||||
// (b) with a physical property in the high specificity rule
|
||||
test(function () {
|
||||
for (const highIsLogical of [true, false]) {
|
||||
let lowDecl = writingModeDecl + group.prerequisites;
|
||||
const high = highIsLogical ? logicals : physicals;
|
||||
const others = highIsLogical ? physicals : logicals;
|
||||
for (const [i, prop] of others.entries()) {
|
||||
lowDecl += `${prop}: ${values[i]}; `;
|
||||
}
|
||||
for (const highProp of high) {
|
||||
const highDecl = `${highProp}: ${values[others.length]}; `;
|
||||
const expected = [];
|
||||
for (const [i, prop] of others.entries()) {
|
||||
const valueIdx = associated[prop] === highProp ? others.length : i;
|
||||
expected.push([prop, values[valueIdx]]);
|
||||
expected.push([associated[prop], values[valueIdx]]);
|
||||
}
|
||||
testComputedValues(`'${highProp}', two declarations, '${writingModeDecl}'`,
|
||||
`#test { ${highDecl} } .test { ${lowDecl} }`,
|
||||
expected);
|
||||
}
|
||||
}
|
||||
}, `Test that ${group.property} properties honor selector specificty when both `
|
||||
+ `logical and physical associates are declared, with '${writingModeDecl}'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
const sheet = document.head.appendChild(document.createElement("style"));
|
||||
|
||||
// Specify size for outer <div> to avoid unconstrained-size warnings
|
||||
// when writing-mode of the inner test <div> is vertical-*
|
||||
const wrapper = document.body.appendChild(document.createElement("div"));
|
||||
wrapper.style.cssText = "width:100px; height: 100px;";
|
||||
export const testElement = wrapper.appendChild(document.createElement("div"));
|
||||
testElement.id = testElement.className = "test";
|
||||
|
||||
// Six unique overall writing modes for property-mapping purposes.
|
||||
export const writingModes = [
|
||||
{
|
||||
styles: [
|
||||
{ "writing-mode": "horizontal-tb", "direction": "ltr" },
|
||||
],
|
||||
blockStart: "top", blockEnd: "bottom", inlineStart: "left", inlineEnd: "right",
|
||||
over: "top", under: "bottom", lineLeft: "left", lineRight: "right",
|
||||
block: "vertical", inline: "horizontal"
|
||||
},
|
||||
{
|
||||
styles: [
|
||||
{ "writing-mode": "horizontal-tb", "direction": "rtl" },
|
||||
],
|
||||
blockStart: "top", blockEnd: "bottom", inlineStart: "right", inlineEnd: "left",
|
||||
over: "top", under: "bottom", lineLeft: "left", lineRight: "right",
|
||||
block: "vertical", inline: "horizontal"
|
||||
},
|
||||
{
|
||||
styles: [
|
||||
{ "writing-mode": "vertical-rl", "direction": "rtl" },
|
||||
{ "writing-mode": "sideways-rl", "direction": "rtl" },
|
||||
],
|
||||
blockStart: "right", blockEnd: "left", inlineStart: "bottom", inlineEnd: "top",
|
||||
over: "right", under: "left", lineLeft: "top", lineRight: "bottom",
|
||||
block: "horizontal", inline: "vertical"
|
||||
},
|
||||
{
|
||||
styles: [
|
||||
{ "writing-mode": "vertical-rl", "direction": "ltr" },
|
||||
{ "writing-mode": "sideways-rl", "direction": "ltr" },
|
||||
],
|
||||
blockStart: "right", blockEnd: "left", inlineStart: "top", inlineEnd: "bottom",
|
||||
over: "right", under: "left", lineLeft: "top", lineRight: "bottom",
|
||||
block: "horizontal", inline: "vertical"
|
||||
},
|
||||
{
|
||||
styles: [
|
||||
{ "writing-mode": "vertical-lr", "direction": "rtl" },
|
||||
],
|
||||
blockStart: "left", blockEnd: "right", inlineStart: "bottom", inlineEnd: "top",
|
||||
over: "right", under: "left", lineLeft: "top", lineRight: "bottom",
|
||||
block: "horizontal", inline: "vertical"
|
||||
},
|
||||
{
|
||||
styles: [
|
||||
{ "writing-mode": "sideways-lr", "direction": "ltr" },
|
||||
],
|
||||
blockStart: "left", blockEnd: "right", inlineStart: "bottom", inlineEnd: "top",
|
||||
over: "left", under: "right", lineLeft: "bottom", lineRight: "top",
|
||||
block: "horizontal", inline: "vertical"
|
||||
},
|
||||
{
|
||||
styles: [
|
||||
{ "writing-mode": "vertical-lr", "direction": "ltr" },
|
||||
],
|
||||
blockStart: "left", blockEnd: "right", inlineStart: "top", inlineEnd: "bottom",
|
||||
over: "right", under: "left", lineLeft: "top", lineRight: "bottom",
|
||||
block: "horizontal", inline: "vertical"
|
||||
},
|
||||
{
|
||||
styles: [
|
||||
{ "writing-mode": "sideways-lr", "direction": "rtl" },
|
||||
],
|
||||
blockStart: "left", blockEnd: "right", inlineStart: "top", inlineEnd: "bottom",
|
||||
over: "left", under: "right", lineLeft: "bottom", lineRight: "top",
|
||||
block: "horizontal", inline: "vertical"
|
||||
},
|
||||
];
|
||||
|
||||
// Check if logical properties work well in WebKit non-standard
|
||||
// '-webkit-writing-mode: horizontal-bt' mode
|
||||
if (CSS.supports("-webkit-writing-mode", "horizontal-bt")) {
|
||||
writingModes.push(
|
||||
{
|
||||
styles: [
|
||||
{ "-webkit-writing-mode": "horizontal-bt", "direction": "ltr" },
|
||||
],
|
||||
blockStart: "bottom", blockEnd: "top", inlineStart: "left", inlineEnd: "right",
|
||||
over: "top", under: "bottom", lineLeft: "left", lineRight: "right",
|
||||
block: "vertical", inline: "horizontal"
|
||||
},
|
||||
{
|
||||
styles: [
|
||||
{ "-webkit-writing-mode": "horizontal-bt", "direction": "rtl" },
|
||||
],
|
||||
blockStart: "bottom", blockEnd: "top", inlineStart: "right", inlineEnd: "left",
|
||||
over: "top", under: "bottom", lineLeft: "left", lineRight: "right",
|
||||
block: "vertical", inline: "horizontal"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function testCSSValues(testName, style, expectedValues) {
|
||||
for (const [property, value] of expectedValues) {
|
||||
assert_equals(style.getPropertyValue(property), value, `${testName}, ${property}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function testComputedValues(testName, rules, expectedValues) {
|
||||
sheet.textContent = rules;
|
||||
const cs = getComputedStyle(testElement);
|
||||
testCSSValues(testName, cs, expectedValues);
|
||||
sheet.textContent = "";
|
||||
}
|
||||
|
||||
export function makeDeclaration(object = {}, replacement = "*") {
|
||||
let decl = "";
|
||||
for (const [property, value] of Object.entries(object)) {
|
||||
decl += `${property.replace("*", replacement)}: ${value}; `;
|
||||
}
|
||||
return decl;
|
||||
}
|
||||
Reference in New Issue
Block a user