From bce61257d8be8124b65230142b29a505a24bc4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:11:47 +0100 Subject: [PATCH 01/40] Add `mask-*` longhands --- packages/alfa-css/src/tsconfig.json | 1 + packages/alfa-css/src/value/coord-box.ts | 68 +++++++++++ packages/alfa-css/src/value/index.ts | 1 + packages/alfa-style/src/longhands.ts | 16 +++ packages/alfa-style/src/property/mask-clip.ts | 19 +++ .../alfa-style/src/property/mask-composite.ts | 28 +++++ .../alfa-style/src/property/mask-image.ts | 31 +++++ packages/alfa-style/src/property/mask-mode.ts | 26 +++++ .../alfa-style/src/property/mask-origin.ts | 16 +++ .../alfa-style/src/property/mask-position.ts | 46 ++++++++ .../alfa-style/src/property/mask-repeat.ts | 37 ++++++ packages/alfa-style/src/property/mask-size.ts | 95 +++++++++++++++ packages/alfa-style/src/tsconfig.json | 8 ++ .../test/property/mask-clip.spec.tsx | 108 ++++++++++++++++++ .../test/property/mask-composite.spec.tsx | 72 ++++++++++++ .../test/property/mask-image.spec.tsx | 99 ++++++++++++++++ .../test/property/mask-mode.spec.tsx | 0 .../test/property/mask-origin.spec.tsx | 0 .../test/property/mask-position.spec.tsx | 0 .../test/property/mask-repeat.spec.tsx | 0 .../test/property/mask-size.spec.tsx | 0 packages/alfa-style/test/tsconfig.json | 8 ++ 22 files changed, 679 insertions(+) create mode 100644 packages/alfa-css/src/value/coord-box.ts create mode 100644 packages/alfa-style/src/property/mask-clip.ts create mode 100644 packages/alfa-style/src/property/mask-composite.ts create mode 100644 packages/alfa-style/src/property/mask-image.ts create mode 100644 packages/alfa-style/src/property/mask-mode.ts create mode 100644 packages/alfa-style/src/property/mask-origin.ts create mode 100644 packages/alfa-style/src/property/mask-position.ts create mode 100644 packages/alfa-style/src/property/mask-repeat.ts create mode 100644 packages/alfa-style/src/property/mask-size.ts create mode 100644 packages/alfa-style/test/property/mask-clip.spec.tsx create mode 100644 packages/alfa-style/test/property/mask-composite.spec.tsx create mode 100644 packages/alfa-style/test/property/mask-image.spec.tsx create mode 100644 packages/alfa-style/test/property/mask-mode.spec.tsx create mode 100644 packages/alfa-style/test/property/mask-origin.spec.tsx create mode 100644 packages/alfa-style/test/property/mask-position.spec.tsx create mode 100644 packages/alfa-style/test/property/mask-repeat.spec.tsx create mode 100644 packages/alfa-style/test/property/mask-size.spec.tsx diff --git a/packages/alfa-css/src/tsconfig.json b/packages/alfa-css/src/tsconfig.json index 68d3344dd9..d909082cb8 100644 --- a/packages/alfa-css/src/tsconfig.json +++ b/packages/alfa-css/src/tsconfig.json @@ -32,6 +32,7 @@ "./syntax/token.ts", "./value/box.ts", "./value/contain.ts", + "./value/coord-box.ts", "./unit/converter.ts", "./unit/index.ts", "./unit/unit.ts", diff --git a/packages/alfa-css/src/value/coord-box.ts b/packages/alfa-css/src/value/coord-box.ts new file mode 100644 index 0000000000..c9a40795b8 --- /dev/null +++ b/packages/alfa-css/src/value/coord-box.ts @@ -0,0 +1,68 @@ +import { Parser } from "@siteimprove/alfa-parser"; + +import { type Parser as CSSParser } from "../syntax/index.js"; + +import { Keyword } from "./textual/keyword.js"; + +const { either } = Parser; + +/** + * @internal + */ +type VisualBox = + | Keyword<"content-box"> + | Keyword<"padding-box"> + | Keyword<"border-box">; + +/** + * @internal + */ +namespace VisualBox { + export type JSON = + | Keyword.JSON<"content-box"> + | Keyword.JSON<"padding-box"> + | Keyword.JSON<"border-box">; + + export const parse: CSSParser = Keyword.parse( + "content-box", + "padding-box", + "border-box", + ); +} + +/** + * @internal + */ +type PaintBox = VisualBox | Keyword<"fill-box"> | Keyword<"stroke-box">; + +/** + * @internal + */ +namespace PaintBox { + export type JSON = + | VisualBox.JSON + | Keyword.JSON<"fill-box"> + | Keyword.JSON<"stroke-box">; + + export const parse: CSSParser = either( + VisualBox.parse, + Keyword.parse("fill-box", "stroke-box"), + ); +} + +/** + * @public + */ +export type CoordBox = PaintBox | Keyword<"view-box">; + +/** + * @public + */ +export namespace CoordBox { + export type JSON = PaintBox.JSON | Keyword.JSON<"view-box">; + + export const parse: CSSParser = either( + PaintBox.parse, + Keyword.parse("view-box"), + ); +} diff --git a/packages/alfa-css/src/value/index.ts b/packages/alfa-css/src/value/index.ts index 552e9dcfde..35b233fe08 100644 --- a/packages/alfa-css/src/value/index.ts +++ b/packages/alfa-css/src/value/index.ts @@ -1,5 +1,6 @@ export * from "./box.js"; export * from "./contain.js"; +export * from "./coord-box.js"; export * from "./collection/index.js"; export * from "./color/index.js"; export * from "./image/index.js"; diff --git a/packages/alfa-style/src/longhands.ts b/packages/alfa-style/src/longhands.ts index 266561512e..148d3869f2 100644 --- a/packages/alfa-style/src/longhands.ts +++ b/packages/alfa-style/src/longhands.ts @@ -83,6 +83,14 @@ import MarginBottom from "./property/margin-bottom.js"; import MarginLeft from "./property/margin-left.js"; import MarginRight from "./property/margin-right.js"; import MarginTop from "./property/margin-top.js"; +import MaskClip from "./property/mask-clip.js"; +import MaskComposite from "./property/mask-composite.js"; +import MaskImage from "./property/mask-image.js"; +import MaskMode from "./property/mask-mode.js"; +import MaskOrigin from "./property/mask-origin.js"; +import MaskPosition from "./property/mask-position.js"; +import MaskRepeat from "./property/mask-repeat.js"; +import MaskSize from "./property/mask-size.js"; import MinHeight from "./property/min-height.js"; import MinWidth from "./property/min-width.js"; import MixBlendMode from "./property/mix-blend-mode.js"; @@ -268,6 +276,14 @@ export namespace Longhands { "margin-left": MarginLeft, "margin-right": MarginRight, "margin-top": MarginTop, + "mask-clip": MaskClip, + "mask-composite": MaskComposite, + "mask-image": MaskImage, + "mask-mode": MaskMode, + "mask-origin": MaskOrigin, + "mask-position": MaskPosition, + "mask-repeat": MaskRepeat, + "mask-size": MaskSize, "min-height": MinHeight, "min-width": MinWidth, "mix-blend-mode": MixBlendMode, diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts new file mode 100644 index 0000000000..189d494cde --- /dev/null +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -0,0 +1,19 @@ +import { Parser } from "@siteimprove/alfa-parser"; +import { CoordBox, Keyword, List } from "@siteimprove/alfa-css"; + +import { Longhand } from "../longhand.js"; + +const { either } = Parser; + +type Specified = List>; +type Computed = Specified; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-clip} + * @internal + */ +export default Longhand.of( + List.of([Keyword.of("border-box")]), + List.parseCommaSeparated(either(CoordBox.parse, Keyword.parse("no-clip"))), + (value) => value, +); diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts new file mode 100644 index 0000000000..6ec11918e0 --- /dev/null +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -0,0 +1,28 @@ +import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; + +import { Longhand } from "../longhand.js"; + +type CompositingOperator = + | Keyword<"add"> + | Keyword<"subtract"> + | Keyword<"intersect"> + | Keyword<"exclude">; +const compositingOperator: CSSParser = Keyword.parse( + "add", + "subtract", + "intersect", + "exclude", +); + +type Specified = List; +type Computed = Specified; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite} + * @internal + */ +export default Longhand.of( + List.of([Keyword.of("add")]), + List.parseCommaSeparated(compositingOperator), + (value) => value, +); diff --git a/packages/alfa-style/src/property/mask-image.ts b/packages/alfa-style/src/property/mask-image.ts new file mode 100644 index 0000000000..808c9b3265 --- /dev/null +++ b/packages/alfa-style/src/property/mask-image.ts @@ -0,0 +1,31 @@ +import { Parser } from "@siteimprove/alfa-parser"; +import { + Image, + Keyword, + List, + URL, + type Parser as CSSParser, +} from "@siteimprove/alfa-css"; + +const { either } = Parser; + +import { Longhand } from "../longhand.js"; + +type MaskReference = Keyword<"none"> | Image | URL; +const maskReference: CSSParser = either( + Keyword.parse("none"), + either(Image.parse, URL.parse), +); + +type Specified = List; +type Computed = Specified; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite} + * @internal + */ +export default Longhand.of( + List.of([Keyword.of("none")]), + List.parseCommaSeparated(maskReference), + (value) => value, // TODO: as specified, but with values made absolute +); diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts new file mode 100644 index 0000000000..22ab9cc2e1 --- /dev/null +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -0,0 +1,26 @@ +import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; + +import { Longhand } from "../longhand.js"; + +type MaskingMode = + | Keyword<"alpha"> + | Keyword<"luminance"> + | Keyword<"match-source">; +const maskingMode: CSSParser = Keyword.parse( + "alpha", + "luminance", + "match-source", +); + +type Specified = List; +type Computed = Specified; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-mode} + * @internal + */ +export default Longhand.of( + List.of([Keyword.of("match-source")]), + List.parseCommaSeparated(maskingMode), + (value) => value, +); diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts new file mode 100644 index 0000000000..494c6b57a8 --- /dev/null +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -0,0 +1,16 @@ +import { CoordBox, Keyword, List } from "@siteimprove/alfa-css"; + +import { Longhand } from "../longhand.js"; + +type Specified = List; +type Computed = Specified; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-origin} + * @internal + */ +export default Longhand.of( + List.of([Keyword.of("border-box")]), + List.parseCommaSeparated(CoordBox.parse), + (value) => value, +); diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts new file mode 100644 index 0000000000..001b76e2f6 --- /dev/null +++ b/packages/alfa-style/src/property/mask-position.ts @@ -0,0 +1,46 @@ +import { Keyword, List, Percentage, Position } from "@siteimprove/alfa-css"; + +import { Longhand } from "../longhand.js"; +import { Resolver } from "../resolver.js"; + +type Specified = List; + +/** + * @internal + */ +export namespace Specified { + export type Item = Position.Component; +} + +type Computed = List; + +namespace Computed { + export type Item = + Position.Component.PartiallyResolved; +} + +const parse = List.parseCommaSeparated(Position.Component.parseHorizontal); + +/** + * @internal + */ +export const initialItem: Computed.Item = Position.Side.of( + Keyword.of("left"), + Percentage.of(0), +); + +// TODO: Copied from background-position-x.ts. Adjust for mask-position. +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position} + * @internal + */ +export default Longhand.of( + List.of([initialItem]), + parse, + (value, style) => + value.map((positions) => + positions.map( + Position.Component.partiallyResolve(Resolver.length(style)), + ), + ), +); diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts new file mode 100644 index 0000000000..6c951af880 --- /dev/null +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -0,0 +1,37 @@ +import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; + +import { Longhand } from "../longhand.js"; + +const { either } = Parser; + +type RepeatStyle = + | Keyword<"repeat-x"> + | Keyword<"repeat-y"> + | List< + | Keyword<"repeat"> + | Keyword<"space"> + | Keyword<"round"> + | Keyword<"no-repeat"> + >; +const repeatStyle: CSSParser = either( + Keyword.parse("repeat-x", "repeat-y"), + List.parseSpaceSeparated( + Keyword.parse("repeat", "space", "round", "no-repeat"), + 1, + 2, + ), +); + +type Specified = List; +type Computed = Specified; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat} + * @internal + */ +export default Longhand.of( + List.of([List.of([Keyword.of("repeat")])]), + List.parseCommaSeparated(repeatStyle), + (value) => value, +); diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts new file mode 100644 index 0000000000..29a17600e2 --- /dev/null +++ b/packages/alfa-style/src/property/mask-size.ts @@ -0,0 +1,95 @@ +import { + Keyword, + List, + LengthPercentage, + Token, + Tuple, +} from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; + +import { Longhand } from "../longhand.js"; +import { Resolver } from "../resolver.js"; + +const { map, either, option, pair, right } = Parser; + +type Specified = List; + +/** + * @internal + */ +export namespace Specified { + type Dimension = LengthPercentage | Keyword<"auto">; + + export type Item = + | Tuple<[Dimension, Dimension]> + | Keyword<"cover"> + | Keyword<"contain">; +} + +type Computed = List; + +namespace Computed { + type Dimension = LengthPercentage.PartiallyResolved | Keyword<"auto">; + + export type Item = + | Tuple<[Dimension, Dimension]> + | Keyword<"cover"> + | Keyword<"contain">; +} + +/** + * @internal + */ +const parseDimension = either(LengthPercentage.parse, Keyword.parse("auto")); + +/** + * @internal + */ +export const parse = either( + map( + pair( + parseDimension, + map(option(right(Token.parseWhitespace, parseDimension)), (y) => + y.getOrElse(() => Keyword.of("auto")), + ), + ), + ([x, y]) => Tuple.of(x, y), + ), + Keyword.parse("contain", "cover"), +); + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = Tuple.of(Keyword.of("auto"), Keyword.of("auto")); + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-size} + * @internal + */ +export default Longhand.of( + List.of([initialItem], ", "), + parseList, + (value, style) => + value.map((sizes) => + sizes.map((size) => { + if (Keyword.isKeyword(size)) { + return size; + } + + const [x, y] = size.values; + const resolver = Resolver.length(style); + + return Tuple.of( + Keyword.isKeyword(x) + ? x + : LengthPercentage.partiallyResolve(resolver)(x), + Keyword.isKeyword(y) + ? y + : LengthPercentage.partiallyResolve(resolver)(y), + ); + }), + ), +); diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index 8a39abebd9..572bde3e22 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -149,6 +149,14 @@ "./property/letter-spacing.ts", "./property/line-height.ts", "./property/margin.ts", + "./property/mask-clip.ts", + "./property/mask-composite.ts", + "./property/mask-image.ts", + "./property/mask-mode.ts", + "./property/mask-origin.ts", + "./property/mask-position.ts", + "./property/mask-repeat.ts", + "./property/mask-size.ts", "./property/margin-bottom.ts", "./property/margin-left.ts", "./property/margin-right.ts", diff --git a/packages/alfa-style/test/property/mask-clip.spec.tsx b/packages/alfa-style/test/property/mask-clip.spec.tsx new file mode 100644 index 0000000000..c7a66a1f0f --- /dev/null +++ b/packages/alfa-style/test/property/mask-clip.spec.tsx @@ -0,0 +1,108 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../dist/index.js"; + +const device = Device.standard(); + +test("initial value is border-box", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-clip").toJSON(), { + value: { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "border-box", + }, + ], + }, + source: null, + }); +}); + +test("#computed parses single keywords", (t) => { + for (const kw of [ + "content-box", + "padding-box", + "border-box", + "fill-box", + "stroke-box", + "view-box", + "no-clip", + ] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-clip").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: kw, + }, + ], + }, + source: h.declaration("mask-clip", kw).toJSON(), + }); + } +}); + +test("#computed parses multiple keywords", (t) => { + const element1 =
; + const style1 = Style.from(element1, device); + t.deepEqual(style1.computed("mask-clip").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "padding-box", + }, + { + type: "keyword", + value: "no-clip", + }, + ], + }, + source: h.declaration("mask-clip", "padding-box, no-clip").toJSON(), + }); + + const element2 = ( +
+ ); + const style2 = Style.from(element2, device); + t.deepEqual(style2.computed("mask-clip").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + { + type: "keyword", + value: "fill-box", + }, + { + type: "keyword", + value: "border-box", + }, + ], + }, + source: h + .declaration("mask-clip", "view-box, fill-box, border-box") + .toJSON(), + }); +}); diff --git a/packages/alfa-style/test/property/mask-composite.spec.tsx b/packages/alfa-style/test/property/mask-composite.spec.tsx new file mode 100644 index 0000000000..0ae8597f19 --- /dev/null +++ b/packages/alfa-style/test/property/mask-composite.spec.tsx @@ -0,0 +1,72 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../dist/index.js"; + +const device = Device.standard(); + +test("initial value is add", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-composite").toJSON(), { + value: { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "add", + }, + ], + }, + source: null, + }); +}); + +test("#computed parses single keywords", (t) => { + for (const kw of ["add", "subtract", "intersect", "exclude"] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-composite").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: kw, + }, + ], + }, + source: h.declaration("mask-composite", kw).toJSON(), + }); + } +}); + +test("#computed parses multiple keywords", (t) => { + const element =
; + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-composite").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "add", + }, + { + type: "keyword", + value: "exclude", + }, + ], + }, + source: h.declaration("mask-composite", "add, exclude").toJSON(), + }); +}); diff --git a/packages/alfa-style/test/property/mask-image.spec.tsx b/packages/alfa-style/test/property/mask-image.spec.tsx new file mode 100644 index 0000000000..4e9181c494 --- /dev/null +++ b/packages/alfa-style/test/property/mask-image.spec.tsx @@ -0,0 +1,99 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../dist/index.js"; + +const device = Device.standard(); + +test("initial value is none", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-image").toJSON(), { + value: { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "none", + }, + ], + }, + source: null, + }); +}); + +test("#computed parses url value", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-image").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "url", + url: "masks.svg#mask1", // TODO: should be made absolute + }, + }, + ], + }, + source: h.declaration("mask-image", "url(masks.svg#mask1)").toJSON(), + }); +}); + +test("#computed parses linear-gradient value", (t) => { + const element = ( +
+ ); + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-image").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "gradient", + kind: "linear", + direction: { + type: "side", + side: "bottom", + }, + items: [ + { + color: { + color: "red", + format: "named", + type: "color", + }, + position: null, + type: "stop", + }, + { + color: { + color: "blue", + format: "named", + type: "color", + }, + position: null, + type: "stop", + }, + ], + repeats: false, + }, + }, + ], + }, + source: h.declaration("mask-image", "linear-gradient(red, blue)").toJSON(), + }); +}); diff --git a/packages/alfa-style/test/property/mask-mode.spec.tsx b/packages/alfa-style/test/property/mask-mode.spec.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/alfa-style/test/property/mask-origin.spec.tsx b/packages/alfa-style/test/property/mask-origin.spec.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/alfa-style/test/property/mask-size.spec.tsx b/packages/alfa-style/test/property/mask-size.spec.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/alfa-style/test/tsconfig.json b/packages/alfa-style/test/tsconfig.json index 38b0315c27..b7202ff56b 100644 --- a/packages/alfa-style/test/tsconfig.json +++ b/packages/alfa-style/test/tsconfig.json @@ -53,6 +53,14 @@ "./property/isolation.spec.tsx", "./property/line-height.spec.tsx", "./property/margin.spec.tsx", + "./property/mask-clip.spec.tsx", + "./property/mask-composite.spec.tsx", + "./property/mask-image.spec.tsx", + "./property/mask-mode.spec.tsx", + "./property/mask-origin.spec.tsx", + "./property/mask-position.spec.tsx", + "./property/mask-repeat.spec.tsx", + "./property/mask-size.spec.tsx", "./property/mix-blend-mode.spec.tsx", "./property/opacity.spec.tsx", "./property/outline.spec.tsx", From c183573554771cacee2072f5c7656ebcc1627c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:27:52 +0100 Subject: [PATCH 02/40] Add tests for mask-mode --- .../test/property/mask-mode.spec.tsx | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/alfa-style/test/property/mask-mode.spec.tsx b/packages/alfa-style/test/property/mask-mode.spec.tsx index e69de29bb2..2f226cfe90 100644 --- a/packages/alfa-style/test/property/mask-mode.spec.tsx +++ b/packages/alfa-style/test/property/mask-mode.spec.tsx @@ -0,0 +1,72 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../dist/index.js"; + +const device = Device.standard(); + +test("initial value is match-source", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-mode").toJSON(), { + value: { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "match-source", + }, + ], + }, + source: null, + }); +}); + +test("#computed parses single keywords", (t) => { + for (const kw of ["alpha", "luminance", "match-source"] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-mode").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: kw, + }, + ], + }, + source: h.declaration("mask-mode", kw).toJSON(), + }); + } +}); + +test("#computed parses multiple keywords", (t) => { + const element =
; + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-mode").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "alpha", + }, + { + type: "keyword", + value: "match-source", + }, + ], + }, + source: h.declaration("mask-mode", "alpha, match-source").toJSON(), + }); +}); From 3eb3e547d7463149708293b47f4f9f94eef4411b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:32:55 +0100 Subject: [PATCH 03/40] Add tests for mask-origin --- .../test/property/mask-origin.spec.tsx | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/packages/alfa-style/test/property/mask-origin.spec.tsx b/packages/alfa-style/test/property/mask-origin.spec.tsx index e69de29bb2..8dff80e9bf 100644 --- a/packages/alfa-style/test/property/mask-origin.spec.tsx +++ b/packages/alfa-style/test/property/mask-origin.spec.tsx @@ -0,0 +1,109 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../dist/index.js"; + +const device = Device.standard(); + +test("initial value is border-box", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-origin").toJSON(), { + value: { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "border-box", + }, + ], + }, + source: null, + }); +}); + +test("#computed parses single keywords", (t) => { + for (const kw of [ + "content-box", + "padding-box", + "border-box", + "fill-box", + "stroke-box", + "view-box", + ] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-origin").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: kw, + }, + ], + }, + source: h.declaration("mask-origin", kw).toJSON(), + }); + } +}); + +test("#computed parses multiple keywords", (t) => { + const element1 = ( +
+ ); + const style1 = Style.from(element1, device); + t.deepEqual(style1.computed("mask-origin").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "padding-box", + }, + { + type: "keyword", + value: "content-box", + }, + ], + }, + source: h.declaration("mask-origin", "padding-box, content-box").toJSON(), + }); + + const element2 = ( +
+ ); + const style2 = Style.from(element2, device); + t.deepEqual(style2.computed("mask-origin").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + { + type: "keyword", + value: "fill-box", + }, + { + type: "keyword", + value: "border-box", + }, + ], + }, + source: h + .declaration("mask-origin", "view-box, fill-box, border-box") + .toJSON(), + }); +}); From bcd169697df0fa4f5c48e5b16127425c72fd6347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:56:48 +0100 Subject: [PATCH 04/40] Add tests for mask-repeat --- .../test/property/mask-repeat.spec.tsx | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index e69de29bb2..38c9621788 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -0,0 +1,106 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../dist/index.js"; + +const device = Device.standard(); + +test("initial value is repeat", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: " ", + values: [ + { + type: "list", + values: [ + { + type: "keyword", + value: "repeat", + }, + ], + separator: " ", + }, + ], + }, + source: null, + }); +}); + +test("#computed parses single keywords", (t) => { + for (const kw of ["repeat-x", "repeat-y"] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: kw, + }, + ], + }, + source: h.declaration("mask-repeat", kw).toJSON(), + }); + } +}); + +test("#computed parses at most two space separated values", (t) => { + const element1 =
; + const style1 = Style.from(element1, device); + t.deepEqual(style1.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "repeat", + }, + { + type: "keyword", + value: "space", + }, + ], + }, + ], + }, + source: h.declaration("mask-repeat", "repeat space").toJSON(), + }); + + const element2 =
; + const style2 = Style.from(element2, device); + t.deepEqual(style2.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: " ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "repeat", + }, + ], + }, + ], + }, + source: null, + }); +}); From d8ece81f539f46497b1d15eef1b311fbcfdc9343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:31:28 +0100 Subject: [PATCH 05/40] Move types from coord-box.ts to box.ts --- packages/alfa-css/src/tsconfig.json | 1 - packages/alfa-css/src/value/box.ts | 55 +++++++++++++++ packages/alfa-css/src/value/coord-box.ts | 68 ------------------- packages/alfa-css/src/value/index.ts | 1 - packages/alfa-style/src/property/mask-clip.ts | 6 +- .../alfa-style/src/property/mask-origin.ts | 6 +- 6 files changed, 61 insertions(+), 76 deletions(-) delete mode 100644 packages/alfa-css/src/value/coord-box.ts diff --git a/packages/alfa-css/src/tsconfig.json b/packages/alfa-css/src/tsconfig.json index d909082cb8..68d3344dd9 100644 --- a/packages/alfa-css/src/tsconfig.json +++ b/packages/alfa-css/src/tsconfig.json @@ -32,7 +32,6 @@ "./syntax/token.ts", "./value/box.ts", "./value/contain.ts", - "./value/coord-box.ts", "./unit/converter.ts", "./unit/index.ts", "./unit/unit.ts", diff --git a/packages/alfa-css/src/value/box.ts b/packages/alfa-css/src/value/box.ts index c370ac60f7..f253f38fe0 100644 --- a/packages/alfa-css/src/value/box.ts +++ b/packages/alfa-css/src/value/box.ts @@ -66,4 +66,59 @@ export namespace Box { parseShape, Keyword.parse("fill-box", "stroke-box", "view-box"), ); + + /** + * {@link https://www.w3.org/TR/css-box-4/#typedef-visual-box} + */ + export type VisualBox = + | Keyword<"content-box"> + | Keyword<"padding-box"> + | Keyword<"border-box">; + + namespace VisualBox { + export type JSON = + | Keyword.JSON<"content-box"> + | Keyword.JSON<"padding-box"> + | Keyword.JSON<"border-box">; + } + + export const parseVisualBox: CSSParser = Keyword.parse( + "content-box", + "padding-box", + "border-box", + ); + + /** + * {@link https://www.w3.org/TR/css-box-4/#typedef-paint-box} + */ + export type PaintBox = + | VisualBox + | Keyword<"fill-box"> + | Keyword<"stroke-box">; + + namespace PaintBox { + export type JSON = + | VisualBox.JSON + | Keyword.JSON<"fill-box"> + | Keyword.JSON<"stroke-box">; + } + + export const parsePaintBox: CSSParser = either( + parseVisualBox, + Keyword.parse("fill-box", "stroke-box"), + ); + + /** + * {@link https://www.w3.org/TR/css-box-4/#typedef-coord-box} + */ + export type CoordBox = PaintBox | Keyword<"view-box">; + + export namespace CoordBox { + export type JSON = PaintBox.JSON | Keyword.JSON<"view-box">; + } + + export const parseCoordBox: CSSParser = either( + parsePaintBox, + Keyword.parse("view-box"), + ); } diff --git a/packages/alfa-css/src/value/coord-box.ts b/packages/alfa-css/src/value/coord-box.ts deleted file mode 100644 index c9a40795b8..0000000000 --- a/packages/alfa-css/src/value/coord-box.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Parser } from "@siteimprove/alfa-parser"; - -import { type Parser as CSSParser } from "../syntax/index.js"; - -import { Keyword } from "./textual/keyword.js"; - -const { either } = Parser; - -/** - * @internal - */ -type VisualBox = - | Keyword<"content-box"> - | Keyword<"padding-box"> - | Keyword<"border-box">; - -/** - * @internal - */ -namespace VisualBox { - export type JSON = - | Keyword.JSON<"content-box"> - | Keyword.JSON<"padding-box"> - | Keyword.JSON<"border-box">; - - export const parse: CSSParser = Keyword.parse( - "content-box", - "padding-box", - "border-box", - ); -} - -/** - * @internal - */ -type PaintBox = VisualBox | Keyword<"fill-box"> | Keyword<"stroke-box">; - -/** - * @internal - */ -namespace PaintBox { - export type JSON = - | VisualBox.JSON - | Keyword.JSON<"fill-box"> - | Keyword.JSON<"stroke-box">; - - export const parse: CSSParser = either( - VisualBox.parse, - Keyword.parse("fill-box", "stroke-box"), - ); -} - -/** - * @public - */ -export type CoordBox = PaintBox | Keyword<"view-box">; - -/** - * @public - */ -export namespace CoordBox { - export type JSON = PaintBox.JSON | Keyword.JSON<"view-box">; - - export const parse: CSSParser = either( - PaintBox.parse, - Keyword.parse("view-box"), - ); -} diff --git a/packages/alfa-css/src/value/index.ts b/packages/alfa-css/src/value/index.ts index 35b233fe08..552e9dcfde 100644 --- a/packages/alfa-css/src/value/index.ts +++ b/packages/alfa-css/src/value/index.ts @@ -1,6 +1,5 @@ export * from "./box.js"; export * from "./contain.js"; -export * from "./coord-box.js"; export * from "./collection/index.js"; export * from "./color/index.js"; export * from "./image/index.js"; diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index 189d494cde..c5cd61120a 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -1,11 +1,11 @@ import { Parser } from "@siteimprove/alfa-parser"; -import { CoordBox, Keyword, List } from "@siteimprove/alfa-css"; +import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; const { either } = Parser; -type Specified = List>; +type Specified = List>; type Computed = Specified; /** @@ -14,6 +14,6 @@ type Computed = Specified; */ export default Longhand.of( List.of([Keyword.of("border-box")]), - List.parseCommaSeparated(either(CoordBox.parse, Keyword.parse("no-clip"))), + List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))), (value) => value, ); diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index 494c6b57a8..607dbff61e 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -1,8 +1,8 @@ -import { CoordBox, Keyword, List } from "@siteimprove/alfa-css"; +import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -type Specified = List; +type Specified = List; type Computed = Specified; /** @@ -11,6 +11,6 @@ type Computed = Specified; */ export default Longhand.of( List.of([Keyword.of("border-box")]), - List.parseCommaSeparated(CoordBox.parse), + List.parseCommaSeparated(Box.parseCoordBox), (value) => value, ); From e49d0576d02ca31ee7f390e62ecf26d2d861f309 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 09:41:13 +0000 Subject: [PATCH 06/40] Extract API --- docs/review/api/alfa-css.api.md | 34 +++- docs/review/api/alfa-style.api.md | 255 ++++++++++++++++-------------- 2 files changed, 168 insertions(+), 121 deletions(-) diff --git a/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index 3d34cdd4a2..36abe1963f 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -201,10 +201,19 @@ export type Box = Keyword<"border-box"> | Keyword<"padding-box"> | Keyword<"cont // @public (undocumented) export namespace Box { // (undocumented) - export type Geometry = Shape | Keyword<"fill-box"> | Keyword<"stroke-box"> | Keyword<"view-box">; + export type CoordBox = PaintBox | Keyword<"view-box">; const // (undocumented) parse: Parser; // (undocumented) + export namespace CoordBox { + // (undocumented) + export type JSON = PaintBox.JSON | Keyword.JSON<"view-box">; + } + // (undocumented) + export type Geometry = Shape | Keyword<"fill-box"> | Keyword<"stroke-box"> | Keyword<"view-box">; + const // (undocumented) + parseShape: Parser; + // (undocumented) export namespace Geometry { // (undocumented) export type JSON = Shape.JSON | Keyword.JSON<"fill-box"> | Keyword.JSON<"stroke-box"> | Keyword.JSON<"view-box">; @@ -212,7 +221,16 @@ export namespace Box { // (undocumented) export type JSON = Keyword.JSON<"border-box"> | Keyword.JSON<"padding-box"> | Keyword.JSON<"content-box">; const // (undocumented) - parseShape: Parser; + parseGeometry: Parser; + // (undocumented) + export type PaintBox = VisualBox | Keyword<"fill-box"> | Keyword<"stroke-box">; + // (undocumented) + export namespace PaintBox { + // (undocumented) + export type JSON = VisualBox.JSON | Keyword.JSON<"fill-box"> | Keyword.JSON<"stroke-box">; + } + const // (undocumented) + parseVisualBox: Parser; // (undocumented) export type Shape = Box | Keyword<"margin-box">; // (undocumented) @@ -221,7 +239,17 @@ export namespace Box { export type JSON = Box.JSON | Keyword.JSON<"margin-box">; } const // (undocumented) - parseGeometry: Parser; + parsePaintBox: Parser; + // (undocumented) + export type VisualBox = Keyword<"content-box"> | Keyword<"padding-box"> | Keyword<"border-box">; + // (undocumented) + export namespace VisualBox { + // (undocumented) + export type JSON = Keyword.JSON<"content-box"> | Keyword.JSON<"padding-box"> | Keyword.JSON<"border-box">; + } + const // (undocumented) + parseCoordBox: Parser; + {}; } // Warning: (ae-forgotten-export) The symbol "BasicShape" needs to be exported by the entry point index.d.ts diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index c6dfaff6d3..ea57630ea5 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -17,6 +17,7 @@ import { Device } from '@siteimprove/alfa-device'; import { Element } from '@siteimprove/alfa-dom'; import { Equatable } from '@siteimprove/alfa-equatable'; import type { Functor } from '@siteimprove/alfa-functor'; +import { Gradient } from '@siteimprove/alfa-css'; import { Image } from '@siteimprove/alfa-css'; import { Integer } from '@siteimprove/alfa-css'; import { Iterable as Iterable_2 } from '@siteimprove/alfa-iterable'; @@ -56,6 +57,8 @@ import { Specified as Specified_13 } from './property/font-stretch.js'; import { Specified as Specified_14 } from './property/font-variant-east-asian.js'; import { Specified as Specified_15 } from './property/font-variant-ligatures.js'; import { Specified as Specified_16 } from './property/font-variant-numeric.js'; +import { Specified as Specified_17 } from './property/mask-position.js'; +import { Specified as Specified_18 } from './property/mask-size.js'; import { Specified as Specified_2 } from './property/background-image.js'; import { Specified as Specified_3 } from './property/background-position-x.js'; import { Specified as Specified_4 } from './property/background-position-y.js'; @@ -224,6 +227,14 @@ export namespace Longhands { readonly "margin-left": Longhand, Length | Percentage | Keyword<"auto">>; readonly "margin-right": Longhand, Length | Percentage | Keyword<"auto">>; readonly "margin-top": Longhand, Length | Percentage | Keyword<"auto">>; + readonly "mask-clip": Longhand>, List>>; + readonly "mask-composite": Longhand | Keyword<"subtract"> | Keyword<"intersect"> | Keyword<"exclude">>, List | Keyword<"subtract"> | Keyword<"intersect"> | Keyword<"exclude">>>; + readonly "mask-image": Longhand | URL | Image>, List | URL | Image>>; + readonly "mask-mode": Longhand | Keyword<"luminance"> | Keyword<"match-source">>, List | Keyword<"luminance"> | Keyword<"match-source">>>; + readonly "mask-origin": Longhand, List>; + readonly "mask-position": Longhand, List>>; + readonly "mask-repeat": Longhand | Keyword<"repeat-y"> | List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">>>, List | Keyword<"repeat-y"> | List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">>>>; + readonly "mask-size": Longhand, List | Keyword<"contain"> | Tuple<[LengthPercentage | Keyword<"auto">, LengthPercentage | Keyword<"auto">]>>>; readonly "min-height": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "min-width": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "mix-blend-mode": Longhand, Keyword.ToKeywords<"screen" | "color" | "hue" | "saturation" | "normal" | "multiply" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "luminosity" | "plus-darker" | "plus-lighter">>; @@ -499,124 +510,132 @@ export namespace Value { // Warnings were encountered during analysis: // -// src/longhands.ts:188:7 - (ae-incompatible-release-tags) The symbol ""background-attachment"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:189:7 - (ae-incompatible-release-tags) The symbol ""background-clip"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:190:7 - (ae-incompatible-release-tags) The symbol ""background-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:191:7 - (ae-incompatible-release-tags) The symbol ""background-image"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:192:7 - (ae-incompatible-release-tags) The symbol ""background-origin"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:193:7 - (ae-incompatible-release-tags) The symbol ""background-position-x"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:194:7 - (ae-incompatible-release-tags) The symbol ""background-position-y"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:195:7 - (ae-incompatible-release-tags) The symbol ""background-repeat-x"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:196:7 - (ae-incompatible-release-tags) The symbol ""background-repeat-y"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:197:7 - (ae-incompatible-release-tags) The symbol ""background-size"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:198:7 - (ae-incompatible-release-tags) The symbol ""border-block-end-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:199:7 - (ae-incompatible-release-tags) The symbol ""border-block-end-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:200:7 - (ae-incompatible-release-tags) The symbol ""border-block-end-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:201:7 - (ae-incompatible-release-tags) The symbol ""border-block-start-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:202:7 - (ae-incompatible-release-tags) The symbol ""border-block-start-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:203:7 - (ae-incompatible-release-tags) The symbol ""border-block-start-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:204:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:205:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-left-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:206:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-right-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:207:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:208:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:209:7 - (ae-incompatible-release-tags) The symbol ""border-collapse"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:210:7 - (ae-incompatible-release-tags) The symbol ""border-end-end-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:211:7 - (ae-incompatible-release-tags) The symbol ""border-end-start-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:212:7 - (ae-incompatible-release-tags) The symbol ""border-image-outset"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:213:7 - (ae-incompatible-release-tags) The symbol ""border-image-repeat"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:214:7 - (ae-incompatible-release-tags) The symbol ""border-image-slice"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:215:7 - (ae-incompatible-release-tags) The symbol ""border-image-source"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:216:7 - (ae-incompatible-release-tags) The symbol ""border-image-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:217:7 - (ae-incompatible-release-tags) The symbol ""border-inline-end-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:218:7 - (ae-incompatible-release-tags) The symbol ""border-inline-end-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:219:7 - (ae-incompatible-release-tags) The symbol ""border-inline-end-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:220:7 - (ae-incompatible-release-tags) The symbol ""border-inline-start-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:221:7 - (ae-incompatible-release-tags) The symbol ""border-inline-start-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:222:7 - (ae-incompatible-release-tags) The symbol ""border-inline-start-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:223:7 - (ae-incompatible-release-tags) The symbol ""border-left-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:224:7 - (ae-incompatible-release-tags) The symbol ""border-left-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:225:7 - (ae-incompatible-release-tags) The symbol ""border-left-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:226:7 - (ae-incompatible-release-tags) The symbol ""border-right-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:227:7 - (ae-incompatible-release-tags) The symbol ""border-right-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:228:7 - (ae-incompatible-release-tags) The symbol ""border-right-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:229:7 - (ae-incompatible-release-tags) The symbol ""border-start-end-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:230:7 - (ae-incompatible-release-tags) The symbol ""border-start-start-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:231:7 - (ae-incompatible-release-tags) The symbol ""border-top-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:232:7 - (ae-incompatible-release-tags) The symbol ""border-top-left-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:233:7 - (ae-incompatible-release-tags) The symbol ""border-top-right-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:234:7 - (ae-incompatible-release-tags) The symbol ""border-top-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:235:7 - (ae-incompatible-release-tags) The symbol ""border-top-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:236:7 - (ae-incompatible-release-tags) The symbol "bottom" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:237:7 - (ae-incompatible-release-tags) The symbol ""box-shadow"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:238:7 - (ae-incompatible-release-tags) The symbol ""clip-path"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:239:7 - (ae-incompatible-release-tags) The symbol "clip" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:240:7 - (ae-incompatible-release-tags) The symbol "color" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:241:7 - (ae-incompatible-release-tags) The symbol "contain" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:242:7 - (ae-incompatible-release-tags) The symbol ""container-type"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:243:7 - (ae-incompatible-release-tags) The symbol "cursor" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:244:7 - (ae-incompatible-release-tags) The symbol "display" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:245:7 - (ae-incompatible-release-tags) The symbol ""flex-direction"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:246:7 - (ae-incompatible-release-tags) The symbol ""flex-wrap"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:247:7 - (ae-incompatible-release-tags) The symbol "float" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:248:7 - (ae-incompatible-release-tags) The symbol ""font-family"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:249:7 - (ae-incompatible-release-tags) The symbol ""font-size"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:250:7 - (ae-incompatible-release-tags) The symbol ""font-stretch"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:251:7 - (ae-incompatible-release-tags) The symbol ""font-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:252:7 - (ae-incompatible-release-tags) The symbol ""font-variant-caps"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:253:7 - (ae-incompatible-release-tags) The symbol ""font-variant-east-asian"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:254:7 - (ae-incompatible-release-tags) The symbol ""font-variant-ligatures"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:255:7 - (ae-incompatible-release-tags) The symbol ""font-variant-numeric"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:256:7 - (ae-incompatible-release-tags) The symbol ""font-variant-position"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:257:7 - (ae-incompatible-release-tags) The symbol ""font-weight"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:258:7 - (ae-incompatible-release-tags) The symbol "height" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:259:7 - (ae-incompatible-release-tags) The symbol ""inset-block-end"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:260:7 - (ae-incompatible-release-tags) The symbol ""inset-block-start"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:261:7 - (ae-incompatible-release-tags) The symbol ""inset-inline-end"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:262:7 - (ae-incompatible-release-tags) The symbol ""inset-inline-start"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:263:7 - (ae-incompatible-release-tags) The symbol "isolation" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:264:7 - (ae-incompatible-release-tags) The symbol "left" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:265:7 - (ae-incompatible-release-tags) The symbol ""letter-spacing"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:266:7 - (ae-incompatible-release-tags) The symbol ""line-height"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:267:7 - (ae-incompatible-release-tags) The symbol ""margin-bottom"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:268:7 - (ae-incompatible-release-tags) The symbol ""margin-left"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:269:7 - (ae-incompatible-release-tags) The symbol ""margin-right"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:270:7 - (ae-incompatible-release-tags) The symbol ""margin-top"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:271:7 - (ae-incompatible-release-tags) The symbol ""min-height"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:272:7 - (ae-incompatible-release-tags) The symbol ""min-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:273:7 - (ae-incompatible-release-tags) The symbol ""mix-blend-mode"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:274:7 - (ae-incompatible-release-tags) The symbol "opacity" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:275:7 - (ae-incompatible-release-tags) The symbol ""outline-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:276:7 - (ae-incompatible-release-tags) The symbol ""outline-offset"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:277:7 - (ae-incompatible-release-tags) The symbol ""outline-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:278:7 - (ae-incompatible-release-tags) The symbol ""outline-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:279:7 - (ae-incompatible-release-tags) The symbol ""overflow-x"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:280:7 - (ae-incompatible-release-tags) The symbol ""overflow-y"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:281:7 - (ae-incompatible-release-tags) The symbol "perspective" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:282:7 - (ae-incompatible-release-tags) The symbol ""pointer-events"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:283:7 - (ae-incompatible-release-tags) The symbol "position" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:284:7 - (ae-incompatible-release-tags) The symbol "right" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:285:7 - (ae-incompatible-release-tags) The symbol "rotate" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:286:7 - (ae-incompatible-release-tags) The symbol "scale" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:287:7 - (ae-incompatible-release-tags) The symbol ""text-align"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:288:7 - (ae-incompatible-release-tags) The symbol ""text-decoration-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:289:7 - (ae-incompatible-release-tags) The symbol ""text-decoration-line"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:290:7 - (ae-incompatible-release-tags) The symbol ""text-decoration-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:291:7 - (ae-incompatible-release-tags) The symbol ""text-decoration-thickness"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:292:7 - (ae-incompatible-release-tags) The symbol ""text-indent"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:293:7 - (ae-incompatible-release-tags) The symbol ""text-overflow"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:294:7 - (ae-incompatible-release-tags) The symbol ""text-shadow"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:295:7 - (ae-incompatible-release-tags) The symbol ""text-transform"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:296:7 - (ae-incompatible-release-tags) The symbol "top" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:297:7 - (ae-incompatible-release-tags) The symbol "transform" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:298:7 - (ae-incompatible-release-tags) The symbol "translate" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:299:7 - (ae-incompatible-release-tags) The symbol ""vertical-align"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:300:7 - (ae-incompatible-release-tags) The symbol "visibility" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:301:7 - (ae-incompatible-release-tags) The symbol ""white-space"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:302:7 - (ae-incompatible-release-tags) The symbol "width" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:303:7 - (ae-incompatible-release-tags) The symbol ""will-change"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:304:7 - (ae-incompatible-release-tags) The symbol ""word-spacing"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/longhands.ts:305:7 - (ae-incompatible-release-tags) The symbol ""z-index"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:196:7 - (ae-incompatible-release-tags) The symbol ""background-attachment"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:197:7 - (ae-incompatible-release-tags) The symbol ""background-clip"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:198:7 - (ae-incompatible-release-tags) The symbol ""background-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:199:7 - (ae-incompatible-release-tags) The symbol ""background-image"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:200:7 - (ae-incompatible-release-tags) The symbol ""background-origin"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:201:7 - (ae-incompatible-release-tags) The symbol ""background-position-x"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:202:7 - (ae-incompatible-release-tags) The symbol ""background-position-y"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:203:7 - (ae-incompatible-release-tags) The symbol ""background-repeat-x"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:204:7 - (ae-incompatible-release-tags) The symbol ""background-repeat-y"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:205:7 - (ae-incompatible-release-tags) The symbol ""background-size"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:206:7 - (ae-incompatible-release-tags) The symbol ""border-block-end-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:207:7 - (ae-incompatible-release-tags) The symbol ""border-block-end-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:208:7 - (ae-incompatible-release-tags) The symbol ""border-block-end-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:209:7 - (ae-incompatible-release-tags) The symbol ""border-block-start-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:210:7 - (ae-incompatible-release-tags) The symbol ""border-block-start-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:211:7 - (ae-incompatible-release-tags) The symbol ""border-block-start-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:212:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:213:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-left-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:214:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-right-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:215:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:216:7 - (ae-incompatible-release-tags) The symbol ""border-bottom-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:217:7 - (ae-incompatible-release-tags) The symbol ""border-collapse"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:218:7 - (ae-incompatible-release-tags) The symbol ""border-end-end-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:219:7 - (ae-incompatible-release-tags) The symbol ""border-end-start-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:220:7 - (ae-incompatible-release-tags) The symbol ""border-image-outset"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:221:7 - (ae-incompatible-release-tags) The symbol ""border-image-repeat"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:222:7 - (ae-incompatible-release-tags) The symbol ""border-image-slice"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:223:7 - (ae-incompatible-release-tags) The symbol ""border-image-source"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:224:7 - (ae-incompatible-release-tags) The symbol ""border-image-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:225:7 - (ae-incompatible-release-tags) The symbol ""border-inline-end-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:226:7 - (ae-incompatible-release-tags) The symbol ""border-inline-end-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:227:7 - (ae-incompatible-release-tags) The symbol ""border-inline-end-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:228:7 - (ae-incompatible-release-tags) The symbol ""border-inline-start-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:229:7 - (ae-incompatible-release-tags) The symbol ""border-inline-start-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:230:7 - (ae-incompatible-release-tags) The symbol ""border-inline-start-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:231:7 - (ae-incompatible-release-tags) The symbol ""border-left-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:232:7 - (ae-incompatible-release-tags) The symbol ""border-left-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:233:7 - (ae-incompatible-release-tags) The symbol ""border-left-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:234:7 - (ae-incompatible-release-tags) The symbol ""border-right-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:235:7 - (ae-incompatible-release-tags) The symbol ""border-right-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:236:7 - (ae-incompatible-release-tags) The symbol ""border-right-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:237:7 - (ae-incompatible-release-tags) The symbol ""border-start-end-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:238:7 - (ae-incompatible-release-tags) The symbol ""border-start-start-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:239:7 - (ae-incompatible-release-tags) The symbol ""border-top-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:240:7 - (ae-incompatible-release-tags) The symbol ""border-top-left-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:241:7 - (ae-incompatible-release-tags) The symbol ""border-top-right-radius"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:242:7 - (ae-incompatible-release-tags) The symbol ""border-top-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:243:7 - (ae-incompatible-release-tags) The symbol ""border-top-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:244:7 - (ae-incompatible-release-tags) The symbol "bottom" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:245:7 - (ae-incompatible-release-tags) The symbol ""box-shadow"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:246:7 - (ae-incompatible-release-tags) The symbol ""clip-path"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:247:7 - (ae-incompatible-release-tags) The symbol "clip" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:248:7 - (ae-incompatible-release-tags) The symbol "color" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:249:7 - (ae-incompatible-release-tags) The symbol "contain" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:250:7 - (ae-incompatible-release-tags) The symbol ""container-type"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:251:7 - (ae-incompatible-release-tags) The symbol "cursor" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:252:7 - (ae-incompatible-release-tags) The symbol "display" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:253:7 - (ae-incompatible-release-tags) The symbol ""flex-direction"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:254:7 - (ae-incompatible-release-tags) The symbol ""flex-wrap"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:255:7 - (ae-incompatible-release-tags) The symbol "float" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:256:7 - (ae-incompatible-release-tags) The symbol ""font-family"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:257:7 - (ae-incompatible-release-tags) The symbol ""font-size"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:258:7 - (ae-incompatible-release-tags) The symbol ""font-stretch"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:259:7 - (ae-incompatible-release-tags) The symbol ""font-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:260:7 - (ae-incompatible-release-tags) The symbol ""font-variant-caps"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:261:7 - (ae-incompatible-release-tags) The symbol ""font-variant-east-asian"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:262:7 - (ae-incompatible-release-tags) The symbol ""font-variant-ligatures"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:263:7 - (ae-incompatible-release-tags) The symbol ""font-variant-numeric"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:264:7 - (ae-incompatible-release-tags) The symbol ""font-variant-position"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:265:7 - (ae-incompatible-release-tags) The symbol ""font-weight"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:266:7 - (ae-incompatible-release-tags) The symbol "height" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:267:7 - (ae-incompatible-release-tags) The symbol ""inset-block-end"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:268:7 - (ae-incompatible-release-tags) The symbol ""inset-block-start"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:269:7 - (ae-incompatible-release-tags) The symbol ""inset-inline-end"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:270:7 - (ae-incompatible-release-tags) The symbol ""inset-inline-start"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:271:7 - (ae-incompatible-release-tags) The symbol "isolation" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:272:7 - (ae-incompatible-release-tags) The symbol "left" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:273:7 - (ae-incompatible-release-tags) The symbol ""letter-spacing"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:274:7 - (ae-incompatible-release-tags) The symbol ""line-height"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:275:7 - (ae-incompatible-release-tags) The symbol ""margin-bottom"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:276:7 - (ae-incompatible-release-tags) The symbol ""margin-left"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:277:7 - (ae-incompatible-release-tags) The symbol ""margin-right"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:278:7 - (ae-incompatible-release-tags) The symbol ""margin-top"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:279:7 - (ae-incompatible-release-tags) The symbol ""mask-clip"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:280:7 - (ae-incompatible-release-tags) The symbol ""mask-composite"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:281:7 - (ae-incompatible-release-tags) The symbol ""mask-image"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:282:7 - (ae-incompatible-release-tags) The symbol ""mask-mode"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:283:7 - (ae-incompatible-release-tags) The symbol ""mask-origin"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:284:7 - (ae-incompatible-release-tags) The symbol ""mask-position"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:285:7 - (ae-incompatible-release-tags) The symbol ""mask-repeat"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:286:7 - (ae-incompatible-release-tags) The symbol ""mask-size"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:287:7 - (ae-incompatible-release-tags) The symbol ""min-height"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:288:7 - (ae-incompatible-release-tags) The symbol ""min-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:289:7 - (ae-incompatible-release-tags) The symbol ""mix-blend-mode"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:290:7 - (ae-incompatible-release-tags) The symbol "opacity" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:291:7 - (ae-incompatible-release-tags) The symbol ""outline-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:292:7 - (ae-incompatible-release-tags) The symbol ""outline-offset"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:293:7 - (ae-incompatible-release-tags) The symbol ""outline-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:294:7 - (ae-incompatible-release-tags) The symbol ""outline-width"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:295:7 - (ae-incompatible-release-tags) The symbol ""overflow-x"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:296:7 - (ae-incompatible-release-tags) The symbol ""overflow-y"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:297:7 - (ae-incompatible-release-tags) The symbol "perspective" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:298:7 - (ae-incompatible-release-tags) The symbol ""pointer-events"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:299:7 - (ae-incompatible-release-tags) The symbol "position" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:300:7 - (ae-incompatible-release-tags) The symbol "right" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:301:7 - (ae-incompatible-release-tags) The symbol "rotate" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:302:7 - (ae-incompatible-release-tags) The symbol "scale" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:303:7 - (ae-incompatible-release-tags) The symbol ""text-align"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:304:7 - (ae-incompatible-release-tags) The symbol ""text-decoration-color"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:305:7 - (ae-incompatible-release-tags) The symbol ""text-decoration-line"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:306:7 - (ae-incompatible-release-tags) The symbol ""text-decoration-style"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:307:7 - (ae-incompatible-release-tags) The symbol ""text-decoration-thickness"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:308:7 - (ae-incompatible-release-tags) The symbol ""text-indent"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:309:7 - (ae-incompatible-release-tags) The symbol ""text-overflow"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:310:7 - (ae-incompatible-release-tags) The symbol ""text-shadow"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:311:7 - (ae-incompatible-release-tags) The symbol ""text-transform"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:312:7 - (ae-incompatible-release-tags) The symbol "top" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:313:7 - (ae-incompatible-release-tags) The symbol "transform" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:314:7 - (ae-incompatible-release-tags) The symbol "translate" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:315:7 - (ae-incompatible-release-tags) The symbol ""vertical-align"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:316:7 - (ae-incompatible-release-tags) The symbol "visibility" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:317:7 - (ae-incompatible-release-tags) The symbol ""white-space"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:318:7 - (ae-incompatible-release-tags) The symbol "width" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:319:7 - (ae-incompatible-release-tags) The symbol ""will-change"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:320:7 - (ae-incompatible-release-tags) The symbol ""word-spacing"" is marked as @public, but its signature references "Longhand" which is marked as @internal +// src/longhands.ts:321:7 - (ae-incompatible-release-tags) The symbol ""z-index"" is marked as @public, but its signature references "Longhand" which is marked as @internal // src/shorthands.ts:47:14 - (ae-incompatible-release-tags) The symbol "background" is marked as @public, but its signature references "Shorthand" which is marked as @internal // src/shorthands.ts:48:14 - (ae-incompatible-release-tags) The symbol ""background-position"" is marked as @public, but its signature references "Shorthand" which is marked as @internal // src/shorthands.ts:49:14 - (ae-incompatible-release-tags) The symbol ""background-repeat"" is marked as @public, but its signature references "Shorthand" which is marked as @internal From a4853eb930d9511fbbef6e918c74ff0a2456547d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:48:57 +0100 Subject: [PATCH 07/40] Fix knip warnings --- packages/alfa-style/src/property/mask-origin.ts | 1 + packages/alfa-style/src/property/mask-position.ts | 2 +- packages/alfa-style/src/property/mask-size.ts | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index 607dbff61e..614bc70bc2 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -7,6 +7,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-origin} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 001b76e2f6..35d5a22d18 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -24,7 +24,7 @@ const parse = List.parseCommaSeparated(Position.Component.parseHorizontal); /** * @internal */ -export const initialItem: Computed.Item = Position.Side.of( +const initialItem: Computed.Item = Position.Side.of( Keyword.of("left"), Percentage.of(0), ); diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index 29a17600e2..e7bd93be45 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -45,7 +45,7 @@ const parseDimension = either(LengthPercentage.parse, Keyword.parse("auto")); /** * @internal */ -export const parse = either( +const parse = either( map( pair( parseDimension, @@ -63,7 +63,7 @@ const parseList = List.parseCommaSeparated(parse); /** * @internal */ -export const initialItem = Tuple.of(Keyword.of("auto"), Keyword.of("auto")); +const initialItem = Tuple.of(Keyword.of("auto"), Keyword.of("auto")); /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-size} From 983ee0170a3bec6d8491aa152b0a29ca250f835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:50:42 +0100 Subject: [PATCH 08/40] Implement mask-repeat --- packages/alfa-style/src/property/mask-clip.ts | 1 + .../alfa-style/src/property/mask-composite.ts | 1 + .../alfa-style/src/property/mask-image.ts | 1 + packages/alfa-style/src/property/mask-mode.ts | 1 + .../alfa-style/src/property/mask-position.ts | 1 + .../alfa-style/src/property/mask-repeat.ts | 47 +++++- packages/alfa-style/src/property/mask-size.ts | 1 + .../test/property/mask-repeat.spec.tsx | 156 +++++++++++++++++- 8 files changed, 204 insertions(+), 5 deletions(-) diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index c5cd61120a..7162ffab61 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -10,6 +10,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-clip} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index 6ec11918e0..72f6cd67e9 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -19,6 +19,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-image.ts b/packages/alfa-style/src/property/mask-image.ts index 808c9b3265..f60cd1438a 100644 --- a/packages/alfa-style/src/property/mask-image.ts +++ b/packages/alfa-style/src/property/mask-image.ts @@ -22,6 +22,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index 22ab9cc2e1..cc47e9e392 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -17,6 +17,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-mode} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 35d5a22d18..0d70a883d0 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -32,6 +32,7 @@ const initialItem: Computed.Item = Position.Side.of( // TODO: Copied from background-position-x.ts. Adjust for mask-position. /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index 6c951af880..f9c451b8ff 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -2,6 +2,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; +import { Value } from "../value.js"; const { either } = Parser; @@ -27,11 +28,51 @@ type Specified = List; type Computed = Specified; /** - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat} + * {@link https://drafts.fxtf.org/css-masking/#the-mask-repeat} + * + * @privateRemarks + * The computed value depends on the number of layers. + * A layer is created for each of the comma separated values for `mask-image`. + * + * If there are more values than layers, the excess layers are discarded. + * Otherwise, the values must be repeated until the numbers match. + * + * See {@link https://drafts.fxtf.org/css-masking/#layering}. + * + * @privateRemarks + * The spec says that the computed value "Consists of: two keywords, one per dimension", + * which could be taken to mean that the one-keyword shorthand values should be expanded to their two-keyword longhands, + * e.g. `repeat-x` would be expanded to `repeat no-repeat` in the computed style, + * but that is not the current behavior observed in the Dev Tools of Chrome and Firefox. + * We mimic the behavior of the browser and do no expand the shorthands. + * * @internal */ export default Longhand.of( - List.of([List.of([Keyword.of("repeat")])]), + List.of([List.of([Keyword.of("repeat")], " ")], ", "), List.parseCommaSeparated(repeatStyle), - (value) => value, + (value, style) => { + const numberOfLayers = Math.max( + style.computed("mask-image").value.values.length, + 1, + ); + + const numberOfValues = value.value.values.length; + if (numberOfValues === numberOfLayers) { + return value; + } + + return Value.of( + List.of( + (numberOfLayers < numberOfValues + ? value.value.values + : Array(Math.ceil(numberOfLayers / numberOfValues)) + .fill(value.value.values) + .flat() + ).slice(0, numberOfLayers), + ", ", + ), + value.source, + ); + }, ); diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index e7bd93be45..52cb21dbde 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -67,6 +67,7 @@ const initialItem = Tuple.of(Keyword.of("auto"), Keyword.of("auto")); /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-size} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index 38c9621788..d51009db67 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -15,7 +15,7 @@ test("initial value is repeat", (t) => { t.deepEqual(style.computed("mask-repeat").toJSON(), { value: { type: "list", - separator: " ", + separator: ", ", values: [ { type: "list", @@ -53,6 +53,32 @@ test("#computed parses single keywords", (t) => { source: h.declaration("mask-repeat", kw).toJSON(), }); } + + for (const kw of ["repeat", "space", "round", "no-repeat"] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: kw + } + ], + }, + ], + }, + source: h.declaration("mask-repeat", kw).toJSON(), + }); + } }); test("#computed parses at most two space separated values", (t) => { @@ -87,7 +113,7 @@ test("#computed parses at most two space separated values", (t) => { t.deepEqual(style2.computed("mask-repeat").toJSON(), { value: { type: "list", - separator: " ", + separator: ", ", values: [ { type: "list", @@ -104,3 +130,129 @@ test("#computed parses at most two space separated values", (t) => { source: null, }); }); + +test("#computed parses mutiple layers", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "round" + }, + { + type: "keyword", + value: "repeat" + } + ], + }, + { + type: "list", + separator: " ", + values: [ + { + + type: "keyword", + value: "space" + } + ] + } + ], + }, + source: h.declaration("mask-repeat", "round repeat, space").toJSON(), + }); +}); + +test("#computed discards excess values when there are more values than layers", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "round" + }, + { + type: "keyword", + value: "repeat" + } + ], + }, + ], + }, + source: h.declaration("mask-repeat", "round repeat, space").toJSON(), + }); +}); + +test("#computed repeats values when there are more layers than values", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "round" + }, + { + type: "keyword", + value: "repeat" + } + ], + }, + { + type: "list", + separator: " ", + values: [ + { + + type: "keyword", + value: "space" + } + ] + }, + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "round" + }, + { + type: "keyword", + value: "repeat" + } + ], + }, + ], + }, + source: h.declaration("mask-repeat", "round repeat, space").toJSON(), + }); +}); From 43fb56b9e3d86e07225ec0dfd7657e1ccd13be0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:57:59 +0100 Subject: [PATCH 09/40] Extract common function --- packages/alfa-style/src/property/mask-clip.ts | 3 +- .../alfa-style/src/property/mask-repeat.ts | 40 ++----------- packages/alfa-style/src/property/mask.ts | 39 ++++++++++++ packages/alfa-style/src/tsconfig.json | 1 + .../test/property/mask-clip.spec.tsx | 60 +++++++++++++++---- .../test/property/mask-repeat.spec.tsx | 15 ++++- 6 files changed, 108 insertions(+), 50 deletions(-) create mode 100644 packages/alfa-style/src/property/mask.ts diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index 7162ffab61..43d0d1f227 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -2,6 +2,7 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; +import { matchLayers } from "./mask.js"; const { either } = Parser; @@ -16,5 +17,5 @@ type Computed = Specified; export default Longhand.of( List.of([Keyword.of("border-box")]), List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))), - (value) => value, + (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index f9c451b8ff..55b6d441d5 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -2,7 +2,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; -import { Value } from "../value.js"; +import { matchLayers } from "./mask.js"; const { either } = Parser; @@ -31,48 +31,16 @@ type Computed = Specified; * {@link https://drafts.fxtf.org/css-masking/#the-mask-repeat} * * @privateRemarks - * The computed value depends on the number of layers. - * A layer is created for each of the comma separated values for `mask-image`. - * - * If there are more values than layers, the excess layers are discarded. - * Otherwise, the values must be repeated until the numbers match. - * - * See {@link https://drafts.fxtf.org/css-masking/#layering}. - * - * @privateRemarks * The spec says that the computed value "Consists of: two keywords, one per dimension", * which could be taken to mean that the one-keyword shorthand values should be expanded to their two-keyword longhands, * e.g. `repeat-x` would be expanded to `repeat no-repeat` in the computed style, - * but that is not the current behavior observed in the Dev Tools of Chrome and Firefox. - * We mimic the behavior of the browser and do no expand the shorthands. + * but that is not the current behavior observed in the DevTools of Chrome and Firefox. + * We mimic the behavior of the browsers and do not expand the shorthands. * * @internal */ export default Longhand.of( List.of([List.of([Keyword.of("repeat")], " ")], ", "), List.parseCommaSeparated(repeatStyle), - (value, style) => { - const numberOfLayers = Math.max( - style.computed("mask-image").value.values.length, - 1, - ); - - const numberOfValues = value.value.values.length; - if (numberOfValues === numberOfLayers) { - return value; - } - - return Value.of( - List.of( - (numberOfLayers < numberOfValues - ? value.value.values - : Array(Math.ceil(numberOfLayers / numberOfValues)) - .fill(value.value.values) - .flat() - ).slice(0, numberOfLayers), - ", ", - ), - value.source, - ); - }, + (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask.ts b/packages/alfa-style/src/property/mask.ts new file mode 100644 index 0000000000..9c80491cee --- /dev/null +++ b/packages/alfa-style/src/property/mask.ts @@ -0,0 +1,39 @@ +import { List, type Value } from "@siteimprove/alfa-css"; + +import type { Style } from "../style.js"; + +/** + * {@link https://drafts.fxtf.org/css-masking/#layering}. + * + * @remarks + * The computed value depends on the number of layers. + * A layer is created for each of the comma separated values for `mask-image`. + * + * If there are more values than layers, the excess values are discarded. + * Otherwise, the values must be repeated + * until the number of values matches the number of layers. + */ +export function matchLayers( + value: List, + style: Style, +): List { + const numberOfLayers = Math.max( + style.computed("mask-image").value.values.length, + 1, + ); + + const numberOfValues = value.values.length; + if (numberOfValues === numberOfLayers) { + return value; + } + + return List.of( + (numberOfLayers < numberOfValues + ? value.values + : Array(Math.ceil(numberOfLayers / numberOfValues)) + .fill(value.values) + .flat() + ).slice(0, numberOfLayers), + ", ", + ); +} diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index 3f316be31f..80e803580f 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -158,6 +158,7 @@ "./property/mask-position.ts", "./property/mask-repeat.ts", "./property/mask-size.ts", + "./property/mask.ts", "./property/margin-bottom.ts", "./property/margin-left.ts", "./property/margin-right.ts", diff --git a/packages/alfa-style/test/property/mask-clip.spec.tsx b/packages/alfa-style/test/property/mask-clip.spec.tsx index c7a66a1f0f..31ece611f7 100644 --- a/packages/alfa-style/test/property/mask-clip.spec.tsx +++ b/packages/alfa-style/test/property/mask-clip.spec.tsx @@ -57,10 +57,15 @@ test("#computed parses single keywords", (t) => { } }); -test("#computed parses multiple keywords", (t) => { - const element1 =
; - const style1 = Style.from(element1, device); - t.deepEqual(style1.computed("mask-clip").toJSON(), { +test("#computed parses multiple layers", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-clip").toJSON(), { value: { type: "list", separator: ", ", @@ -78,11 +83,46 @@ test("#computed parses multiple keywords", (t) => { source: h.declaration("mask-clip", "padding-box, no-clip").toJSON(), }); - const element2 = ( -
+}); + +test("#computed discards excess values when there are more values than layers", (t) => { + const element = ( +
+ ); + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-clip").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + { + type: "keyword", + value: "fill-box", + }, + ], + }, + source: h + .declaration("mask-clip", "view-box, fill-box, border-box") + .toJSON(), + }); +}); + +test("#computed repeats values when there are more layers than values", (t) => { + const element = ( +
); - const style2 = Style.from(element2, device); - t.deepEqual(style2.computed("mask-clip").toJSON(), { + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-clip").toJSON(), { value: { type: "list", separator: ", ", @@ -97,12 +137,12 @@ test("#computed parses multiple keywords", (t) => { }, { type: "keyword", - value: "border-box", + value: "view-box", }, ], }, source: h - .declaration("mask-clip", "view-box, fill-box, border-box") + .declaration("mask-clip", "view-box, fill-box") .toJSON(), }); }); diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index d51009db67..cf5862c926 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -132,7 +132,10 @@ test("#computed parses at most two space separated values", (t) => { }); test("#computed parses mutiple layers", (t) => { - const element =
; + const element =
; const style = Style.from(element, device); @@ -173,7 +176,10 @@ test("#computed parses mutiple layers", (t) => { }); test("#computed discards excess values when there are more values than layers", (t) => { - const element =
; + const element =
; const style = Style.from(element, device); @@ -203,7 +209,10 @@ test("#computed discards excess values when there are more values than layers", }); test("#computed repeats values when there are more layers than values", (t) => { - const element =
; + const element =
; const style = Style.from(element, device); From 1b164b4734f963e9dbba10c89fe461292b21e59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:10:45 +0100 Subject: [PATCH 10/40] Remove empty test files --- packages/alfa-style/src/property/mask-repeat.ts | 2 +- packages/alfa-style/test/property/mask-position.spec.tsx | 0 packages/alfa-style/test/property/mask-size.spec.tsx | 0 packages/alfa-style/test/tsconfig.json | 2 -- 4 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 packages/alfa-style/test/property/mask-position.spec.tsx delete mode 100644 packages/alfa-style/test/property/mask-size.spec.tsx diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index 55b6d441d5..39e58a2b23 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -28,7 +28,7 @@ type Specified = List; type Computed = Specified; /** - * {@link https://drafts.fxtf.org/css-masking/#the-mask-repeat} + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-repeat} * * @privateRemarks * The spec says that the computed value "Consists of: two keywords, one per dimension", diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/alfa-style/test/property/mask-size.spec.tsx b/packages/alfa-style/test/property/mask-size.spec.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/alfa-style/test/tsconfig.json b/packages/alfa-style/test/tsconfig.json index a4415680a6..1ab9ec90d4 100644 --- a/packages/alfa-style/test/tsconfig.json +++ b/packages/alfa-style/test/tsconfig.json @@ -59,9 +59,7 @@ "./property/mask-image.spec.tsx", "./property/mask-mode.spec.tsx", "./property/mask-origin.spec.tsx", - "./property/mask-position.spec.tsx", "./property/mask-repeat.spec.tsx", - "./property/mask-size.spec.tsx", "./property/mix-blend-mode.spec.tsx", "./property/opacity.spec.tsx", "./property/outline.spec.tsx", From 24f2ea74ed39f14e48195c774ca21cbff9761e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:55:45 +0100 Subject: [PATCH 11/40] Handle layers in mask-composite --- .../alfa-style/src/property/mask-composite.ts | 5 +- .../test/property/mask-composite.spec.tsx | 79 ++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index 72f6cd67e9..fe1c1e0921 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -1,6 +1,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; +import { matchLayers } from "./mask.js"; type CompositingOperator = | Keyword<"add"> @@ -23,7 +24,7 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([Keyword.of("add")]), + List.of([Keyword.of("add")], ", "), List.parseCommaSeparated(compositingOperator), - (value) => value, + (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/test/property/mask-composite.spec.tsx b/packages/alfa-style/test/property/mask-composite.spec.tsx index 0ae8597f19..5bc7b84093 100644 --- a/packages/alfa-style/test/property/mask-composite.spec.tsx +++ b/packages/alfa-style/test/property/mask-composite.spec.tsx @@ -15,7 +15,7 @@ test("initial value is add", (t) => { t.deepEqual(style.computed("mask-composite").toJSON(), { value: { type: "list", - separator: " ", + separator: ", ", values: [ { type: "keyword", @@ -49,8 +49,77 @@ test("#computed parses single keywords", (t) => { } }); -test("#computed parses multiple keywords", (t) => { - const element =
; +test("#computed parses multiple layers", (t) => { + const element = ( +
+ ); + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-composite").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "add", + }, + { + type: "keyword", + value: "exclude", + }, + ], + }, + source: h.declaration("mask-composite", "add, exclude").toJSON(), + }); +}); + +test("#computed discards excess values when there are more values than layers", (t) => { + const element = ( +
+ ); + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-composite").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "add", + }, + { + type: "keyword", + value: "exclude", + }, + ], + }, + source: h.declaration("mask-composite", "add, exclude, intersect").toJSON(), + }); +}); + +test("#computed repeats values when there are more layers than values", (t) => { + const element = ( +
+ ); const style = Style.from(element, device); t.deepEqual(style.computed("mask-composite").toJSON(), { value: { @@ -65,6 +134,10 @@ test("#computed parses multiple keywords", (t) => { type: "keyword", value: "exclude", }, + { + type: "keyword", + value: "add", + }, ], }, source: h.declaration("mask-composite", "add, exclude").toJSON(), From 22813a7a26c54380f24ae717605133867b1bdf63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:03:29 +0100 Subject: [PATCH 12/40] Update TODO and fix link --- package.json | 1 + packages/alfa-style/src/property/mask-image.ts | 4 ++-- packages/alfa-style/test/property/mask-composite.spec.tsx | 2 ++ packages/alfa-style/test/property/mask-image.spec.tsx | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c719d4f7fc..a51f8776d0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build": "node --max-old-space-size=8192 scripts/build.mjs --pretty", "clean": "node scripts/clean.mjs --pretty", "test": "yarn build && vitest run --config ./config/vitest.config.ts", + "test:watch": "yarn build && vitest --config ./config/vitest.config.ts", "watch": "node scripts/watch.mjs --pretty", "extract": "node scripts/api-extractor.mjs --pretty", "document": "api-documenter generate -i docs/data/api -o docs/api", diff --git a/packages/alfa-style/src/property/mask-image.ts b/packages/alfa-style/src/property/mask-image.ts index f60cd1438a..2b0f8992fa 100644 --- a/packages/alfa-style/src/property/mask-image.ts +++ b/packages/alfa-style/src/property/mask-image.ts @@ -21,12 +21,12 @@ type Specified = List; type Computed = Specified; /** - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite} + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image} * * @internal */ export default Longhand.of( List.of([Keyword.of("none")]), List.parseCommaSeparated(maskReference), - (value) => value, // TODO: as specified, but with values made absolute + (value) => value, // TODO: How to resolve absolute URL? ); diff --git a/packages/alfa-style/test/property/mask-composite.spec.tsx b/packages/alfa-style/test/property/mask-composite.spec.tsx index 5bc7b84093..0a931218cb 100644 --- a/packages/alfa-style/test/property/mask-composite.spec.tsx +++ b/packages/alfa-style/test/property/mask-composite.spec.tsx @@ -120,7 +120,9 @@ test("#computed repeats values when there are more layers than values", (t) => { }} > ); + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-composite").toJSON(), { value: { type: "list", diff --git a/packages/alfa-style/test/property/mask-image.spec.tsx b/packages/alfa-style/test/property/mask-image.spec.tsx index 4e9181c494..976ddeac81 100644 --- a/packages/alfa-style/test/property/mask-image.spec.tsx +++ b/packages/alfa-style/test/property/mask-image.spec.tsx @@ -41,7 +41,7 @@ test("#computed parses url value", (t) => { type: "image", image: { type: "url", - url: "masks.svg#mask1", // TODO: should be made absolute + url: "masks.svg#mask1", }, }, ], From cfd68759205dff137748fd3a71c76feeb963a9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:17:24 +0100 Subject: [PATCH 13/40] Handle layers in mask-mode --- packages/alfa-style/src/property/mask-mode.ts | 5 +- .../test/property/mask-mode.spec.tsx | 81 ++++++++++++++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index cc47e9e392..0049920765 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -1,6 +1,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; +import { matchLayers } from "./mask.js"; type MaskingMode = | Keyword<"alpha"> @@ -21,7 +22,7 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([Keyword.of("match-source")]), + List.of([Keyword.of("match-source")], ", "), List.parseCommaSeparated(maskingMode), - (value) => value, + (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/test/property/mask-mode.spec.tsx b/packages/alfa-style/test/property/mask-mode.spec.tsx index 2f226cfe90..928ce668fc 100644 --- a/packages/alfa-style/test/property/mask-mode.spec.tsx +++ b/packages/alfa-style/test/property/mask-mode.spec.tsx @@ -15,7 +15,7 @@ test("initial value is match-source", (t) => { t.deepEqual(style.computed("mask-mode").toJSON(), { value: { type: "list", - separator: " ", + separator: ", ", values: [ { type: "keyword", @@ -49,9 +49,80 @@ test("#computed parses single keywords", (t) => { } }); -test("#computed parses multiple keywords", (t) => { - const element =
; +test("#computed parses multiple layers", (t) => { + const element = ( +
+ ); + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-mode").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "alpha", + }, + { + type: "keyword", + value: "match-source", + }, + ], + }, + source: h.declaration("mask-mode", "alpha, match-source").toJSON(), + }); +}); + +test("#computed discards excess values when there are more values than layers", (t) => { + const element = ( +
+ ); + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-mode").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "alpha", + }, + { + type: "keyword", + value: "match-source", + }, + ], + }, + source: h + .declaration("mask-mode", "alpha, match-source, luminance") + .toJSON(), + }); +}); + +test("#computed repeats values when there are more layers than values", (t) => { + const element = ( +
+ ); + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-mode").toJSON(), { value: { type: "list", @@ -65,6 +136,10 @@ test("#computed parses multiple keywords", (t) => { type: "keyword", value: "match-source", }, + { + type: "keyword", + value: "alpha", + }, ], }, source: h.declaration("mask-mode", "alpha, match-source").toJSON(), From 04761a985dcc0a2518a9495cbb5616779a71bc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:31:43 +0100 Subject: [PATCH 14/40] Handle layers in mask-origin --- .../alfa-style/src/property/mask-origin.ts | 5 +- .../test/property/mask-origin.spec.tsx | 81 +++++++++++++++---- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index 614bc70bc2..7811d6a6cd 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -1,6 +1,7 @@ import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; +import { matchLayers } from "./mask.js"; type Specified = List; type Computed = Specified; @@ -11,7 +12,7 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([Keyword.of("border-box")]), + List.of([Keyword.of("border-box")], ", "), List.parseCommaSeparated(Box.parseCoordBox), - (value) => value, + (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/test/property/mask-origin.spec.tsx b/packages/alfa-style/test/property/mask-origin.spec.tsx index 8dff80e9bf..0ff5bf7437 100644 --- a/packages/alfa-style/test/property/mask-origin.spec.tsx +++ b/packages/alfa-style/test/property/mask-origin.spec.tsx @@ -15,7 +15,7 @@ test("initial value is border-box", (t) => { t.deepEqual(style.computed("mask-origin").toJSON(), { value: { type: "list", - separator: " ", + separator: ", ", values: [ { type: "keyword", @@ -56,54 +56,101 @@ test("#computed parses single keywords", (t) => { } }); -test("#computed parses multiple keywords", (t) => { - const element1 = ( -
+test("#computed parses multiple layers", (t) => { + const element = ( +
); - const style1 = Style.from(element1, device); - t.deepEqual(style1.computed("mask-origin").toJSON(), { + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-origin").toJSON(), { value: { type: "list", separator: ", ", values: [ + { + type: "keyword", + value: "content-box", + }, { type: "keyword", value: "padding-box", }, + ], + }, + source: h.declaration("mask-origin", "content-box, padding-box").toJSON(), + }); +}); + +test("#computed discards excess values when there are more values than layers", (t) => { + const element = ( +
+ ); + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-origin").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ { type: "keyword", value: "content-box", }, + { + type: "keyword", + value: "padding-box", + }, ], }, - source: h.declaration("mask-origin", "padding-box, content-box").toJSON(), + source: h + .declaration("mask-origin", "content-box, padding-box, border-box") + .toJSON(), }); +}); - const element2 = ( -
+test("#computed repeats values when there are more layers than values", (t) => { + const element = ( +
); - const style2 = Style.from(element2, device); - t.deepEqual(style2.computed("mask-origin").toJSON(), { + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-origin").toJSON(), { value: { type: "list", separator: ", ", values: [ { type: "keyword", - value: "view-box", + value: "content-box", }, { type: "keyword", - value: "fill-box", + value: "padding-box", }, { type: "keyword", - value: "border-box", + value: "content-box", }, ], }, - source: h - .declaration("mask-origin", "view-box, fill-box, border-box") - .toJSON(), + source: h.declaration("mask-origin", "content-box, padding-box").toJSON(), }); }); From 949c703de0ba52b522afd846f8c2637f55c3b173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:58:59 +0100 Subject: [PATCH 15/40] Add mask-size --- packages/alfa-style/src/property/mask-size.ts | 105 +++----- .../test/property/mask-size.spec.tsx | 226 ++++++++++++++++++ packages/alfa-style/test/tsconfig.json | 1 + 3 files changed, 260 insertions(+), 72 deletions(-) create mode 100644 packages/alfa-style/test/property/mask-size.spec.tsx diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index 52cb21dbde..de416e7bb8 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -1,96 +1,57 @@ import { Keyword, - List, LengthPercentage, - Token, - Tuple, + List, + type Parser as CSSParser, } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; +import { matchLayers } from "./mask.js"; -const { map, either, option, pair, right } = Parser; - -type Specified = List; - -/** - * @internal - */ -export namespace Specified { - type Dimension = LengthPercentage | Keyword<"auto">; +const { either } = Parser; - export type Item = - | Tuple<[Dimension, Dimension]> - | Keyword<"cover"> - | Keyword<"contain">; -} - -type Computed = List; - -namespace Computed { - type Dimension = LengthPercentage.PartiallyResolved | Keyword<"auto">; - - export type Item = - | Tuple<[Dimension, Dimension]> - | Keyword<"cover"> - | Keyword<"contain">; -} - -/** - * @internal - */ -const parseDimension = either(LengthPercentage.parse, Keyword.parse("auto")); +type BgSize = + | List> + | Keyword<"cover"> + | Keyword<"contain">; -/** - * @internal - */ -const parse = either( - map( - pair( - parseDimension, - map(option(right(Token.parseWhitespace, parseDimension)), (y) => - y.getOrElse(() => Keyword.of("auto")), - ), - ), - ([x, y]) => Tuple.of(x, y), +const bgSize: CSSParser = either( + List.parseSpaceSeparated( + either(LengthPercentage.parse, Keyword.parse("auto")), + 1, + 2, ), - Keyword.parse("contain", "cover"), + Keyword.parse("cover", "contain"), ); -const parseList = List.parseCommaSeparated(parse); +type Specified = List; +type Computed = Specified; /** - * @internal - */ -const initialItem = Tuple.of(Keyword.of("auto"), Keyword.of("auto")); - -/** - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-size} + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-size} * * @internal */ export default Longhand.of( - List.of([initialItem], ", "), - parseList, + List.of([List.of([Keyword.of("auto")], " ")], ", "), + List.parseCommaSeparated(bgSize), (value, style) => value.map((sizes) => - sizes.map((size) => { - if (Keyword.isKeyword(size)) { - return size; - } - - const [x, y] = size.values; - const resolver = Resolver.length(style); - - return Tuple.of( - Keyword.isKeyword(x) - ? x - : LengthPercentage.partiallyResolve(resolver)(x), - Keyword.isKeyword(y) - ? y - : LengthPercentage.partiallyResolve(resolver)(y), - ); - }), + matchLayers( + sizes.map((size) => + Keyword.isKeyword(size) + ? size + : size.map((value) => + Keyword.isKeyword(value) + ? value + : LengthPercentage.partiallyResolve(Resolver.length(style))( + value, + ), + ), + ), + style, + ), ), ); diff --git a/packages/alfa-style/test/property/mask-size.spec.tsx b/packages/alfa-style/test/property/mask-size.spec.tsx new file mode 100644 index 0000000000..b72b9a5316 --- /dev/null +++ b/packages/alfa-style/test/property/mask-size.spec.tsx @@ -0,0 +1,226 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../dist/index.js"; + +const device = Device.standard(); + +test("initial value is auto", (t) => { + const element = ( +
+ ); + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-size").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "auto", + }, + ], + }, + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "auto", + }, + ], + }, + ], + }, + source: null, + }); +}); + +test("#computed parses single keywords", (t) => { + for (const kw of ["cover", "contain"] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-size").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: kw, + }, + ], + }, + source: h.declaration("mask-size", kw).toJSON(), + }); + } +}); + +test("#computed parses percentage width", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-size").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "percentage", + value: 0.5, + }, + ], + }, + ], + }, + source: h.declaration("mask-size", "50%").toJSON(), + }); +}); + +test("#computed resolves em width", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-size").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "length", + unit: "px", + value: 48, + }, + ], + }, + ], + }, + source: h.declaration("mask-size", "3em").toJSON(), + }); +}); + +test("#computed parses pixel width", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-size").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "length", + unit: "px", + value: 12, + }, + ], + }, + ], + }, + source: h.declaration("mask-size", "12px").toJSON(), + }); +}); + +test("#computed parses width and height", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-size").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "length", + unit: "px", + value: 48, + }, + { + type: "percentage", + value: 0.25, + }, + ], + }, + ], + }, + source: h.declaration("mask-size", "3em 25%").toJSON(), + }); +}); + +test("#computed parses multiple layers", (t) => { + const element = ( +
+ ); + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-size").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "percentage", + value: 0.5, + }, + ], + }, + { + type: "list", + separator: " ", + values: [ + { + type: "percentage", + value: 0.25, + }, + ], + }, + ], + }, + source: h.declaration("mask-size", "50%, 25%").toJSON(), + }); +}); diff --git a/packages/alfa-style/test/tsconfig.json b/packages/alfa-style/test/tsconfig.json index 1ab9ec90d4..0bfb6783cb 100644 --- a/packages/alfa-style/test/tsconfig.json +++ b/packages/alfa-style/test/tsconfig.json @@ -60,6 +60,7 @@ "./property/mask-mode.spec.tsx", "./property/mask-origin.spec.tsx", "./property/mask-repeat.spec.tsx", + "./property/mask-size.spec.tsx", "./property/mix-blend-mode.spec.tsx", "./property/opacity.spec.tsx", "./property/outline.spec.tsx", From d6745dfe515855d56109eeb6c01a9eda4f3070b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:57:27 +0100 Subject: [PATCH 16/40] Add mask-position --- .../alfa-style/src/property/mask-position.ts | 55 +++++++------------ .../test/property/mask-position.spec.tsx | 49 +++++++++++++++++ packages/alfa-style/test/tsconfig.json | 1 + 3 files changed, 70 insertions(+), 35 deletions(-) create mode 100644 packages/alfa-style/test/property/mask-position.spec.tsx diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 0d70a883d0..5c62eb527f 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -1,47 +1,32 @@ -import { Keyword, List, Percentage, Position } from "@siteimprove/alfa-css"; +import { + Keyword, + LengthPercentage, + List, + Position, +} from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; +import { matchLayers } from "./mask.js"; -type Specified = List; +type Specified = List; +type Computed = Specified; /** - * @internal - */ -export namespace Specified { - export type Item = Position.Component; -} - -type Computed = List; - -namespace Computed { - export type Item = - Position.Component.PartiallyResolved; -} - -const parse = List.parseCommaSeparated(Position.Component.parseHorizontal); - -/** - * @internal - */ -const initialItem: Computed.Item = Position.Side.of( - Keyword.of("left"), - Percentage.of(0), -); - -// TODO: Copied from background-position-x.ts. Adjust for mask-position. -/** - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position} + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-position} * * @internal */ export default Longhand.of( - List.of([initialItem]), - parse, - (value, style) => - value.map((positions) => - positions.map( - Position.Component.partiallyResolve(Resolver.length(style)), + List.of( + [ + Position.of( + Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), + Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), ), - ), + ], + ", ", + ), + List.parseCommaSeparated(Position.parse(/* legacySyntax */ true)), + (value, style) => value.map((positions) => matchLayers(positions, style)), ); diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx new file mode 100644 index 0000000000..f228b5a757 --- /dev/null +++ b/packages/alfa-style/test/property/mask-position.spec.tsx @@ -0,0 +1,49 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../dist/index.js"; + +const device = Device.standard(); + +test("initial value is 0% 0%", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-position").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "side", + side: { + type: "keyword", + value: "left", + }, + offset: { + type: "percentage", + value: 0, + }, + }, + vertical: { + type: "side", + side: { + type: "keyword", + value: "top", + }, + offset: { + type: "percentage", + value: 0, + }, + }, + }, + ], + }, + source: null, + }); +}); diff --git a/packages/alfa-style/test/tsconfig.json b/packages/alfa-style/test/tsconfig.json index 0bfb6783cb..a4415680a6 100644 --- a/packages/alfa-style/test/tsconfig.json +++ b/packages/alfa-style/test/tsconfig.json @@ -59,6 +59,7 @@ "./property/mask-image.spec.tsx", "./property/mask-mode.spec.tsx", "./property/mask-origin.spec.tsx", + "./property/mask-position.spec.tsx", "./property/mask-repeat.spec.tsx", "./property/mask-size.spec.tsx", "./property/mix-blend-mode.spec.tsx", From e82fe0f618301fac4818b1145095de82bca27087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:53:47 +0100 Subject: [PATCH 17/40] Remove unused imports --- packages/alfa-style/src/property/mask-position.ts | 1 - packages/alfa-style/test/property/mask-position.spec.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 5c62eb527f..eececb931c 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -6,7 +6,6 @@ import { } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { Resolver } from "../resolver.js"; import { matchLayers } from "./mask.js"; type Specified = List; diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx index f228b5a757..f1b18740fb 100644 --- a/packages/alfa-style/test/property/mask-position.spec.tsx +++ b/packages/alfa-style/test/property/mask-position.spec.tsx @@ -1,5 +1,4 @@ import { test } from "@siteimprove/alfa-test"; -import { h } from "@siteimprove/alfa-dom"; import { Device } from "@siteimprove/alfa-device"; From 9e11e9adac7c6558e8b1608f9d89d83040349c10 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 08:57:37 +0000 Subject: [PATCH 18/40] Extract API --- docs/review/api/alfa-style.api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index ea57630ea5..0ba6c21439 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -8,6 +8,7 @@ import type { Applicative } from '@siteimprove/alfa-applicative'; import { Array as Array_2 } from '@siteimprove/alfa-array'; import { Box } from '@siteimprove/alfa-css'; import { Color } from '@siteimprove/alfa-css'; +import { Component } from '@siteimprove/alfa-css/dist/value/position/component.js'; import { Computed } from './property/line-height.js'; import { Contain } from '@siteimprove/alfa-css'; import { Context } from '@siteimprove/alfa-selector'; @@ -39,6 +40,7 @@ import { Parser } from '@siteimprove/alfa-css'; import * as parser from '@siteimprove/alfa-parser'; import { Percentage } from '@siteimprove/alfa-css'; import { Perspective } from '@siteimprove/alfa-css'; +import { Position } from '@siteimprove/alfa-css'; import { Predicate } from '@siteimprove/alfa-predicate'; import { Rectangle } from '@siteimprove/alfa-css'; import type { Resolvable } from '@siteimprove/alfa-css'; @@ -57,8 +59,6 @@ import { Specified as Specified_13 } from './property/font-stretch.js'; import { Specified as Specified_14 } from './property/font-variant-east-asian.js'; import { Specified as Specified_15 } from './property/font-variant-ligatures.js'; import { Specified as Specified_16 } from './property/font-variant-numeric.js'; -import { Specified as Specified_17 } from './property/mask-position.js'; -import { Specified as Specified_18 } from './property/mask-size.js'; import { Specified as Specified_2 } from './property/background-image.js'; import { Specified as Specified_3 } from './property/background-position-x.js'; import { Specified as Specified_4 } from './property/background-position-y.js'; @@ -232,9 +232,9 @@ export namespace Longhands { readonly "mask-image": Longhand | URL | Image>, List | URL | Image>>; readonly "mask-mode": Longhand | Keyword<"luminance"> | Keyword<"match-source">>, List | Keyword<"luminance"> | Keyword<"match-source">>>; readonly "mask-origin": Longhand, List>; - readonly "mask-position": Longhand, List>>; + readonly "mask-position": Longhand, Component>>, List, Component>>>; readonly "mask-repeat": Longhand | Keyword<"repeat-y"> | List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">>>, List | Keyword<"repeat-y"> | List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">>>>; - readonly "mask-size": Longhand, List | Keyword<"contain"> | Tuple<[LengthPercentage | Keyword<"auto">, LengthPercentage | Keyword<"auto">]>>>; + readonly "mask-size": Longhand | Keyword<"contain"> | List>>, List | Keyword<"contain"> | List>>>; readonly "min-height": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "min-width": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "mix-blend-mode": Longhand, Keyword.ToKeywords<"screen" | "color" | "hue" | "saturation" | "normal" | "multiply" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "luminosity" | "plus-darker" | "plus-lighter">>; From f3d6f90a02cbf387b5c3028a76e5c76d0a2a3f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:57:06 +0100 Subject: [PATCH 19/40] Implement mask-position --- .../alfa-style/src/property/mask-position.ts | 13 +- .../test/property/mask-position.spec.tsx | 195 ++++++++++++++++++ 2 files changed, 206 insertions(+), 2 deletions(-) diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index eececb931c..7e49961ae8 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -7,9 +7,10 @@ import { import { Longhand } from "../longhand.js"; import { matchLayers } from "./mask.js"; +import { Resolver } from "../resolver.js"; type Specified = List; -type Computed = Specified; +type Computed = List; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-position} @@ -27,5 +28,13 @@ export default Longhand.of( ", ", ), List.parseCommaSeparated(Position.parse(/* legacySyntax */ true)), - (value, style) => value.map((positions) => matchLayers(positions, style)), + (value, style) => + value.map((positions) => + matchLayers( + positions.map((position) => + position.partiallyResolve(Resolver.length(style)), + ), + style, + ), + ), ); diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx index f1b18740fb..c24726391e 100644 --- a/packages/alfa-style/test/property/mask-position.spec.tsx +++ b/packages/alfa-style/test/property/mask-position.spec.tsx @@ -1,4 +1,5 @@ import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; import { Device } from "@siteimprove/alfa-device"; @@ -46,3 +47,197 @@ test("initial value is 0% 0%", (t) => { source: null, }); }); + +// TODO: The spec requires the computed value to be two lengths or percentages, not a keyword value. +// E.g. the keyword `left` should be computes to `0% 50%` in Chrome and Firefox. +test("#computed parses single keywords", (t) => { + for (const kw of ["top", "bottom"] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-position").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "keyword", + value: "center", + }, + vertical: { + type: "side", + offset: null, + side: { + type: "keyword", + value: kw, + }, + }, + }, + ], + }, + source: h.declaration("mask-position", kw).toJSON(), + }); + } + + for (const kw of ["left", "right"] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-position").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + vertical: { + type: "keyword", + value: "center", + }, + horizontal: { + type: "side", + offset: null, + side: { + type: "keyword", + value: kw, + }, + }, + }, + ], + }, + source: h.declaration("mask-position", kw).toJSON(), + }); + } + + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-position").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + vertical: { + type: "keyword", + value: "center", + }, + horizontal: { + type: "keyword", + value: "center", + }, + }, + ], + }, + source: h.declaration("mask-position", "center").toJSON(), + }); +}); + +test("#computed parses lengths and percentages", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-position").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "side", + offset: { + type: "percentage", + value: 0.1, + }, + side: { + type: "keyword", + value: "left", + }, + }, + vertical: { + type: "side", + offset: { + type: "length", + unit: "px", + value: 48, + }, + side: { + type: "keyword", + value: "top", + }, + }, + }, + ], + }, + source: h.declaration("mask-position", "10% 3em").toJSON(), + }); +}); + +test("#computed parses multiple layers", (t) => { + const element = ( +
+ ); + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-position").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "side", + offset: { + type: "length", + unit: "px", + value: 16, + }, + side: { + type: "keyword", + value: "left", + }, + }, + vertical: { + type: "side", + offset: { + type: "length", + unit: "px", + value: 16, + }, + side: { + type: "keyword", + value: "top", + }, + }, + }, + { + type: "position", + horizontal: { + type: "keyword", + value: "center", + }, + vertical: { + type: "keyword", + value: "center", + }, + }, + ], + }, + source: h.declaration("mask-position", "1rem 1rem, center").toJSON(), + }); +}); From 873b22bccfbf5dfc5517d5a83899dc8e19619174 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:11:04 +0000 Subject: [PATCH 20/40] Extract API --- docs/review/api/alfa-style.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 0ba6c21439..6e997413af 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -232,7 +232,7 @@ export namespace Longhands { readonly "mask-image": Longhand | URL | Image>, List | URL | Image>>; readonly "mask-mode": Longhand | Keyword<"luminance"> | Keyword<"match-source">>, List | Keyword<"luminance"> | Keyword<"match-source">>>; readonly "mask-origin": Longhand, List>; - readonly "mask-position": Longhand, Component>>, List, Component>>>; + readonly "mask-position": Longhand, Component>>, List>>; readonly "mask-repeat": Longhand | Keyword<"repeat-y"> | List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">>>, List | Keyword<"repeat-y"> | List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">>>>; readonly "mask-size": Longhand | Keyword<"contain"> | List>>, List | Keyword<"contain"> | List>>>; readonly "min-height": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; From 2baf4f377d9d0d7bd3d1c8eda228654f06cf1e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:16:45 +0100 Subject: [PATCH 21/40] Implement shorthand `mask` --- package.json | 1 - packages/alfa-css/src/value/box.ts | 3 + packages/alfa-style/src/property/mask-clip.ts | 4 +- .../alfa-style/src/property/mask-composite.ts | 22 ++- .../alfa-style/src/property/mask-image.ts | 19 ++- packages/alfa-style/src/property/mask-mode.ts | 20 ++- .../alfa-style/src/property/mask-origin.ts | 4 +- .../alfa-style/src/property/mask-position.ts | 15 +- .../alfa-style/src/property/mask-repeat.ts | 26 +-- packages/alfa-style/src/property/mask-size.ts | 25 +-- packages/alfa-style/src/property/mask.ts | 155 +++++++++++++++++- .../test/property/mask-image.spec.tsx | 2 +- 12 files changed, 236 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index a51f8776d0..c719d4f7fc 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "build": "node --max-old-space-size=8192 scripts/build.mjs --pretty", "clean": "node scripts/clean.mjs --pretty", "test": "yarn build && vitest run --config ./config/vitest.config.ts", - "test:watch": "yarn build && vitest --config ./config/vitest.config.ts", "watch": "node scripts/watch.mjs --pretty", "extract": "node scripts/api-extractor.mjs --pretty", "document": "api-documenter generate -i docs/data/api -o docs/api", diff --git a/packages/alfa-css/src/value/box.ts b/packages/alfa-css/src/value/box.ts index f253f38fe0..dc539fa215 100644 --- a/packages/alfa-css/src/value/box.ts +++ b/packages/alfa-css/src/value/box.ts @@ -110,6 +110,9 @@ export namespace Box { /** * {@link https://www.w3.org/TR/css-box-4/#typedef-coord-box} + * + * @privateRemarks + * This is not the same type as `Geometry`. The only difference is that this type does not contain `margin-box`. */ export type CoordBox = PaintBox | Keyword<"view-box">; diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index 43d0d1f227..a2b08e9c03 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -9,13 +9,15 @@ const { either } = Parser; type Specified = List>; type Computed = Specified; +export const initialItem = Keyword.of("border-box"); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-clip} * * @internal */ export default Longhand.of( - List.of([Keyword.of("border-box")]), + List.of([initialItem]), List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index fe1c1e0921..853b176270 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -3,17 +3,21 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; import { matchLayers } from "./mask.js"; -type CompositingOperator = +export type CompositingOperator = | Keyword<"add"> | Keyword<"subtract"> | Keyword<"intersect"> | Keyword<"exclude">; -const compositingOperator: CSSParser = Keyword.parse( - "add", - "subtract", - "intersect", - "exclude", -); + +export namespace CompositingOperator { + export const parse: CSSParser = Keyword.parse( + "add", + "subtract", + "intersect", + "exclude", + ); + export const initialItem = Keyword.of("add"); +} type Specified = List; type Computed = Specified; @@ -24,7 +28,7 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([Keyword.of("add")], ", "), - List.parseCommaSeparated(compositingOperator), + List.of([CompositingOperator.initialItem], ", "), + List.parseCommaSeparated(CompositingOperator.parse), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-image.ts b/packages/alfa-style/src/property/mask-image.ts index 2b0f8992fa..fd52551ba7 100644 --- a/packages/alfa-style/src/property/mask-image.ts +++ b/packages/alfa-style/src/property/mask-image.ts @@ -11,11 +11,16 @@ const { either } = Parser; import { Longhand } from "../longhand.js"; -type MaskReference = Keyword<"none"> | Image | URL; -const maskReference: CSSParser = either( - Keyword.parse("none"), - either(Image.parse, URL.parse), -); +export type MaskReference = Keyword<"none"> | Image | URL; + +export namespace MaskReference { + export const parse: CSSParser = either( + Keyword.parse("none"), + either(Image.parse, URL.parse), + ); + + export const initialItem = Keyword.of("none"); +} type Specified = List; type Computed = Specified; @@ -26,7 +31,7 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([Keyword.of("none")]), - List.parseCommaSeparated(maskReference), + List.of([MaskReference.initialItem], ", "), + List.parseCommaSeparated(MaskReference.parse), (value) => value, // TODO: How to resolve absolute URL? ); diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index 0049920765..684d9d744e 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -3,15 +3,19 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; import { matchLayers } from "./mask.js"; -type MaskingMode = +export type MaskingMode = | Keyword<"alpha"> | Keyword<"luminance"> | Keyword<"match-source">; -const maskingMode: CSSParser = Keyword.parse( - "alpha", - "luminance", - "match-source", -); + +export namespace MaskingMode { + export const parse: CSSParser = Keyword.parse( + "alpha", + "luminance", + "match-source", + ); + export const initialItem = Keyword.of("match-source"); +} type Specified = List; type Computed = Specified; @@ -22,7 +26,7 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([Keyword.of("match-source")], ", "), - List.parseCommaSeparated(maskingMode), + List.of([MaskingMode.initialItem], ", "), + List.parseCommaSeparated(MaskingMode.parse), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index 7811d6a6cd..4e13219ad6 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -6,13 +6,15 @@ import { matchLayers } from "./mask.js"; type Specified = List; type Computed = Specified; +export const initialItem = Keyword.of("border-box"); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-origin} * * @internal */ export default Longhand.of( - List.of([Keyword.of("border-box")], ", "), + List.of([initialItem], ", "), List.parseCommaSeparated(Box.parseCoordBox), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 7e49961ae8..9df75f5adb 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -12,21 +12,18 @@ import { Resolver } from "../resolver.js"; type Specified = List; type Computed = List; +export const initialItem = Position.of( + Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), + Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), +); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-position} * * @internal */ export default Longhand.of( - List.of( - [ - Position.of( - Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), - Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), - ), - ], - ", ", - ), + List.of([initialItem], ", "), List.parseCommaSeparated(Position.parse(/* legacySyntax */ true)), (value, style) => value.map((positions) => diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index 39e58a2b23..9b3c5def10 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -6,7 +6,7 @@ import { matchLayers } from "./mask.js"; const { either } = Parser; -type RepeatStyle = +export type RepeatStyle = | Keyword<"repeat-x"> | Keyword<"repeat-y"> | List< @@ -15,14 +15,18 @@ type RepeatStyle = | Keyword<"round"> | Keyword<"no-repeat"> >; -const repeatStyle: CSSParser = either( - Keyword.parse("repeat-x", "repeat-y"), - List.parseSpaceSeparated( - Keyword.parse("repeat", "space", "round", "no-repeat"), - 1, - 2, - ), -); + +export namespace RepeatStyle { + export const parse: CSSParser = either( + Keyword.parse("repeat-x", "repeat-y"), + List.parseSpaceSeparated( + Keyword.parse("repeat", "space", "round", "no-repeat"), + 1, + 2, + ), + ); + export const initialItem = List.of([Keyword.of("repeat")], " "); +} type Specified = List; type Computed = Specified; @@ -40,7 +44,7 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([List.of([Keyword.of("repeat")], " ")], ", "), - List.parseCommaSeparated(repeatStyle), + List.of([RepeatStyle.initialItem], ", "), + List.parseCommaSeparated(RepeatStyle.parse), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index de416e7bb8..993729084e 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -12,19 +12,22 @@ import { matchLayers } from "./mask.js"; const { either } = Parser; -type BgSize = +export type BgSize = | List> | Keyword<"cover"> | Keyword<"contain">; -const bgSize: CSSParser = either( - List.parseSpaceSeparated( - either(LengthPercentage.parse, Keyword.parse("auto")), - 1, - 2, - ), - Keyword.parse("cover", "contain"), -); +export namespace BgSize { + export const parse: CSSParser = either( + List.parseSpaceSeparated( + either(LengthPercentage.parse, Keyword.parse("auto")), + 1, + 2, + ), + Keyword.parse("cover", "contain"), + ); + export const initialItem = List.of([Keyword.of("auto")], " "); +} type Specified = List; type Computed = Specified; @@ -35,8 +38,8 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([List.of([Keyword.of("auto")], " ")], ", "), - List.parseCommaSeparated(bgSize), + List.of([BgSize.initialItem], ", "), + List.parseCommaSeparated(BgSize.parse), (value, style) => value.map((sizes) => matchLayers( diff --git a/packages/alfa-style/src/property/mask.ts b/packages/alfa-style/src/property/mask.ts index 9c80491cee..a412d0f60b 100644 --- a/packages/alfa-style/src/property/mask.ts +++ b/packages/alfa-style/src/property/mask.ts @@ -1,7 +1,40 @@ -import { List, type Value } from "@siteimprove/alfa-css"; +import { + Token, + List, + type Parser as CSSParser, + Position, + type Value, + Box, + Keyword, +} from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; +import type { Slice } from "@siteimprove/alfa-slice"; +import { Option } from "@siteimprove/alfa-option"; + +import { Shorthand } from "../shorthand.js"; import type { Style } from "../style.js"; +import { MaskReference } from "./mask-image.js"; +import { BgSize } from "./mask-size.js"; +import { RepeatStyle } from "./mask-repeat.js"; +import { CompositingOperator } from "./mask-composite.js"; +import { MaskingMode } from "./mask-mode.js"; +import { initialItem as posInitialItem } from "./mask-position.js"; +import { initialItem as clipInitialItem } from "./mask-clip.js"; +import { initialItem as originInitialItem } from "./mask-origin.js"; + +const { + doubleBar, + either, + map, + option, + pair, + right, + delimited, + separatedList, +} = Parser; + /** * {@link https://drafts.fxtf.org/css-masking/#layering}. * @@ -37,3 +70,123 @@ export function matchLayers( ", ", ); } + +const slash = delimited(option(Token.parseWhitespace), Token.parseDelim("/")); + +const parsePosAndSize: CSSParser<[Position, Option]> = pair( + Position.parse(/* legacySyntax */ true), + option(right(slash, BgSize.parse)), +); + +/** + * {@link https://drafts.fxtf.org/css-masking/#typedef-mask-layer} + * + * @privateRemarks + * As of December 2024 the specification uses the type in the shorthand defintion + * whereas the longhands `mask-clip` and `mask-origin` uses . + * These are not the same types - does not have `margin-box`. + * Chrome and Firefox does not, at time of writing, allow `margin-box` in the shorthand. + * Therefore we assume that the discrepancy is a spec-bug and that the intended type is . + */ +const maskLayer: CSSParser< + [ + MaskReference | undefined, + Position | undefined, + BgSize | undefined, + RepeatStyle | undefined, + Box.CoordBox | undefined, + Box.CoordBox | Keyword<"no-clip"> | undefined, + CompositingOperator | undefined, + MaskingMode | undefined, + ] +> = map( + doubleBar< + Slice, + [ + MaskReference, + [Position, Option], + RepeatStyle, + Box.CoordBox, + Box.CoordBox | Keyword<"no-clip">, + CompositingOperator, + MaskingMode, + ], + string + >( + Token.parseWhitespace, + MaskReference.parse, + parsePosAndSize, + RepeatStyle.parse, + Box.parseCoordBox, + either(Box.parseCoordBox, Keyword.parse("no-clip")), + CompositingOperator.parse, + MaskingMode.parse, + ), + ([image, posAndSize, repeat, box1, box2, composite, mode]) => { + const [pos, size] = + posAndSize !== undefined + ? [posAndSize[0], posAndSize[1].getOr(undefined)] + : [undefined, undefined]; + + const origin = box1; + const clip = box2 ?? box1; + + return [image, pos, size, repeat, origin, clip, composite, mode] as const; + }, +); + +const parseList = separatedList( + maskLayer, + delimited(option(Token.parseWhitespace), Token.parseComma), +); + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/border-top} + * + * @internal + */ +export default Shorthand.of( + [ + "mask-image", + "mask-position", + "mask-size", + "mask-repeat", + "mask-origin", + "mask-clip", + "mask-composite", + "mask-mode", + ], + map(parseList, (layers) => { + const images: Array = []; + const positions: Array = []; + const sizes: Array = []; + const repeats: Array = []; + const origins: Array = []; + const clips: Array> = []; + const composites: Array = []; + const modes: Array = []; + + for (const layer of layers) { + const [image, pos, size, repeat, origin, clip, composite, mode] = layer; + images.push(image ?? MaskReference.initialItem); + positions.push(pos ?? posInitialItem); + sizes.push(size ?? BgSize.initialItem); + repeats.push(repeat ?? RepeatStyle.initialItem); + origins.push(origin ?? originInitialItem); + clips.push(clip ?? clipInitialItem); + composites.push(composite ?? CompositingOperator.initialItem); + modes.push(mode ?? MaskingMode.initialItem); + } + + return [ + ["mask-image", List.of(images, ", ")], + ["mask-position", List.of(positions, ", ")], + ["mask-size", List.of(sizes, ", ")], + ["mask-repeat", List.of(repeats, ", ")], + ["mask-origin", List.of(origins, ", ")], + ["mask-clip", List.of(clips, ", ")], + ["mask-composite", List.of(composites, ", ")], + ["mask-mode", List.of(modes, ", ")], + ]; + }), +); diff --git a/packages/alfa-style/test/property/mask-image.spec.tsx b/packages/alfa-style/test/property/mask-image.spec.tsx index 976ddeac81..0ee3a1656e 100644 --- a/packages/alfa-style/test/property/mask-image.spec.tsx +++ b/packages/alfa-style/test/property/mask-image.spec.tsx @@ -15,7 +15,7 @@ test("initial value is none", (t) => { t.deepEqual(style.computed("mask-image").toJSON(), { value: { type: "list", - separator: " ", + separator: ", ", values: [ { type: "keyword", From 758da49d1b25535538946fa35920e5dee48e26aa Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:25:07 +0000 Subject: [PATCH 22/40] Extract API --- docs/review/api/alfa-style.api.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 6e997413af..c149f9dd23 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -6,9 +6,11 @@ import type { Applicative } from '@siteimprove/alfa-applicative'; import { Array as Array_2 } from '@siteimprove/alfa-array'; +import { BgSize } from './property/mask-size.js'; import { Box } from '@siteimprove/alfa-css'; import { Color } from '@siteimprove/alfa-css'; import { Component } from '@siteimprove/alfa-css/dist/value/position/component.js'; +import { CompositingOperator } from './property/mask-composite.js'; import { Computed } from './property/line-height.js'; import { Contain } from '@siteimprove/alfa-css'; import { Context } from '@siteimprove/alfa-selector'; @@ -18,7 +20,6 @@ import { Device } from '@siteimprove/alfa-device'; import { Element } from '@siteimprove/alfa-dom'; import { Equatable } from '@siteimprove/alfa-equatable'; import type { Functor } from '@siteimprove/alfa-functor'; -import { Gradient } from '@siteimprove/alfa-css'; import { Image } from '@siteimprove/alfa-css'; import { Integer } from '@siteimprove/alfa-css'; import { Iterable as Iterable_2 } from '@siteimprove/alfa-iterable'; @@ -30,6 +31,8 @@ import { LengthPercentage } from '@siteimprove/alfa-css'; import { List } from '@siteimprove/alfa-css'; import { Map as Map_2 } from '@siteimprove/alfa-map'; import type { Mapper } from '@siteimprove/alfa-mapper'; +import { MaskingMode } from './property/mask-mode.js'; +import { MaskReference } from './property/mask-image.js'; import type { Monad } from '@siteimprove/alfa-monad'; import { Node } from '@siteimprove/alfa-dom'; import { Number as Number_2 } from '@siteimprove/alfa-css'; @@ -43,6 +46,7 @@ import { Perspective } from '@siteimprove/alfa-css'; import { Position } from '@siteimprove/alfa-css'; import { Predicate } from '@siteimprove/alfa-predicate'; import { Rectangle } from '@siteimprove/alfa-css'; +import { RepeatStyle } from './property/mask-repeat.js'; import type { Resolvable } from '@siteimprove/alfa-css'; import { Rotate } from '@siteimprove/alfa-css'; import { Scale } from '@siteimprove/alfa-css'; @@ -228,13 +232,13 @@ export namespace Longhands { readonly "margin-right": Longhand, Length | Percentage | Keyword<"auto">>; readonly "margin-top": Longhand, Length | Percentage | Keyword<"auto">>; readonly "mask-clip": Longhand>, List>>; - readonly "mask-composite": Longhand | Keyword<"subtract"> | Keyword<"intersect"> | Keyword<"exclude">>, List | Keyword<"subtract"> | Keyword<"intersect"> | Keyword<"exclude">>>; - readonly "mask-image": Longhand | URL | Image>, List | URL | Image>>; - readonly "mask-mode": Longhand | Keyword<"luminance"> | Keyword<"match-source">>, List | Keyword<"luminance"> | Keyword<"match-source">>>; + readonly "mask-composite": Longhand, List>; + readonly "mask-image": Longhand, List>; + readonly "mask-mode": Longhand, List>; readonly "mask-origin": Longhand, List>; readonly "mask-position": Longhand, Component>>, List>>; - readonly "mask-repeat": Longhand | Keyword<"repeat-y"> | List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">>>, List | Keyword<"repeat-y"> | List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">>>>; - readonly "mask-size": Longhand | Keyword<"contain"> | List>>, List | Keyword<"contain"> | List>>>; + readonly "mask-repeat": Longhand, List>; + readonly "mask-size": Longhand, List>; readonly "min-height": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "min-width": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "mix-blend-mode": Longhand, Keyword.ToKeywords<"screen" | "color" | "hue" | "saturation" | "normal" | "multiply" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "luminosity" | "plus-darker" | "plus-lighter">>; From 216928dbc383d1af761d3589d34c224faa180fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:59:10 +0100 Subject: [PATCH 23/40] Add `mask` to shorthands --- packages/alfa-style/src/shorthands.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/alfa-style/src/shorthands.ts b/packages/alfa-style/src/shorthands.ts index 30555c7219..b214b58975 100644 --- a/packages/alfa-style/src/shorthands.ts +++ b/packages/alfa-style/src/shorthands.ts @@ -30,6 +30,7 @@ import InsetBlock from "./property/inset-block.js"; import InsetInline from "./property/inset-inline.js"; import Inset from "./property/inset.js"; import Margin from "./property/margin.js"; +import Mask from "./property/mask.js"; import Outline from "./property/outline.js"; import Overflow from "./property/overflow.js"; import TextDecoration from "./property/text-decoration.js"; @@ -76,6 +77,7 @@ export namespace Shorthands { "inset-inline": InsetInline, inset: Inset, margin: Margin, + mask: Mask, outline: Outline, overflow: Overflow, "text-decoration": TextDecoration, From 7e820c43a1442da698529aee8cd20585c408fec7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:28:47 +0000 Subject: [PATCH 24/40] Extract API --- docs/review/api/alfa-style.api.md | 72 ++++++++++++++++--------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index c149f9dd23..1c11c795ee 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -352,6 +352,7 @@ export namespace Shorthands { readonly "inset-inline": Shorthand<"inset-inline-end" | "inset-inline-start">; readonly inset: Shorthand<"top" | "bottom" | "left" | "right">; readonly margin: Shorthand<"margin-bottom" | "margin-left" | "margin-right" | "margin-top">; + readonly mask: Shorthand<"mask-clip" | "mask-composite" | "mask-image" | "mask-mode" | "mask-origin" | "mask-position" | "mask-repeat" | "mask-size">; readonly outline: Shorthand<"outline-color" | "outline-style" | "outline-width">; readonly overflow: Shorthand<"overflow-x" | "overflow-y">; readonly "text-decoration": Shorthand<"text-decoration-color" | "text-decoration-line" | "text-decoration-style" | "text-decoration-thickness">; @@ -640,41 +641,42 @@ export namespace Value { // src/longhands.ts:319:7 - (ae-incompatible-release-tags) The symbol ""will-change"" is marked as @public, but its signature references "Longhand" which is marked as @internal // src/longhands.ts:320:7 - (ae-incompatible-release-tags) The symbol ""word-spacing"" is marked as @public, but its signature references "Longhand" which is marked as @internal // src/longhands.ts:321:7 - (ae-incompatible-release-tags) The symbol ""z-index"" is marked as @public, but its signature references "Longhand" which is marked as @internal -// src/shorthands.ts:47:14 - (ae-incompatible-release-tags) The symbol "background" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:48:14 - (ae-incompatible-release-tags) The symbol ""background-position"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:49:14 - (ae-incompatible-release-tags) The symbol ""background-repeat"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:50:14 - (ae-incompatible-release-tags) The symbol ""border-block-color"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:51:14 - (ae-incompatible-release-tags) The symbol ""border-block-end"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:52:14 - (ae-incompatible-release-tags) The symbol ""border-block-start"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:53:14 - (ae-incompatible-release-tags) The symbol ""border-block-style"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:54:14 - (ae-incompatible-release-tags) The symbol ""border-block"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:55:14 - (ae-incompatible-release-tags) The symbol ""border-block-width"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:56:14 - (ae-incompatible-release-tags) The symbol ""border-bottom"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:57:14 - (ae-incompatible-release-tags) The symbol ""border-color"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:58:14 - (ae-incompatible-release-tags) The symbol ""border-image"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:59:14 - (ae-incompatible-release-tags) The symbol ""border-inline-color"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:60:14 - (ae-incompatible-release-tags) The symbol ""border-inline-end"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:61:14 - (ae-incompatible-release-tags) The symbol ""border-inline-start"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:62:14 - (ae-incompatible-release-tags) The symbol ""border-inline-style"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:63:14 - (ae-incompatible-release-tags) The symbol ""border-inline"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:64:14 - (ae-incompatible-release-tags) The symbol ""border-inline-width"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:65:14 - (ae-incompatible-release-tags) The symbol ""border-left"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:66:14 - (ae-incompatible-release-tags) The symbol ""border-radius"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:67:14 - (ae-incompatible-release-tags) The symbol ""border-right"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:68:14 - (ae-incompatible-release-tags) The symbol ""border-style"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:69:14 - (ae-incompatible-release-tags) The symbol ""border-top"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:70:14 - (ae-incompatible-release-tags) The symbol "border" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:71:14 - (ae-incompatible-release-tags) The symbol ""border-width"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:72:14 - (ae-incompatible-release-tags) The symbol ""flex-flow"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:73:14 - (ae-incompatible-release-tags) The symbol "font" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:74:14 - (ae-incompatible-release-tags) The symbol ""font-variant"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:75:14 - (ae-incompatible-release-tags) The symbol ""inset-block"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:76:14 - (ae-incompatible-release-tags) The symbol ""inset-inline"" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:77:14 - (ae-incompatible-release-tags) The symbol "inset" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:78:14 - (ae-incompatible-release-tags) The symbol "margin" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:79:14 - (ae-incompatible-release-tags) The symbol "outline" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:80:14 - (ae-incompatible-release-tags) The symbol "overflow" is marked as @public, but its signature references "Shorthand" which is marked as @internal -// src/shorthands.ts:81:14 - (ae-incompatible-release-tags) The symbol ""text-decoration"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:48:14 - (ae-incompatible-release-tags) The symbol "background" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:49:14 - (ae-incompatible-release-tags) The symbol ""background-position"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:50:14 - (ae-incompatible-release-tags) The symbol ""background-repeat"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:51:14 - (ae-incompatible-release-tags) The symbol ""border-block-color"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:52:14 - (ae-incompatible-release-tags) The symbol ""border-block-end"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:53:14 - (ae-incompatible-release-tags) The symbol ""border-block-start"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:54:14 - (ae-incompatible-release-tags) The symbol ""border-block-style"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:55:14 - (ae-incompatible-release-tags) The symbol ""border-block"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:56:14 - (ae-incompatible-release-tags) The symbol ""border-block-width"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:57:14 - (ae-incompatible-release-tags) The symbol ""border-bottom"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:58:14 - (ae-incompatible-release-tags) The symbol ""border-color"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:59:14 - (ae-incompatible-release-tags) The symbol ""border-image"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:60:14 - (ae-incompatible-release-tags) The symbol ""border-inline-color"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:61:14 - (ae-incompatible-release-tags) The symbol ""border-inline-end"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:62:14 - (ae-incompatible-release-tags) The symbol ""border-inline-start"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:63:14 - (ae-incompatible-release-tags) The symbol ""border-inline-style"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:64:14 - (ae-incompatible-release-tags) The symbol ""border-inline"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:65:14 - (ae-incompatible-release-tags) The symbol ""border-inline-width"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:66:14 - (ae-incompatible-release-tags) The symbol ""border-left"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:67:14 - (ae-incompatible-release-tags) The symbol ""border-radius"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:68:14 - (ae-incompatible-release-tags) The symbol ""border-right"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:69:14 - (ae-incompatible-release-tags) The symbol ""border-style"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:70:14 - (ae-incompatible-release-tags) The symbol ""border-top"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:71:14 - (ae-incompatible-release-tags) The symbol "border" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:72:14 - (ae-incompatible-release-tags) The symbol ""border-width"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:73:14 - (ae-incompatible-release-tags) The symbol ""flex-flow"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:74:14 - (ae-incompatible-release-tags) The symbol "font" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:75:14 - (ae-incompatible-release-tags) The symbol ""font-variant"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:76:14 - (ae-incompatible-release-tags) The symbol ""inset-block"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:77:14 - (ae-incompatible-release-tags) The symbol ""inset-inline"" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:78:14 - (ae-incompatible-release-tags) The symbol "inset" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:79:14 - (ae-incompatible-release-tags) The symbol "margin" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:80:14 - (ae-incompatible-release-tags) The symbol "mask" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:81:14 - (ae-incompatible-release-tags) The symbol "outline" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:82:14 - (ae-incompatible-release-tags) The symbol "overflow" is marked as @public, but its signature references "Shorthand" which is marked as @internal +// src/shorthands.ts:83:14 - (ae-incompatible-release-tags) The symbol ""text-decoration"" is marked as @public, but its signature references "Shorthand" which is marked as @internal // (No @packageDocumentation comment for this package) From 071c4682d6be4f131527454f9ed08eeca228d72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:39:41 +0100 Subject: [PATCH 25/40] Add test of shorthand as well as clean up and minor fixes --- .changeset/few-clouds-play.md | 5 + .../src/property/helpers/match-layers.ts | 39 ++ packages/alfa-style/src/property/mask-clip.ts | 9 +- .../alfa-style/src/property/mask-composite.ts | 3 +- .../alfa-style/src/property/mask-image.ts | 13 +- packages/alfa-style/src/property/mask-mode.ts | 3 +- .../alfa-style/src/property/mask-origin.ts | 9 +- .../alfa-style/src/property/mask-position.ts | 15 +- .../alfa-style/src/property/mask-repeat.ts | 3 +- packages/alfa-style/src/property/mask-size.ts | 3 +- packages/alfa-style/src/property/mask.ts | 51 +-- packages/alfa-style/src/tsconfig.json | 1 + packages/alfa-style/test/common.ts | 11 + .../test/property/mask-clip.spec.tsx | 63 ++-- .../test/property/mask-composite.spec.tsx | 26 +- .../test/property/mask-image.spec.tsx | 86 ++++- .../test/property/mask-mode.spec.tsx | 26 +- .../test/property/mask-origin.spec.tsx | 26 +- .../test/property/mask-position.spec.tsx | 30 +- .../test/property/mask-repeat.spec.tsx | 116 +++--- .../test/property/mask-size.spec.tsx | 34 +- .../alfa-style/test/property/mask.spec.tsx | 343 ++++++++++++++++++ packages/alfa-style/test/tsconfig.json | 1 + 23 files changed, 633 insertions(+), 283 deletions(-) create mode 100644 .changeset/few-clouds-play.md create mode 100644 packages/alfa-style/src/property/helpers/match-layers.ts create mode 100644 packages/alfa-style/test/property/mask.spec.tsx diff --git a/.changeset/few-clouds-play.md b/.changeset/few-clouds-play.md new file mode 100644 index 0000000000..d57ab69734 --- /dev/null +++ b/.changeset/few-clouds-play.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-style": minor +--- + +**Added:** CSS shorthand property `mask` and corresponding longhand properties is now supported. diff --git a/packages/alfa-style/src/property/helpers/match-layers.ts b/packages/alfa-style/src/property/helpers/match-layers.ts new file mode 100644 index 0000000000..3152284d09 --- /dev/null +++ b/packages/alfa-style/src/property/helpers/match-layers.ts @@ -0,0 +1,39 @@ +import { List, type Value } from "@siteimprove/alfa-css"; + +import type { Style } from "../../style.js"; + +/** + * {@link https://drafts.fxtf.org/css-masking/#layering}. + * + * @remarks + * The computed value depends on the number of layers. + * A layer is created for each of the comma separated values for `mask-image`. + * + * If there are more values than layers, the excess values are discarded. + * Otherwise, the values must be repeated + * until the number of values matches the number of layers. + */ +export function matchLayers( + value: List, + style: Style, +): List { + const numberOfLayers = Math.max( + style.computed("mask-image").value.values.length, + 1, + ); + + const numberOfValues = value.values.length; + if (numberOfValues === numberOfLayers) { + return value; + } + + return List.of( + (numberOfLayers < numberOfValues + ? value.values + : Array(Math.ceil(numberOfLayers / numberOfValues)) + .fill(value.values) + .flat() + ).slice(0, numberOfLayers), + ", ", + ); +} diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index a2b08e9c03..e05318d69a 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -2,14 +2,17 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; const { either } = Parser; type Specified = List>; type Computed = Specified; -export const initialItem = Keyword.of("border-box"); +export namespace MaskClip { + export const initialItem = Keyword.of("border-box"); +} /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-clip} @@ -17,7 +20,7 @@ export const initialItem = Keyword.of("border-box"); * @internal */ export default Longhand.of( - List.of([initialItem]), + List.of([MaskClip.initialItem]), List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index 853b176270..bb7e80a783 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -1,7 +1,8 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; export type CompositingOperator = | Keyword<"add"> diff --git a/packages/alfa-style/src/property/mask-image.ts b/packages/alfa-style/src/property/mask-image.ts index fd52551ba7..7111fb511c 100644 --- a/packages/alfa-style/src/property/mask-image.ts +++ b/packages/alfa-style/src/property/mask-image.ts @@ -6,10 +6,12 @@ import { URL, type Parser as CSSParser, } from "@siteimprove/alfa-css"; +import { Selective } from "@siteimprove/alfa-selective"; const { either } = Parser; import { Longhand } from "../longhand.js"; +import { Resolver } from "../resolver.js"; export type MaskReference = Keyword<"none"> | Image | URL; @@ -33,5 +35,14 @@ type Computed = Specified; export default Longhand.of( List.of([MaskReference.initialItem], ", "), List.parseCommaSeparated(MaskReference.parse), - (value) => value, // TODO: How to resolve absolute URL? + (value, style) => + value.map((images) => + images.map((image) => + Selective.of(image) + .if(Image.isImage, (image) => + image.partiallyResolve(Resolver.length(style)), + ) + .get(), + ), + ), ); diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index 684d9d744e..56cd0f4136 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -1,7 +1,8 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; export type MaskingMode = | Keyword<"alpha"> diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index 4e13219ad6..de1e3f125e 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -1,12 +1,15 @@ import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; type Specified = List; type Computed = Specified; -export const initialItem = Keyword.of("border-box"); +export namespace MaskOrigin { + export const initialItem = Keyword.of("border-box"); +} /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-origin} @@ -14,7 +17,7 @@ export const initialItem = Keyword.of("border-box"); * @internal */ export default Longhand.of( - List.of([initialItem], ", "), + List.of([MaskOrigin.initialItem], ", "), List.parseCommaSeparated(Box.parseCoordBox), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 9df75f5adb..1dc321769b 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -6,16 +6,19 @@ import { } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; import { Resolver } from "../resolver.js"; +import { matchLayers } from "./helpers/match-layers.js"; + type Specified = List; type Computed = List; -export const initialItem = Position.of( - Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), - Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), -); +export namespace MaskPosition { + export const initialItem = Position.of( + Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), + Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), + ); +} /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-position} @@ -23,7 +26,7 @@ export const initialItem = Position.of( * @internal */ export default Longhand.of( - List.of([initialItem], ", "), + List.of([MaskPosition.initialItem], ", "), List.parseCommaSeparated(Position.parse(/* legacySyntax */ true)), (value, style) => value.map((positions) => diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index 9b3c5def10..55504d02d6 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -2,7 +2,8 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; const { either } = Parser; diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index 993729084e..d00337da67 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -8,7 +8,8 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; const { either } = Parser; diff --git a/packages/alfa-style/src/property/mask.ts b/packages/alfa-style/src/property/mask.ts index a412d0f60b..5007d3b0fa 100644 --- a/packages/alfa-style/src/property/mask.ts +++ b/packages/alfa-style/src/property/mask.ts @@ -3,7 +3,6 @@ import { List, type Parser as CSSParser, Position, - type Value, Box, Keyword, } from "@siteimprove/alfa-css"; @@ -13,16 +12,14 @@ import { Option } from "@siteimprove/alfa-option"; import { Shorthand } from "../shorthand.js"; -import type { Style } from "../style.js"; - import { MaskReference } from "./mask-image.js"; import { BgSize } from "./mask-size.js"; import { RepeatStyle } from "./mask-repeat.js"; import { CompositingOperator } from "./mask-composite.js"; import { MaskingMode } from "./mask-mode.js"; -import { initialItem as posInitialItem } from "./mask-position.js"; -import { initialItem as clipInitialItem } from "./mask-clip.js"; -import { initialItem as originInitialItem } from "./mask-origin.js"; +import { MaskPosition } from "./mask-position.js"; +import { MaskClip } from "./mask-clip.js"; +import { MaskOrigin } from "./mask-origin.js"; const { doubleBar, @@ -35,42 +32,6 @@ const { separatedList, } = Parser; -/** - * {@link https://drafts.fxtf.org/css-masking/#layering}. - * - * @remarks - * The computed value depends on the number of layers. - * A layer is created for each of the comma separated values for `mask-image`. - * - * If there are more values than layers, the excess values are discarded. - * Otherwise, the values must be repeated - * until the number of values matches the number of layers. - */ -export function matchLayers( - value: List, - style: Style, -): List { - const numberOfLayers = Math.max( - style.computed("mask-image").value.values.length, - 1, - ); - - const numberOfValues = value.values.length; - if (numberOfValues === numberOfLayers) { - return value; - } - - return List.of( - (numberOfLayers < numberOfValues - ? value.values - : Array(Math.ceil(numberOfLayers / numberOfValues)) - .fill(value.values) - .flat() - ).slice(0, numberOfLayers), - ", ", - ); -} - const slash = delimited(option(Token.parseWhitespace), Token.parseDelim("/")); const parsePosAndSize: CSSParser<[Position, Option]> = pair( @@ -169,11 +130,11 @@ export default Shorthand.of( for (const layer of layers) { const [image, pos, size, repeat, origin, clip, composite, mode] = layer; images.push(image ?? MaskReference.initialItem); - positions.push(pos ?? posInitialItem); + positions.push(pos ?? MaskPosition.initialItem); sizes.push(size ?? BgSize.initialItem); repeats.push(repeat ?? RepeatStyle.initialItem); - origins.push(origin ?? originInitialItem); - clips.push(clip ?? clipInitialItem); + origins.push(origin ?? MaskOrigin.initialItem); + clips.push(clip ?? MaskClip.initialItem); composites.push(composite ?? CompositingOperator.initialItem); modes.push(mode ?? MaskingMode.initialItem); } diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index 80e803580f..91bba34172 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -39,6 +39,7 @@ "./predicate/is-block-container.ts", "./predicate/is-flex-container.ts", "./predicate/is-grid-container.ts", + "./property/helpers/match-layers.ts", "./property/background.ts", "./property/background-attachment.ts", "./property/background-clip.ts", diff --git a/packages/alfa-style/test/common.ts b/packages/alfa-style/test/common.ts index e4c9283347..60a8a43413 100755 --- a/packages/alfa-style/test/common.ts +++ b/packages/alfa-style/test/common.ts @@ -32,6 +32,17 @@ export function specified( return Style.from(element, device, context).specified(name).toJSON(); } +/** + * @internal + */ +export function computed( + element: Element, + name: N, + context: Context = Context.empty(), +): Value.JSON> { + return Style.from(element, device, context).computed(name).toJSON(); +} + /** * @internal */ diff --git a/packages/alfa-style/test/property/mask-clip.spec.tsx b/packages/alfa-style/test/property/mask-clip.spec.tsx index 31ece611f7..54f3aae722 100644 --- a/packages/alfa-style/test/property/mask-clip.spec.tsx +++ b/packages/alfa-style/test/property/mask-clip.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is border-box", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-clip").toJSON(), { + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: " ", @@ -39,9 +33,7 @@ test("#computed parses single keywords", (t) => { ] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-clip").toJSON(), { + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", @@ -58,14 +50,16 @@ test("#computed parses single keywords", (t) => { }); test("#computed parses multiple layers", (t) => { - const element =
; - - const style = Style.from(element, device); + const element = ( +
+ ); - t.deepEqual(style.computed("mask-clip").toJSON(), { + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", @@ -82,18 +76,19 @@ test("#computed parses multiple layers", (t) => { }, source: h.declaration("mask-clip", "padding-box, no-clip").toJSON(), }); - }); test("#computed discards excess values when there are more values than layers", (t) => { const element = ( -
+
); - const style = Style.from(element, device); - t.deepEqual(style.computed("mask-clip").toJSON(), { + + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", @@ -116,13 +111,15 @@ test("#computed discards excess values when there are more values than layers", test("#computed repeats values when there are more layers than values", (t) => { const element = ( -
+
); - const style = Style.from(element, device); - t.deepEqual(style.computed("mask-clip").toJSON(), { + + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", @@ -141,8 +138,6 @@ test("#computed repeats values when there are more layers than values", (t) => { }, ], }, - source: h - .declaration("mask-clip", "view-box, fill-box") - .toJSON(), + source: h.declaration("mask-clip", "view-box, fill-box").toJSON(), }); }); diff --git a/packages/alfa-style/test/property/mask-composite.spec.tsx b/packages/alfa-style/test/property/mask-composite.spec.tsx index 0a931218cb..7080334298 100644 --- a/packages/alfa-style/test/property/mask-composite.spec.tsx +++ b/packages/alfa-style/test/property/mask-composite.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is add", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", @@ -31,9 +25,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["add", "subtract", "intersect", "exclude"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", @@ -59,9 +51,7 @@ test("#computed parses multiple layers", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", @@ -90,9 +80,7 @@ test("#computed discards excess values when there are more values than layers", > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", @@ -121,9 +109,7 @@ test("#computed repeats values when there are more layers than values", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask-image.spec.tsx b/packages/alfa-style/test/property/mask-image.spec.tsx index 0ee3a1656e..a50040eb72 100644 --- a/packages/alfa-style/test/property/mask-image.spec.tsx +++ b/packages/alfa-style/test/property/mask-image.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is none", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-image").toJSON(), { + t.deepEqual(computed(element, "mask-image"), { value: { type: "list", separator: ", ", @@ -30,9 +24,7 @@ test("initial value is none", (t) => { test("#computed parses url value", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-image").toJSON(), { + t.deepEqual(computed(element, "mask-image"), { value: { type: "list", separator: ", ", @@ -54,8 +46,8 @@ test("#computed parses linear-gradient value", (t) => { const element = (
); - const style = Style.from(element, device); - t.deepEqual(style.computed("mask-image").toJSON(), { + + t.deepEqual(computed(element, "mask-image"), { value: { type: "list", separator: ", ", @@ -72,18 +64,48 @@ test("#computed parses linear-gradient value", (t) => { items: [ { color: { - color: "red", - format: "named", type: "color", + format: "rgb", + alpha: { + type: "percentage", + value: 1, + }, + red: { + type: "percentage", + value: 1, + }, + green: { + type: "percentage", + value: 0, + }, + blue: { + type: "percentage", + value: 0, + }, }, position: null, type: "stop", }, { color: { - color: "blue", - format: "named", type: "color", + format: "rgb", + alpha: { + type: "percentage", + value: 1, + }, + red: { + type: "percentage", + value: 0, + }, + green: { + type: "percentage", + value: 0, + }, + blue: { + type: "percentage", + value: 1, + }, }, position: null, type: "stop", @@ -97,3 +119,33 @@ test("#computed parses linear-gradient value", (t) => { source: h.declaration("mask-image", "linear-gradient(red, blue)").toJSON(), }); }); + +test("#computed parses multiple layers", (t) => { + const element = ( +
+ ); + + t.deepEqual(computed(element, "mask-image"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "url", + url: "foo.svg", + }, + }, + { + type: "image", + image: { + type: "url", + url: "bar.svg", + }, + }, + ], + }, + source: h.declaration("mask-image", "url(foo.svg), url(bar.svg)").toJSON(), + }); +}); diff --git a/packages/alfa-style/test/property/mask-mode.spec.tsx b/packages/alfa-style/test/property/mask-mode.spec.tsx index 928ce668fc..59e7d916a8 100644 --- a/packages/alfa-style/test/property/mask-mode.spec.tsx +++ b/packages/alfa-style/test/property/mask-mode.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is match-source", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-mode").toJSON(), { + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", @@ -31,9 +25,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["alpha", "luminance", "match-source"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-mode").toJSON(), { + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", @@ -58,8 +50,8 @@ test("#computed parses multiple layers", (t) => { }} > ); - const style = Style.from(element, device); - t.deepEqual(style.computed("mask-mode").toJSON(), { + + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", @@ -88,9 +80,7 @@ test("#computed discards excess values when there are more values than layers", > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-mode").toJSON(), { + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", @@ -121,9 +111,7 @@ test("#computed repeats values when there are more layers than values", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-mode").toJSON(), { + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask-origin.spec.tsx b/packages/alfa-style/test/property/mask-origin.spec.tsx index 0ff5bf7437..da10230917 100644 --- a/packages/alfa-style/test/property/mask-origin.spec.tsx +++ b/packages/alfa-style/test/property/mask-origin.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is border-box", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", @@ -38,9 +32,7 @@ test("#computed parses single keywords", (t) => { ] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", @@ -66,9 +58,7 @@ test("#computed parses multiple layers", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", @@ -97,9 +87,7 @@ test("#computed discards excess values when there are more values than layers", > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", @@ -130,9 +118,7 @@ test("#computed repeats values when there are more layers than values", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx index c24726391e..c35669ebce 100644 --- a/packages/alfa-style/test/property/mask-position.spec.tsx +++ b/packages/alfa-style/test/property/mask-position.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is 0% 0%", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -54,9 +48,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["top", "bottom"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -85,9 +77,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["left", "right"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -115,9 +105,7 @@ test("#computed parses single keywords", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -142,9 +130,7 @@ test("#computed parses single keywords", (t) => { test("#computed parses lengths and percentages", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -191,9 +177,7 @@ test("#computed parses multiple layers", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index cf5862c926..34816be52e 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is repeat", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -37,9 +31,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["repeat-x", "repeat-y"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -57,9 +49,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["repeat", "space", "round", "no-repeat"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -70,8 +60,8 @@ test("#computed parses single keywords", (t) => { values: [ { type: "keyword", - value: kw - } + value: kw, + }, ], }, ], @@ -83,8 +73,7 @@ test("#computed parses single keywords", (t) => { test("#computed parses at most two space separated values", (t) => { const element1 =
; - const style1 = Style.from(element1, device); - t.deepEqual(style1.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element1, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -109,8 +98,7 @@ test("#computed parses at most two space separated values", (t) => { }); const element2 =
; - const style2 = Style.from(element2, device); - t.deepEqual(style2.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element2, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -132,14 +120,16 @@ test("#computed parses at most two space separated values", (t) => { }); test("#computed parses mutiple layers", (t) => { - const element =
; - - const style = Style.from(element, device); + const element = ( +
+ ); - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -150,12 +140,12 @@ test("#computed parses mutiple layers", (t) => { values: [ { type: "keyword", - value: "round" + value: "round", }, { type: "keyword", - value: "repeat" - } + value: "repeat", + }, ], }, { @@ -163,12 +153,11 @@ test("#computed parses mutiple layers", (t) => { separator: " ", values: [ { - type: "keyword", - value: "space" - } - ] - } + value: "space", + }, + ], + }, ], }, source: h.declaration("mask-repeat", "round repeat, space").toJSON(), @@ -176,14 +165,16 @@ test("#computed parses mutiple layers", (t) => { }); test("#computed discards excess values when there are more values than layers", (t) => { - const element =
; + const element = ( +
+ ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -194,12 +185,12 @@ test("#computed discards excess values when there are more values than layers", values: [ { type: "keyword", - value: "round" + value: "round", }, { type: "keyword", - value: "repeat" - } + value: "repeat", + }, ], }, ], @@ -209,14 +200,16 @@ test("#computed discards excess values when there are more values than layers", }); test("#computed repeats values when there are more layers than values", (t) => { - const element =
; + const element = ( +
+ ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -227,12 +220,12 @@ test("#computed repeats values when there are more layers than values", (t) => { values: [ { type: "keyword", - value: "round" + value: "round", }, { type: "keyword", - value: "repeat" - } + value: "repeat", + }, ], }, { @@ -240,11 +233,10 @@ test("#computed repeats values when there are more layers than values", (t) => { separator: " ", values: [ { - type: "keyword", - value: "space" - } - ] + value: "space", + }, + ], }, { type: "list", @@ -252,12 +244,12 @@ test("#computed repeats values when there are more layers than values", (t) => { values: [ { type: "keyword", - value: "round" + value: "round", }, { type: "keyword", - value: "repeat" - } + value: "repeat", + }, ], }, ], diff --git a/packages/alfa-style/test/property/mask-size.spec.tsx b/packages/alfa-style/test/property/mask-size.spec.tsx index b72b9a5316..08d3ead997 100644 --- a/packages/alfa-style/test/property/mask-size.spec.tsx +++ b/packages/alfa-style/test/property/mask-size.spec.tsx @@ -1,11 +1,7 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is auto", (t) => { const element = ( @@ -16,9 +12,7 @@ test("initial value is auto", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -53,9 +47,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["cover", "contain"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -74,9 +66,7 @@ test("#computed parses single keywords", (t) => { test("#computed parses percentage width", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -100,9 +90,7 @@ test("#computed parses percentage width", (t) => { test("#computed resolves em width", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -127,9 +115,7 @@ test("#computed resolves em width", (t) => { test("#computed parses pixel width", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -154,9 +140,7 @@ test("#computed parses pixel width", (t) => { test("#computed parses width and height", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -192,9 +176,7 @@ test("#computed parses multiple layers", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask.spec.tsx b/packages/alfa-style/test/property/mask.spec.tsx new file mode 100644 index 0000000000..73e87fbfb2 --- /dev/null +++ b/packages/alfa-style/test/property/mask.spec.tsx @@ -0,0 +1,343 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { computed } from "../common.js"; + +test("longhands resolve correctly from shorthand", (t) => { + const mask = + "url(foo.svg) 50% 0% / 12px repeat-x view-box padding-box subtract luminance"; + const decl = h.declaration("mask", mask); + const element =
; + + t.deepEqual(computed(element, "mask-image"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "url", + url: "foo.svg", + }, + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-position"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "side", + offset: { + type: "percentage", + value: 0.5, + }, + side: { + type: "keyword", + value: "left", + }, + }, + vertical: { + type: "side", + offset: { + type: "percentage", + value: 0, + }, + side: { + type: "keyword", + value: "top", + }, + }, + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-size"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "length", + unit: "px", + value: 12, + }, + ], + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-repeat"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "repeat-x", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-origin"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-clip"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "padding-box", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-composite"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "subtract", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-mode"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "luminance", + }, + ], + }, + source: decl.toJSON(), + }); +}); + +test("if one `` value and the `no-clip` keyword are present then `` sets `mask-origin` and `no-clip` sets `mask-clip`", (t) => { + const mask = "url(foo.svg) view-box no-clip"; + const decl = h.declaration("mask", mask); + const element =
; + + t.deepEqual(computed(element, "mask-origin"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-clip"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "no-clip", + }, + ], + }, + source: decl.toJSON(), + }); +}); + +test("if one `` value and no `no-clip` keyword are present then `` sets both `mask-origin` and `mask-clip`", (t) => { + const mask = "url(foo.svg) view-box"; + const decl = h.declaration("mask", mask); + const element =
; + + t.deepEqual(computed(element, "mask-origin"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-clip"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + ], + }, + source: decl.toJSON(), + }); +}); + +test("longhands resolves correctly from shorthand with layers", (t) => { + const mask = "url(foo.svg) 50% 0% / 12px, url(bar.svg) 0% 50%"; + const decl = h.declaration("mask", mask); + const element =
; + + t.deepEqual(computed(element, "mask-image"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "url", + url: "foo.svg", + }, + }, + { + type: "image", + image: { + type: "url", + url: "bar.svg", + }, + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-position"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "side", + offset: { + type: "percentage", + value: 0.5, + }, + side: { + type: "keyword", + value: "left", + }, + }, + vertical: { + type: "side", + offset: { + type: "percentage", + value: 0, + }, + side: { + type: "keyword", + value: "top", + }, + }, + }, + { + type: "position", + horizontal: { + type: "side", + offset: { + type: "percentage", + value: 0, + }, + side: { + type: "keyword", + value: "left", + }, + }, + vertical: { + type: "side", + offset: { + type: "percentage", + value: 0.5, + }, + side: { + type: "keyword", + value: "top", + }, + }, + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-size"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "length", + unit: "px", + value: 12, + }, + ], + }, + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "auto", + }, + ], + }, + ], + }, + source: decl.toJSON(), + }); +}); diff --git a/packages/alfa-style/test/tsconfig.json b/packages/alfa-style/test/tsconfig.json index a4415680a6..40bb05d75e 100644 --- a/packages/alfa-style/test/tsconfig.json +++ b/packages/alfa-style/test/tsconfig.json @@ -62,6 +62,7 @@ "./property/mask-position.spec.tsx", "./property/mask-repeat.spec.tsx", "./property/mask-size.spec.tsx", + "./property/mask.spec.tsx", "./property/mix-blend-mode.spec.tsx", "./property/opacity.spec.tsx", "./property/outline.spec.tsx", From 029bbd8658d01102fa7b2b3f6d3c462a52a9e127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:40:52 +0100 Subject: [PATCH 26/40] Update few-clouds-play.md --- .changeset/few-clouds-play.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/few-clouds-play.md b/.changeset/few-clouds-play.md index d57ab69734..f9a7faff4e 100644 --- a/.changeset/few-clouds-play.md +++ b/.changeset/few-clouds-play.md @@ -2,4 +2,4 @@ "@siteimprove/alfa-style": minor --- -**Added:** CSS shorthand property `mask` and corresponding longhand properties is now supported. +**Added:** CSS shorthand property `mask` and corresponding longhand properties are now supported. From 7cee5bf8e5080caa19200f4781a5a35eb38143c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:46:26 +0100 Subject: [PATCH 27/40] Rename file --- .../src/property/helpers/{match-layers.ts => mask-layers.ts} | 0 packages/alfa-style/src/property/mask-clip.ts | 2 +- packages/alfa-style/src/property/mask-composite.ts | 2 +- packages/alfa-style/src/property/mask-mode.ts | 2 +- packages/alfa-style/src/property/mask-origin.ts | 2 +- packages/alfa-style/src/property/mask-position.ts | 2 +- packages/alfa-style/src/property/mask-repeat.ts | 2 +- packages/alfa-style/src/property/mask-size.ts | 2 +- packages/alfa-style/src/tsconfig.json | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) rename packages/alfa-style/src/property/helpers/{match-layers.ts => mask-layers.ts} (100%) diff --git a/packages/alfa-style/src/property/helpers/match-layers.ts b/packages/alfa-style/src/property/helpers/mask-layers.ts similarity index 100% rename from packages/alfa-style/src/property/helpers/match-layers.ts rename to packages/alfa-style/src/property/helpers/mask-layers.ts diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index e05318d69a..d58ae27706 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -3,7 +3,7 @@ import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./helpers/match-layers.js"; +import { matchLayers } from "./helpers/mask-layers.js"; const { either } = Parser; diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index bb7e80a783..e434a700c5 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -2,7 +2,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./helpers/match-layers.js"; +import { matchLayers } from "./helpers/mask-layers.js"; export type CompositingOperator = | Keyword<"add"> diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index 56cd0f4136..ce037782a0 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -2,7 +2,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./helpers/match-layers.js"; +import { matchLayers } from "./helpers/mask-layers.js"; export type MaskingMode = | Keyword<"alpha"> diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index de1e3f125e..f53c3ac653 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -2,7 +2,7 @@ import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./helpers/match-layers.js"; +import { matchLayers } from "./helpers/mask-layers.js"; type Specified = List; type Computed = Specified; diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 1dc321769b..747511d89a 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -8,7 +8,7 @@ import { import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -import { matchLayers } from "./helpers/match-layers.js"; +import { matchLayers } from "./helpers/mask-layers.js"; type Specified = List; type Computed = List; diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index 55504d02d6..2dfb1fd5f0 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -3,7 +3,7 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./helpers/match-layers.js"; +import { matchLayers } from "./helpers/mask-layers.js"; const { either } = Parser; diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index d00337da67..859aa54674 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -9,7 +9,7 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -import { matchLayers } from "./helpers/match-layers.js"; +import { matchLayers } from "./helpers/mask-layers.js"; const { either } = Parser; diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index 91bba34172..7dd2710b1d 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -39,7 +39,7 @@ "./predicate/is-block-container.ts", "./predicate/is-flex-container.ts", "./predicate/is-grid-container.ts", - "./property/helpers/match-layers.ts", + "./property/helpers/mask-layers.ts", "./property/background.ts", "./property/background-attachment.ts", "./property/background-clip.ts", From 6c928101d93349b5b17f84838fd1829564657bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:47:44 +0100 Subject: [PATCH 28/40] Update mask-layers.ts --- packages/alfa-style/src/property/helpers/mask-layers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/alfa-style/src/property/helpers/mask-layers.ts b/packages/alfa-style/src/property/helpers/mask-layers.ts index 3152284d09..48d548b613 100644 --- a/packages/alfa-style/src/property/helpers/mask-layers.ts +++ b/packages/alfa-style/src/property/helpers/mask-layers.ts @@ -10,8 +10,7 @@ import type { Style } from "../../style.js"; * A layer is created for each of the comma separated values for `mask-image`. * * If there are more values than layers, the excess values are discarded. - * Otherwise, the values must be repeated - * until the number of values matches the number of layers. + * Otherwise, the values must be repeated until the number of values matches the number of layers. */ export function matchLayers( value: List, From b7a779c0079de879351e0da6ef525a603cb42c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:33:43 +0100 Subject: [PATCH 29/40] Update packages/alfa-style/src/property/helpers/mask-layers.ts Co-authored-by: Jean-Yves Moyen --- packages/alfa-style/src/property/helpers/mask-layers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/alfa-style/src/property/helpers/mask-layers.ts b/packages/alfa-style/src/property/helpers/mask-layers.ts index 48d548b613..81eec9ac09 100644 --- a/packages/alfa-style/src/property/helpers/mask-layers.ts +++ b/packages/alfa-style/src/property/helpers/mask-layers.ts @@ -11,6 +11,8 @@ import type { Style } from "../../style.js"; * * If there are more values than layers, the excess values are discarded. * Otherwise, the values must be repeated until the number of values matches the number of layers. + * + * @internal */ export function matchLayers( value: List, From 8174e64e1e8a47bd71361a81a8a8c863b7113daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:22:44 +0100 Subject: [PATCH 30/40] Update packages/alfa-style/src/property/mask.ts Co-authored-by: Jean-Yves Moyen --- packages/alfa-style/src/property/mask.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/alfa-style/src/property/mask.ts b/packages/alfa-style/src/property/mask.ts index 5007d3b0fa..d75fe79a96 100644 --- a/packages/alfa-style/src/property/mask.ts +++ b/packages/alfa-style/src/property/mask.ts @@ -46,8 +46,8 @@ const parsePosAndSize: CSSParser<[Position, Option]> = pair( * As of December 2024 the specification uses the type in the shorthand defintion * whereas the longhands `mask-clip` and `mask-origin` uses . * These are not the same types - does not have `margin-box`. - * Chrome and Firefox does not, at time of writing, allow `margin-box` in the shorthand. - * Therefore we assume that the discrepancy is a spec-bug and that the intended type is . + * There is an open PR to fix that, which we follow. + * {@link https://github.com/w3c/fxtf-drafts/pull/552} */ const maskLayer: CSSParser< [ From 4f9c2d2f72d42fe2c7b85f4f6c8a71efb63a6d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:28:35 +0100 Subject: [PATCH 31/40] Add method for cutting or repeating an `alfa-css` `List` --- .../alfa-css/src/value/collection/list.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/alfa-css/src/value/collection/list.ts b/packages/alfa-css/src/value/collection/list.ts index 3a75b1f48d..4f42c570ab 100644 --- a/packages/alfa-css/src/value/collection/list.ts +++ b/packages/alfa-css/src/value/collection/list.ts @@ -44,6 +44,10 @@ export class List return this._values; } + public get size(): number { + return this._values.length; + } + public resolve( resolver?: Resolvable.Resolver, ): List> { @@ -65,6 +69,33 @@ export class List return new List(this._values.map(mapper), this._separator); } + /** + * Returns a copy of the current instance cut off or extended with repeated values to match the given `length`. + * + * @example + * List.of([1, 2, 3]).cutOrExtend(2); // returns a new List with values [1, 2] + * + * @example + * List.of([1, 2, 3]).cutOrExtend(5); // returns a new List with values [1, 2, 3, 1, 2] + */ + public cutOrExtend(length: number): List { + if (this.size === length) { + return new List(this._values, this._separator); + } + + if (length < this.size) { + return new List(this._values.slice(0, length), this._separator); + } + + const extended: Array = []; + for (let i = 0; i < length; ++i) { + // Cyclically repeat the values until the result has the desired length. + extended.push(this._values[i % this.size]); + } + + return new List(extended, this._separator); + } + public equals(value: List): boolean; public equals(value: unknown): value is this; From 56f48ac11d4aba9704a86e6d890b7f97d99df5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:16:40 +0100 Subject: [PATCH 32/40] Move layering logic --- .changeset/rare-lies-invent.md | 5 + .changeset/tame-ants-work.md | 5 + .../test/value/collection/list.spec.ts | 43 ++- .../src/property/helpers/mask-layers.ts | 40 --- packages/alfa-style/src/property/mask-clip.ts | 5 +- .../alfa-style/src/property/mask-composite.ts | 5 +- packages/alfa-style/src/property/mask-mode.ts | 5 +- .../alfa-style/src/property/mask-origin.ts | 5 +- .../alfa-style/src/property/mask-position.ts | 17 +- .../alfa-style/src/property/mask-repeat.ts | 5 +- packages/alfa-style/src/property/mask-size.ts | 14 +- packages/alfa-style/src/resolver.ts | 26 +- packages/alfa-style/src/tsconfig.json | 1 - .../test/property/mask-clip.spec.tsx | 97 ++----- .../test/property/mask-composite.spec.tsx | 126 +------- .../test/property/mask-image.spec.tsx | 186 ++++-------- .../test/property/mask-mode.spec.tsx | 115 +------- .../test/property/mask-origin.spec.tsx | 128 +-------- .../test/property/mask-position.spec.tsx | 244 +++++----------- .../test/property/mask-repeat.spec.tsx | 270 ++++-------------- .../test/property/mask-size.spec.tsx | 189 ++++-------- .../alfa-style/test/property/mask.spec.tsx | 172 ++--------- 22 files changed, 425 insertions(+), 1278 deletions(-) create mode 100644 .changeset/rare-lies-invent.md create mode 100644 .changeset/tame-ants-work.md delete mode 100644 packages/alfa-style/src/property/helpers/mask-layers.ts diff --git a/.changeset/rare-lies-invent.md b/.changeset/rare-lies-invent.md new file mode 100644 index 0000000000..8e00a347cd --- /dev/null +++ b/.changeset/rare-lies-invent.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-css": minor +--- + +**Added:** `List#cutOrExtend` is now available. diff --git a/.changeset/tame-ants-work.md b/.changeset/tame-ants-work.md new file mode 100644 index 0000000000..ff74dd7842 --- /dev/null +++ b/.changeset/tame-ants-work.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-css": minor +--- + +**Added:** `List#size` is now available. diff --git a/packages/alfa-css/test/value/collection/list.spec.ts b/packages/alfa-css/test/value/collection/list.spec.ts index f175a85847..af8bb1bbac 100644 --- a/packages/alfa-css/test/value/collection/list.spec.ts +++ b/packages/alfa-css/test/value/collection/list.spec.ts @@ -2,15 +2,13 @@ import type { Parser } from "@siteimprove/alfa-parser"; import type { Slice } from "@siteimprove/alfa-slice"; import { test } from "@siteimprove/alfa-test"; -import type { - Token, - Value} from "../../../dist/index.js"; +import type { Token, Value } from "../../../dist/index.js"; import { Length, LengthPercentage, Lexer, List, - Number + Number, } from "../../../dist/index.js"; function parse( @@ -68,3 +66,40 @@ test("resolve() resolves all values in a list", (t) => { separator: ", ", }); }); + +test("#cutOrExtend returns identical copy if list has the same length as the given length", (t) => { + t.deepEqual(parse("1, 2, 3", Number.parse).cutOrExtend(3).toJSON(), { + type: "list", + values: [ + { type: "number", value: 1 }, + { type: "number", value: 2 }, + { type: "number", value: 3 }, + ], + separator: ", ", + }); +}); + +test("#cutOrExtend cuts a list longer than the given length", (t) => { + t.deepEqual(parse("1, 2, 3", Number.parse).cutOrExtend(2).toJSON(), { + type: "list", + values: [ + { type: "number", value: 1 }, + { type: "number", value: 2 }, + ], + separator: ", ", + }); +}); + +test("#cutOrExtend repeats a list shorter than the given length", (t) => { + t.deepEqual(parse("1, 2, 3", Number.parse).cutOrExtend(5).toJSON(), { + type: "list", + values: [ + { type: "number", value: 1 }, + { type: "number", value: 2 }, + { type: "number", value: 3 }, + { type: "number", value: 1 }, + { type: "number", value: 2 }, + ], + separator: ", ", + }); +}); diff --git a/packages/alfa-style/src/property/helpers/mask-layers.ts b/packages/alfa-style/src/property/helpers/mask-layers.ts deleted file mode 100644 index 81eec9ac09..0000000000 --- a/packages/alfa-style/src/property/helpers/mask-layers.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { List, type Value } from "@siteimprove/alfa-css"; - -import type { Style } from "../../style.js"; - -/** - * {@link https://drafts.fxtf.org/css-masking/#layering}. - * - * @remarks - * The computed value depends on the number of layers. - * A layer is created for each of the comma separated values for `mask-image`. - * - * If there are more values than layers, the excess values are discarded. - * Otherwise, the values must be repeated until the number of values matches the number of layers. - * - * @internal - */ -export function matchLayers( - value: List, - style: Style, -): List { - const numberOfLayers = Math.max( - style.computed("mask-image").value.values.length, - 1, - ); - - const numberOfValues = value.values.length; - if (numberOfValues === numberOfLayers) { - return value; - } - - return List.of( - (numberOfLayers < numberOfValues - ? value.values - : Array(Math.ceil(numberOfLayers / numberOfValues)) - .fill(value.values) - .flat() - ).slice(0, numberOfLayers), - ", ", - ); -} diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index d58ae27706..2a3f7be718 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -2,8 +2,7 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; - -import { matchLayers } from "./helpers/mask-layers.js"; +import { Resolver } from "../resolver.js"; const { either } = Parser; @@ -22,5 +21,5 @@ export namespace MaskClip { export default Longhand.of( List.of([MaskClip.initialItem]), List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))), - (value, style) => value.map((value) => matchLayers(value, style)), + (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index e434a700c5..48362ca8d0 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -1,8 +1,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; - -import { matchLayers } from "./helpers/mask-layers.js"; +import { Resolver } from "../resolver.js"; export type CompositingOperator = | Keyword<"add"> @@ -31,5 +30,5 @@ type Computed = Specified; export default Longhand.of( List.of([CompositingOperator.initialItem], ", "), List.parseCommaSeparated(CompositingOperator.parse), - (value, style) => value.map((value) => matchLayers(value, style)), + (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index ce037782a0..2a013ca05b 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -1,8 +1,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; - -import { matchLayers } from "./helpers/mask-layers.js"; +import { Resolver } from "../resolver.js"; export type MaskingMode = | Keyword<"alpha"> @@ -29,5 +28,5 @@ type Computed = Specified; export default Longhand.of( List.of([MaskingMode.initialItem], ", "), List.parseCommaSeparated(MaskingMode.parse), - (value, style) => value.map((value) => matchLayers(value, style)), + (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index f53c3ac653..c335617955 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -1,8 +1,7 @@ import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; - -import { matchLayers } from "./helpers/mask-layers.js"; +import { Resolver } from "../resolver.js"; type Specified = List; type Computed = Specified; @@ -19,5 +18,5 @@ export namespace MaskOrigin { export default Longhand.of( List.of([MaskOrigin.initialItem], ", "), List.parseCommaSeparated(Box.parseCoordBox), - (value, style) => value.map((value) => matchLayers(value, style)), + (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 747511d89a..14da6fcd7a 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -8,8 +8,6 @@ import { import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -import { matchLayers } from "./helpers/mask-layers.js"; - type Specified = List; type Computed = List; @@ -28,13 +26,18 @@ export namespace MaskPosition { export default Longhand.of( List.of([MaskPosition.initialItem], ", "), List.parseCommaSeparated(Position.parse(/* legacySyntax */ true)), - (value, style) => - value.map((positions) => - matchLayers( + (value, style) => { + const layers = Resolver.layers( + style, + "mask-image", + ); + + return value.map((positions) => + layers( positions.map((position) => position.partiallyResolve(Resolver.length(style)), ), - style, ), - ), + ); + }, ); diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index 2dfb1fd5f0..d865fce605 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -2,8 +2,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; - -import { matchLayers } from "./helpers/mask-layers.js"; +import { Resolver } from "../resolver.js"; const { either } = Parser; @@ -47,5 +46,5 @@ type Computed = Specified; export default Longhand.of( List.of([RepeatStyle.initialItem], ", "), List.parseCommaSeparated(RepeatStyle.parse), - (value, style) => value.map((value) => matchLayers(value, style)), + (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index 859aa54674..d7e556b7e3 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -9,8 +9,6 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -import { matchLayers } from "./helpers/mask-layers.js"; - const { either } = Parser; export type BgSize = @@ -41,9 +39,11 @@ type Computed = Specified; export default Longhand.of( List.of([BgSize.initialItem], ", "), List.parseCommaSeparated(BgSize.parse), - (value, style) => - value.map((sizes) => - matchLayers( + (value, style) => { + const layers = Resolver.layers(style, "mask-image"); + + return value.map((sizes) => + layers( sizes.map((size) => Keyword.isKeyword(size) ? size @@ -55,7 +55,7 @@ export default Longhand.of( ), ), ), - style, ), - ), + ); + }, ); diff --git a/packages/alfa-style/src/resolver.ts b/packages/alfa-style/src/resolver.ts index 8172734a46..be3b53045a 100644 --- a/packages/alfa-style/src/resolver.ts +++ b/packages/alfa-style/src/resolver.ts @@ -1,5 +1,5 @@ -import type { LengthPercentage, Unit } from "@siteimprove/alfa-css"; -import { Length } from "@siteimprove/alfa-css"; +import type { LengthPercentage, Unit, Value } from "@siteimprove/alfa-css"; +import { Length, List } from "@siteimprove/alfa-css"; import type { Mapper } from "@siteimprove/alfa-mapper"; import type { Style } from "./style.js"; @@ -50,4 +50,26 @@ export namespace Resolver { ): LengthPercentage.Resolver { return { percentageBase: base, length: lengthResolver(style) }; } + + /** + * Resolve layers for properties that uses layering like background and mask. + * + * The number of layers is determined by the number of comma separated values + * in the property where the image is specified, i.e. `background-image` or `mask-image`. + * + * If there are more values than layers, the excess values are discarded. + * Otherwise, the values must be repeated until the number of values matches the number of layers. + * + * {@link https://www.w3.org/TR/css-backgrounds-3/#layering} + * {@link https://drafts.fxtf.org/css-masking/#layering}. + * + * @internal + */ + export function layers( + style: Style, + name: "mask-image" | "background-image", + ): Mapper, List> { + return (value) => + value.cutOrExtend(Math.max(style.computed(name).value.size, 1)); + } } diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index 7dd2710b1d..80e803580f 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -39,7 +39,6 @@ "./predicate/is-block-container.ts", "./predicate/is-flex-container.ts", "./predicate/is-grid-container.ts", - "./property/helpers/mask-layers.ts", "./property/background.ts", "./property/background-attachment.ts", "./property/background-clip.ts", diff --git a/packages/alfa-style/test/property/mask-clip.spec.tsx b/packages/alfa-style/test/property/mask-clip.spec.tsx index 54f3aae722..ab7474f101 100644 --- a/packages/alfa-style/test/property/mask-clip.spec.tsx +++ b/packages/alfa-style/test/property/mask-clip.spec.tsx @@ -4,18 +4,11 @@ import { h } from "@siteimprove/alfa-dom"; import { computed } from "../common.js"; test("initial value is border-box", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-clip"), { + t.deepEqual(computed(
, "mask-clip"), { value: { type: "list", separator: " ", - values: [ - { - type: "keyword", - value: "border-box", - }, - ], + values: [{ type: "keyword", value: "border-box" }], }, source: null, }); @@ -31,18 +24,11 @@ test("#computed parses single keywords", (t) => { "view-box", "no-clip", ] as const) { - const element =
; - - t.deepEqual(computed(element, "mask-clip"), { + t.deepEqual(computed(
, "mask-clip"), { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: kw, - }, - ], + values: [{ type: "keyword", value: kw }], }, source: h.declaration("mask-clip", kw).toJSON(), }); @@ -50,34 +36,29 @@ test("#computed parses single keywords", (t) => { }); test("#computed parses multiple layers", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-clip"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "padding-box", - }, - { - type: "keyword", - value: "no-clip", - }, - ], + t.deepEqual( + computed( +
, + "mask-clip", + ), + { + value: { + type: "list", + separator: ", ", + values: [ + { type: "keyword", value: "padding-box" }, + { type: "keyword", value: "no-clip" }, + ], + }, + source: h.declaration("mask-clip", "padding-box, no-clip").toJSON(), }, - source: h.declaration("mask-clip", "padding-box, no-clip").toJSON(), - }); + ); }); - test("#computed discards excess values when there are more values than layers", (t) => { const element = (
); - t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", values: [ - { - type: "keyword", - value: "view-box", - }, - { - type: "keyword", - value: "fill-box", - }, + { type: "keyword", value: "view-box" }, + { type: "keyword", value: "fill-box" }, ], }, source: h @@ -108,7 +82,6 @@ test("#computed discards excess values when there are more values than layers", .toJSON(), }); }); - test("#computed repeats values when there are more layers than values", (t) => { const element = (
{ }} >
); - t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", values: [ - { - type: "keyword", - value: "view-box", - }, - { - type: "keyword", - value: "fill-box", - }, - { - type: "keyword", - value: "view-box", - }, + { type: "keyword", value: "view-box" }, + { type: "keyword", value: "fill-box" }, + { type: "keyword", value: "view-box" }, ], }, source: h.declaration("mask-clip", "view-box, fill-box").toJSON(), diff --git a/packages/alfa-style/test/property/mask-composite.spec.tsx b/packages/alfa-style/test/property/mask-composite.spec.tsx index 7080334298..66072abec8 100644 --- a/packages/alfa-style/test/property/mask-composite.spec.tsx +++ b/packages/alfa-style/test/property/mask-composite.spec.tsx @@ -4,18 +4,11 @@ import { h } from "@siteimprove/alfa-dom"; import { computed } from "../common.js"; test("initial value is add", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-composite"), { + t.deepEqual(computed(
, "mask-composite"), { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "add", - }, - ], + values: [{ type: "keyword", value: "add" }], }, source: null, }); @@ -23,111 +16,16 @@ test("initial value is add", (t) => { test("#computed parses single keywords", (t) => { for (const kw of ["add", "subtract", "intersect", "exclude"] as const) { - const element =
; - - t.deepEqual(computed(element, "mask-composite"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: kw, - }, - ], + t.deepEqual( + computed(
, "mask-composite"), + { + value: { + type: "list", + separator: ", ", + values: [{ type: "keyword", value: kw }], + }, + source: h.declaration("mask-composite", kw).toJSON(), }, - source: h.declaration("mask-composite", kw).toJSON(), - }); + ); } }); - -test("#computed parses multiple layers", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-composite"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "add", - }, - { - type: "keyword", - value: "exclude", - }, - ], - }, - source: h.declaration("mask-composite", "add, exclude").toJSON(), - }); -}); - -test("#computed discards excess values when there are more values than layers", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-composite"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "add", - }, - { - type: "keyword", - value: "exclude", - }, - ], - }, - source: h.declaration("mask-composite", "add, exclude, intersect").toJSON(), - }); -}); - -test("#computed repeats values when there are more layers than values", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-composite"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "add", - }, - { - type: "keyword", - value: "exclude", - }, - { - type: "keyword", - value: "add", - }, - ], - }, - source: h.declaration("mask-composite", "add, exclude").toJSON(), - }); -}); diff --git a/packages/alfa-style/test/property/mask-image.spec.tsx b/packages/alfa-style/test/property/mask-image.spec.tsx index a50040eb72..2182d1b7a1 100644 --- a/packages/alfa-style/test/property/mask-image.spec.tsx +++ b/packages/alfa-style/test/property/mask-image.spec.tsx @@ -4,148 +4,86 @@ import { h } from "@siteimprove/alfa-dom"; import { computed } from "../common.js"; test("initial value is none", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-image"), { + t.deepEqual(computed(
, "mask-image"), { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "none", - }, - ], + values: [{ type: "keyword", value: "none" }], }, source: null, }); }); test("#computed parses url value", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-image"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "image", - image: { - type: "url", - url: "masks.svg#mask1", - }, - }, - ], + t.deepEqual( + computed( +
, + "mask-image", + ), + { + value: { + type: "list", + separator: ", ", + values: [ + { type: "image", image: { type: "url", url: "masks.svg#mask1" } }, + ], + }, + source: h.declaration("mask-image", "url(masks.svg#mask1)").toJSON(), }, - source: h.declaration("mask-image", "url(masks.svg#mask1)").toJSON(), - }); + ); }); test("#computed parses linear-gradient value", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-image"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "image", - image: { - type: "gradient", - kind: "linear", - direction: { - type: "side", - side: "bottom", - }, - items: [ - { - color: { - type: "color", - format: "rgb", - alpha: { - type: "percentage", - value: 1, - }, - red: { - type: "percentage", - value: 1, - }, - green: { - type: "percentage", - value: 0, - }, - blue: { - type: "percentage", - value: 0, + t.deepEqual( + computed( +
, + "mask-image", + ), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "gradient", + kind: "linear", + direction: { type: "side", side: "bottom" }, + items: [ + { + color: { + type: "color", + format: "rgb", + alpha: { type: "percentage", value: 1 }, + red: { type: "percentage", value: 1 }, + green: { type: "percentage", value: 0 }, + blue: { type: "percentage", value: 0 }, }, + position: null, + type: "stop", }, - position: null, - type: "stop", - }, - { - color: { - type: "color", - format: "rgb", - alpha: { - type: "percentage", - value: 1, - }, - red: { - type: "percentage", - value: 0, - }, - green: { - type: "percentage", - value: 0, - }, - blue: { - type: "percentage", - value: 1, + { + color: { + type: "color", + format: "rgb", + alpha: { type: "percentage", value: 1 }, + red: { type: "percentage", value: 0 }, + green: { type: "percentage", value: 0 }, + blue: { type: "percentage", value: 1 }, }, + position: null, + type: "stop", }, - position: null, - type: "stop", - }, - ], - repeats: false, + ], + repeats: false, + }, }, - }, - ], + ], + }, + source: h + .declaration("mask-image", "linear-gradient(red, blue)") + .toJSON(), }, - source: h.declaration("mask-image", "linear-gradient(red, blue)").toJSON(), - }); -}); - -test("#computed parses multiple layers", (t) => { - const element = ( -
); - - t.deepEqual(computed(element, "mask-image"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "image", - image: { - type: "url", - url: "foo.svg", - }, - }, - { - type: "image", - image: { - type: "url", - url: "bar.svg", - }, - }, - ], - }, - source: h.declaration("mask-image", "url(foo.svg), url(bar.svg)").toJSON(), - }); }); diff --git a/packages/alfa-style/test/property/mask-mode.spec.tsx b/packages/alfa-style/test/property/mask-mode.spec.tsx index 59e7d916a8..ac33c1767b 100644 --- a/packages/alfa-style/test/property/mask-mode.spec.tsx +++ b/packages/alfa-style/test/property/mask-mode.spec.tsx @@ -4,18 +4,11 @@ import { h } from "@siteimprove/alfa-dom"; import { computed } from "../common.js"; test("initial value is match-source", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-mode"), { + t.deepEqual(computed(
, "mask-mode"), { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "match-source", - }, - ], + values: [{ type: "keyword", value: "match-source" }], }, source: null, }); @@ -23,113 +16,13 @@ test("initial value is match-source", (t) => { test("#computed parses single keywords", (t) => { for (const kw of ["alpha", "luminance", "match-source"] as const) { - const element =
; - - t.deepEqual(computed(element, "mask-mode"), { + t.deepEqual(computed(
, "mask-mode"), { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: kw, - }, - ], + values: [{ type: "keyword", value: kw }], }, source: h.declaration("mask-mode", kw).toJSON(), }); } }); - -test("#computed parses multiple layers", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-mode"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "alpha", - }, - { - type: "keyword", - value: "match-source", - }, - ], - }, - source: h.declaration("mask-mode", "alpha, match-source").toJSON(), - }); -}); - -test("#computed discards excess values when there are more values than layers", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-mode"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "alpha", - }, - { - type: "keyword", - value: "match-source", - }, - ], - }, - source: h - .declaration("mask-mode", "alpha, match-source, luminance") - .toJSON(), - }); -}); - -test("#computed repeats values when there are more layers than values", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-mode"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "alpha", - }, - { - type: "keyword", - value: "match-source", - }, - { - type: "keyword", - value: "alpha", - }, - ], - }, - source: h.declaration("mask-mode", "alpha, match-source").toJSON(), - }); -}); diff --git a/packages/alfa-style/test/property/mask-origin.spec.tsx b/packages/alfa-style/test/property/mask-origin.spec.tsx index da10230917..6456902afa 100644 --- a/packages/alfa-style/test/property/mask-origin.spec.tsx +++ b/packages/alfa-style/test/property/mask-origin.spec.tsx @@ -4,18 +4,11 @@ import { h } from "@siteimprove/alfa-dom"; import { computed } from "../common.js"; test("initial value is border-box", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-origin"), { + t.deepEqual(computed(
, "mask-origin"), { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "border-box", - }, - ], + values: [{ type: "keyword", value: "border-box" }], }, source: null, }); @@ -30,113 +23,16 @@ test("#computed parses single keywords", (t) => { "stroke-box", "view-box", ] as const) { - const element =
; - - t.deepEqual(computed(element, "mask-origin"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: kw, - }, - ], + t.deepEqual( + computed(
, "mask-origin"), + { + value: { + type: "list", + separator: ", ", + values: [{ type: "keyword", value: kw }], + }, + source: h.declaration("mask-origin", kw).toJSON(), }, - source: h.declaration("mask-origin", kw).toJSON(), - }); + ); } }); - -test("#computed parses multiple layers", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-origin"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "content-box", - }, - { - type: "keyword", - value: "padding-box", - }, - ], - }, - source: h.declaration("mask-origin", "content-box, padding-box").toJSON(), - }); -}); - -test("#computed discards excess values when there are more values than layers", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-origin"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "content-box", - }, - { - type: "keyword", - value: "padding-box", - }, - ], - }, - source: h - .declaration("mask-origin", "content-box, padding-box, border-box") - .toJSON(), - }); -}); - -test("#computed repeats values when there are more layers than values", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-origin"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: "content-box", - }, - { - type: "keyword", - value: "padding-box", - }, - { - type: "keyword", - value: "content-box", - }, - ], - }, - source: h.declaration("mask-origin", "content-box, padding-box").toJSON(), - }); -}); diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx index c35669ebce..40bd9b3e02 100644 --- a/packages/alfa-style/test/property/mask-position.spec.tsx +++ b/packages/alfa-style/test/property/mask-position.spec.tsx @@ -4,9 +4,7 @@ import { h } from "@siteimprove/alfa-dom"; import { computed } from "../common.js"; test("initial value is 0% 0%", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-position"), { + t.deepEqual(computed(
, "mask-position"), { value: { type: "list", separator: ", ", @@ -15,25 +13,13 @@ test("initial value is 0% 0%", (t) => { type: "position", horizontal: { type: "side", - side: { - type: "keyword", - value: "left", - }, - offset: { - type: "percentage", - value: 0, - }, + side: { type: "keyword", value: "left" }, + offset: { type: "percentage", value: 0 }, }, vertical: { type: "side", - side: { - type: "keyword", - value: "top", - }, - offset: { - type: "percentage", - value: 0, - }, + side: { type: "keyword", value: "top" }, + offset: { type: "percentage", value: 0 }, }, }, ], @@ -46,182 +32,96 @@ test("initial value is 0% 0%", (t) => { // E.g. the keyword `left` should be computes to `0% 50%` in Chrome and Firefox. test("#computed parses single keywords", (t) => { for (const kw of ["top", "bottom"] as const) { - const element =
; - - t.deepEqual(computed(element, "mask-position"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "position", - horizontal: { - type: "keyword", - value: "center", - }, - vertical: { - type: "side", - offset: null, - side: { - type: "keyword", - value: kw, + t.deepEqual( + computed(
, "mask-position"), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { type: "keyword", value: "center" }, + vertical: { + type: "side", + offset: null, + side: { type: "keyword", value: kw }, }, }, - }, - ], + ], + }, + source: h.declaration("mask-position", kw).toJSON(), }, - source: h.declaration("mask-position", kw).toJSON(), - }); + ); } for (const kw of ["left", "right"] as const) { - const element =
; + t.deepEqual( + computed(
, "mask-position"), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + vertical: { type: "keyword", value: "center" }, + horizontal: { + type: "side", + offset: null, + side: { type: "keyword", value: kw }, + }, + }, + ], + }, + source: h.declaration("mask-position", kw).toJSON(), + }, + ); + } - t.deepEqual(computed(element, "mask-position"), { + t.deepEqual( + computed(
, "mask-position"), + { value: { type: "list", separator: ", ", values: [ { type: "position", - vertical: { - type: "keyword", - value: "center", - }, - horizontal: { - type: "side", - offset: null, - side: { - type: "keyword", - value: kw, - }, - }, + vertical: { type: "keyword", value: "center" }, + horizontal: { type: "keyword", value: "center" }, }, ], }, - source: h.declaration("mask-position", kw).toJSON(), - }); - } - - const element =
; - - t.deepEqual(computed(element, "mask-position"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "position", - vertical: { - type: "keyword", - value: "center", - }, - horizontal: { - type: "keyword", - value: "center", - }, - }, - ], + source: h.declaration("mask-position", "center").toJSON(), }, - source: h.declaration("mask-position", "center").toJSON(), - }); + ); }); test("#computed parses lengths and percentages", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-position"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "position", - horizontal: { - type: "side", - offset: { - type: "percentage", - value: 0.1, - }, - side: { - type: "keyword", - value: "left", - }, - }, - vertical: { - type: "side", - offset: { - type: "length", - unit: "px", - value: 48, + t.deepEqual( + computed(
, "mask-position"), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "side", + offset: { type: "percentage", value: 0.1 }, + side: { type: "keyword", value: "left" }, }, - side: { - type: "keyword", - value: "top", + vertical: { + type: "side", + offset: { type: "length", unit: "px", value: 48 }, + side: { type: "keyword", value: "top" }, }, }, - }, - ], + ], + }, + source: h.declaration("mask-position", "10% 3em").toJSON(), }, - source: h.declaration("mask-position", "10% 3em").toJSON(), - }); -}); - -test("#computed parses multiple layers", (t) => { - const element = ( -
); - - t.deepEqual(computed(element, "mask-position"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "position", - horizontal: { - type: "side", - offset: { - type: "length", - unit: "px", - value: 16, - }, - side: { - type: "keyword", - value: "left", - }, - }, - vertical: { - type: "side", - offset: { - type: "length", - unit: "px", - value: 16, - }, - side: { - type: "keyword", - value: "top", - }, - }, - }, - { - type: "position", - horizontal: { - type: "keyword", - value: "center", - }, - vertical: { - type: "keyword", - value: "center", - }, - }, - ], - }, - source: h.declaration("mask-position", "1rem 1rem, center").toJSON(), - }); }); diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index 34816be52e..9e52655b7d 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -4,21 +4,14 @@ import { h } from "@siteimprove/alfa-dom"; import { computed } from "../common.js"; test("initial value is repeat", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-repeat"), { + t.deepEqual(computed(
, "mask-repeat"), { value: { type: "list", separator: ", ", values: [ { type: "list", - values: [ - { - type: "keyword", - value: "repeat", - }, - ], + values: [{ type: "keyword", value: "repeat" }], separator: " ", }, ], @@ -29,27 +22,44 @@ test("initial value is repeat", (t) => { test("#computed parses single keywords", (t) => { for (const kw of ["repeat-x", "repeat-y"] as const) { - const element =
; - - t.deepEqual(computed(element, "mask-repeat"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "keyword", - value: kw, - }, - ], + t.deepEqual( + computed(
, "mask-repeat"), + { + value: { + type: "list", + separator: ", ", + values: [{ type: "keyword", value: kw }], + }, + source: h.declaration("mask-repeat", kw).toJSON(), }, - source: h.declaration("mask-repeat", kw).toJSON(), - }); + ); } for (const kw of ["repeat", "space", "round", "no-repeat"] as const) { - const element =
; + t.deepEqual( + computed(
, "mask-repeat"), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [{ type: "keyword", value: kw }], + }, + ], + }, + source: h.declaration("mask-repeat", kw).toJSON(), + }, + ); + } +}); - t.deepEqual(computed(element, "mask-repeat"), { +test("#computed parses at most two space separated values", (t) => { + t.deepEqual( + computed(
, "mask-repeat"), + { value: { type: "list", separator: ", ", @@ -58,202 +68,34 @@ test("#computed parses single keywords", (t) => { type: "list", separator: " ", values: [ - { - type: "keyword", - value: kw, - }, + { type: "keyword", value: "repeat" }, + { type: "keyword", value: "space" }, ], }, ], }, - source: h.declaration("mask-repeat", kw).toJSON(), - }); - } -}); - -test("#computed parses at most two space separated values", (t) => { - const element1 =
; - t.deepEqual(computed(element1, "mask-repeat"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "repeat", - }, - { - type: "keyword", - value: "space", - }, - ], - }, - ], - }, - source: h.declaration("mask-repeat", "repeat space").toJSON(), - }); - - const element2 =
; - t.deepEqual(computed(element2, "mask-repeat"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "repeat", - }, - ], - }, - ], + source: h.declaration("mask-repeat", "repeat space").toJSON(), }, - source: null, - }); -}); - -test("#computed parses mutiple layers", (t) => { - const element = ( -
); - t.deepEqual(computed(element, "mask-repeat"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "round", - }, - { - type: "keyword", - value: "repeat", - }, - ], - }, - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "space", - }, - ], - }, - ], - }, - source: h.declaration("mask-repeat", "round repeat, space").toJSON(), - }); -}); - -test("#computed discards excess values when there are more values than layers", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-repeat"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "round", - }, - { - type: "keyword", - value: "repeat", - }, - ], - }, - ], + t.deepEqual( + computed( +
, + "mask-repeat", + ), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [{ type: "keyword", value: "repeat" }], + }, + ], + }, + source: null, }, - source: h.declaration("mask-repeat", "round repeat, space").toJSON(), - }); -}); - -test("#computed repeats values when there are more layers than values", (t) => { - const element = ( -
); - - t.deepEqual(computed(element, "mask-repeat"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "round", - }, - { - type: "keyword", - value: "repeat", - }, - ], - }, - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "space", - }, - ], - }, - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "round", - }, - { - type: "keyword", - value: "repeat", - }, - ], - }, - ], - }, - source: h.declaration("mask-repeat", "round repeat, space").toJSON(), - }); }); diff --git a/packages/alfa-style/test/property/mask-size.spec.tsx b/packages/alfa-style/test/property/mask-size.spec.tsx index 08d3ead997..155c59abb8 100644 --- a/packages/alfa-style/test/property/mask-size.spec.tsx +++ b/packages/alfa-style/test/property/mask-size.spec.tsx @@ -4,59 +4,40 @@ import { h } from "@siteimprove/alfa-dom"; import { computed } from "../common.js"; test("initial value is auto", (t) => { - const element = ( -
- ); - - t.deepEqual(computed(element, "mask-size"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "auto", - }, - ], - }, - { - type: "list", - separator: " ", - values: [ - { - type: "keyword", - value: "auto", - }, - ], - }, - ], + t.deepEqual( + computed( +
, + "mask-size", + ), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [{ type: "keyword", value: "auto" }], + }, + { + type: "list", + separator: " ", + values: [{ type: "keyword", value: "auto" }], + }, + ], + }, + source: null, }, - source: null, - }); + ); }); test("#computed parses single keywords", (t) => { for (const kw of ["cover", "contain"] as const) { - const element =
; - - t.deepEqual(computed(element, "mask-size"), { + t.deepEqual(computed(
, "mask-size"), { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: kw, - }, - ], + values: [{ type: "keyword", value: kw }], }, source: h.declaration("mask-size", kw).toJSON(), }); @@ -64,9 +45,7 @@ test("#computed parses single keywords", (t) => { }); test("#computed parses percentage width", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-size"), { + t.deepEqual(computed(
, "mask-size"), { value: { type: "list", separator: ", ", @@ -74,12 +53,7 @@ test("#computed parses percentage width", (t) => { { type: "list", separator: " ", - values: [ - { - type: "percentage", - value: 0.5, - }, - ], + values: [{ type: "percentage", value: 0.5 }], }, ], }, @@ -88,9 +62,7 @@ test("#computed parses percentage width", (t) => { }); test("#computed resolves em width", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-size"), { + t.deepEqual(computed(
, "mask-size"), { value: { type: "list", separator: ", ", @@ -98,13 +70,7 @@ test("#computed resolves em width", (t) => { { type: "list", separator: " ", - values: [ - { - type: "length", - unit: "px", - value: 48, - }, - ], + values: [{ type: "length", unit: "px", value: 48 }], }, ], }, @@ -113,9 +79,7 @@ test("#computed resolves em width", (t) => { }); test("#computed parses pixel width", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-size"), { + t.deepEqual(computed(
, "mask-size"), { value: { type: "list", separator: ", ", @@ -123,13 +87,7 @@ test("#computed parses pixel width", (t) => { { type: "list", separator: " ", - values: [ - { - type: "length", - unit: "px", - value: 12, - }, - ], + values: [{ type: "length", unit: "px", value: 12 }], }, ], }, @@ -138,71 +96,24 @@ test("#computed parses pixel width", (t) => { }); test("#computed parses width and height", (t) => { - const element =
; - - t.deepEqual(computed(element, "mask-size"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "list", - separator: " ", - values: [ - { - type: "length", - unit: "px", - value: 48, - }, - { - type: "percentage", - value: 0.25, - }, - ], - }, - ], + t.deepEqual( + computed(
, "mask-size"), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { type: "length", unit: "px", value: 48 }, + { type: "percentage", value: 0.25 }, + ], + }, + ], + }, + source: h.declaration("mask-size", "3em 25%").toJSON(), }, - source: h.declaration("mask-size", "3em 25%").toJSON(), - }); -}); - -test("#computed parses multiple layers", (t) => { - const element = ( -
); - - t.deepEqual(computed(element, "mask-size"), { - value: { - type: "list", - separator: ", ", - values: [ - { - type: "list", - separator: " ", - values: [ - { - type: "percentage", - value: 0.5, - }, - ], - }, - { - type: "list", - separator: " ", - values: [ - { - type: "percentage", - value: 0.25, - }, - ], - }, - ], - }, - source: h.declaration("mask-size", "50%, 25%").toJSON(), - }); }); diff --git a/packages/alfa-style/test/property/mask.spec.tsx b/packages/alfa-style/test/property/mask.spec.tsx index 73e87fbfb2..d6cae97cf5 100644 --- a/packages/alfa-style/test/property/mask.spec.tsx +++ b/packages/alfa-style/test/property/mask.spec.tsx @@ -13,15 +13,7 @@ test("longhands resolve correctly from shorthand", (t) => { value: { type: "list", separator: ", ", - values: [ - { - type: "image", - image: { - type: "url", - url: "foo.svg", - }, - }, - ], + values: [{ type: "image", image: { type: "url", url: "foo.svg" } }], }, source: decl.toJSON(), }); @@ -35,25 +27,13 @@ test("longhands resolve correctly from shorthand", (t) => { type: "position", horizontal: { type: "side", - offset: { - type: "percentage", - value: 0.5, - }, - side: { - type: "keyword", - value: "left", - }, + offset: { type: "percentage", value: 0.5 }, + side: { type: "keyword", value: "left" }, }, vertical: { type: "side", - offset: { - type: "percentage", - value: 0, - }, - side: { - type: "keyword", - value: "top", - }, + offset: { type: "percentage", value: 0 }, + side: { type: "keyword", value: "top" }, }, }, ], @@ -69,13 +49,7 @@ test("longhands resolve correctly from shorthand", (t) => { { type: "list", separator: " ", - values: [ - { - type: "length", - unit: "px", - value: 12, - }, - ], + values: [{ type: "length", unit: "px", value: 12 }], }, ], }, @@ -86,12 +60,7 @@ test("longhands resolve correctly from shorthand", (t) => { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "repeat-x", - }, - ], + values: [{ type: "keyword", value: "repeat-x" }], }, source: decl.toJSON(), }); @@ -100,12 +69,7 @@ test("longhands resolve correctly from shorthand", (t) => { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "view-box", - }, - ], + values: [{ type: "keyword", value: "view-box" }], }, source: decl.toJSON(), }); @@ -114,12 +78,7 @@ test("longhands resolve correctly from shorthand", (t) => { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "padding-box", - }, - ], + values: [{ type: "keyword", value: "padding-box" }], }, source: decl.toJSON(), }); @@ -128,12 +87,7 @@ test("longhands resolve correctly from shorthand", (t) => { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "subtract", - }, - ], + values: [{ type: "keyword", value: "subtract" }], }, source: decl.toJSON(), }); @@ -142,12 +96,7 @@ test("longhands resolve correctly from shorthand", (t) => { value: { type: "list", separator: ", ", - values: [ - { - type: "keyword", - value: "luminance", - }, - ], + values: [{ type: "keyword", value: "luminance" }], }, source: decl.toJSON(), }); @@ -162,12 +111,7 @@ test("if one `` value and the `no-clip` keyword are present then `` value and the `no-clip` keyword are present then `` value and no `no-clip` keyword are present then `` value and no `no-clip` keyword are present then ` { type: "list", separator: ", ", values: [ - { - type: "image", - image: { - type: "url", - url: "foo.svg", - }, - }, - { - type: "image", - image: { - type: "url", - url: "bar.svg", - }, - }, + { type: "image", image: { type: "url", url: "foo.svg" } }, + { type: "image", image: { type: "url", url: "bar.svg" } }, ], }, source: decl.toJSON(), @@ -259,50 +176,26 @@ test("longhands resolves correctly from shorthand with layers", (t) => { type: "position", horizontal: { type: "side", - offset: { - type: "percentage", - value: 0.5, - }, - side: { - type: "keyword", - value: "left", - }, + offset: { type: "percentage", value: 0.5 }, + side: { type: "keyword", value: "left" }, }, vertical: { type: "side", - offset: { - type: "percentage", - value: 0, - }, - side: { - type: "keyword", - value: "top", - }, + offset: { type: "percentage", value: 0 }, + side: { type: "keyword", value: "top" }, }, }, { type: "position", horizontal: { type: "side", - offset: { - type: "percentage", - value: 0, - }, - side: { - type: "keyword", - value: "left", - }, + offset: { type: "percentage", value: 0 }, + side: { type: "keyword", value: "left" }, }, vertical: { type: "side", - offset: { - type: "percentage", - value: 0.5, - }, - side: { - type: "keyword", - value: "top", - }, + offset: { type: "percentage", value: 0.5 }, + side: { type: "keyword", value: "top" }, }, }, ], @@ -318,23 +211,12 @@ test("longhands resolves correctly from shorthand with layers", (t) => { { type: "list", separator: " ", - values: [ - { - type: "length", - unit: "px", - value: 12, - }, - ], + values: [{ type: "length", unit: "px", value: 12 }], }, { type: "list", separator: " ", - values: [ - { - type: "keyword", - value: "auto", - }, - ], + values: [{ type: "keyword", value: "auto" }], }, ], }, From c51a4c07ed22b161e5092a8201a70ac3972b07d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:19:20 +0100 Subject: [PATCH 33/40] Align with existing code structure --- packages/alfa-style/src/property/mask-clip.ts | 28 ++++- .../alfa-style/src/property/mask-composite.ts | 44 ++++--- .../alfa-style/src/property/mask-image.ts | 44 ++++--- packages/alfa-style/src/property/mask-mode.ts | 41 ++++--- .../alfa-style/src/property/mask-origin.ts | 28 ++++- .../alfa-style/src/property/mask-position.ts | 36 ++++-- .../alfa-style/src/property/mask-repeat.ts | 61 ++++++---- packages/alfa-style/src/property/mask-size.ts | 58 +++++---- packages/alfa-style/src/property/mask.ts | 113 +++++++----------- .../test/property/mask-clip.spec.tsx | 86 +++++++------ 10 files changed, 307 insertions(+), 232 deletions(-) diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index 2a3f7be718..525c97724b 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -6,20 +6,36 @@ import { Resolver } from "../resolver.js"; const { either } = Parser; -type Specified = List>; -type Computed = Specified; +type Specified = List; -export namespace MaskClip { - export const initialItem = Keyword.of("border-box"); +/** + * @internal + */ +export namespace Specified { + export type Item = Box.CoordBox | Keyword<"no-clip">; } +type Computed = Specified; + +/** + * @internal + */ +export const parse = either(Box.parseCoordBox, Keyword.parse("no-clip")); + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = Keyword.of("border-box"); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-clip} * * @internal */ export default Longhand.of( - List.of([MaskClip.initialItem]), - List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))), + List.of([initialItem]), + parseList, (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index 48362ca8d0..ff9c29b8a4 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -1,34 +1,42 @@ -import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; +import { Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -export type CompositingOperator = - | Keyword<"add"> - | Keyword<"subtract"> - | Keyword<"intersect"> - | Keyword<"exclude">; - -export namespace CompositingOperator { - export const parse: CSSParser = Keyword.parse( - "add", - "subtract", - "intersect", - "exclude", - ); - export const initialItem = Keyword.of("add"); +type Specified = List; + +/** + * @internal + */ +export namespace Specified { + export type Item = + | Keyword<"add"> + | Keyword<"subtract"> + | Keyword<"intersect"> + | Keyword<"exclude">; } -type Specified = List; type Computed = Specified; +/** + * @internal + */ +export const parse = Keyword.parse("add", "subtract", "intersect", "exclude"); + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = Keyword.of("add"); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite} * * @internal */ export default Longhand.of( - List.of([CompositingOperator.initialItem], ", "), - List.parseCommaSeparated(CompositingOperator.parse), + List.of([initialItem], ", "), + parseList, (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-image.ts b/packages/alfa-style/src/property/mask-image.ts index 7111fb511c..845585bbf5 100644 --- a/packages/alfa-style/src/property/mask-image.ts +++ b/packages/alfa-style/src/property/mask-image.ts @@ -1,40 +1,46 @@ import { Parser } from "@siteimprove/alfa-parser"; -import { - Image, - Keyword, - List, - URL, - type Parser as CSSParser, -} from "@siteimprove/alfa-css"; +import { Image, Keyword, List, URL } from "@siteimprove/alfa-css"; import { Selective } from "@siteimprove/alfa-selective"; -const { either } = Parser; - import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -export type MaskReference = Keyword<"none"> | Image | URL; +const { either } = Parser; -export namespace MaskReference { - export const parse: CSSParser = either( - Keyword.parse("none"), - either(Image.parse, URL.parse), - ); +type Specified = List; - export const initialItem = Keyword.of("none"); +/** + * @internal + */ +export namespace Specified { + export type Item = Keyword<"none"> | Image | URL; } -type Specified = List; type Computed = Specified; +/** + * @internal + */ +export const parse = either( + Keyword.parse("none"), + either(Image.parse, URL.parse), +); + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = Keyword.of("none"); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image} * * @internal */ export default Longhand.of( - List.of([MaskReference.initialItem], ", "), - List.parseCommaSeparated(MaskReference.parse), + List.of([initialItem], ", "), + parseList, (value, style) => value.map((images) => images.map((image) => diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index 2a013ca05b..76d5d88282 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -1,32 +1,41 @@ -import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; +import { Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -export type MaskingMode = - | Keyword<"alpha"> - | Keyword<"luminance"> - | Keyword<"match-source">; - -export namespace MaskingMode { - export const parse: CSSParser = Keyword.parse( - "alpha", - "luminance", - "match-source", - ); - export const initialItem = Keyword.of("match-source"); +type Specified = List; + +/** + * @internal + */ +export namespace Specified { + export type Item = + | Keyword<"alpha"> + | Keyword<"luminance"> + | Keyword<"match-source">; } -type Specified = List; type Computed = Specified; +/** + * @internal + */ +export const parse = Keyword.parse("alpha", "luminance", "match-source"); + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = Keyword.of("match-source"); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-mode} * * @internal */ export default Longhand.of( - List.of([MaskingMode.initialItem], ", "), - List.parseCommaSeparated(MaskingMode.parse), + List.of([initialItem], ", "), + parseList, (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index c335617955..d968516f52 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -3,20 +3,36 @@ import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -type Specified = List; -type Computed = Specified; +type Specified = List; -export namespace MaskOrigin { - export const initialItem = Keyword.of("border-box"); +/** + * @internal + */ +export namespace Specified { + export type Item = Box.CoordBox; } +type Computed = Specified; + +/** + * @internal + */ +export const parse = Box.parseCoordBox; + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = Keyword.of("border-box"); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-origin} * * @internal */ export default Longhand.of( - List.of([MaskOrigin.initialItem], ", "), - List.parseCommaSeparated(Box.parseCoordBox), + List.of([initialItem], ", "), + parseList, (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 14da6fcd7a..8d4b11e0c3 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -8,24 +8,40 @@ import { import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -type Specified = List; -type Computed = List; - -export namespace MaskPosition { - export const initialItem = Position.of( - Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), - Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), - ); +type Specified = List; + +/** + * @internal + */ +export namespace Specified { + export type Item = Position; } +type Computed = Specified; + +/** + * @internal + */ +export const parse = Position.parse(/* legacySyntax */ true); + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = Position.of( + Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), + Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), +); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-position} * * @internal */ export default Longhand.of( - List.of([MaskPosition.initialItem], ", "), - List.parseCommaSeparated(Position.parse(/* legacySyntax */ true)), + List.of([initialItem], ", "), + parseList, (value, style) => { const layers = Resolver.layers( style, diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index d865fce605..afcf28540e 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -1,4 +1,4 @@ -import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; +import { Keyword, List } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; @@ -6,31 +6,44 @@ import { Resolver } from "../resolver.js"; const { either } = Parser; -export type RepeatStyle = - | Keyword<"repeat-x"> - | Keyword<"repeat-y"> - | List< - | Keyword<"repeat"> - | Keyword<"space"> - | Keyword<"round"> - | Keyword<"no-repeat"> - >; - -export namespace RepeatStyle { - export const parse: CSSParser = either( - Keyword.parse("repeat-x", "repeat-y"), - List.parseSpaceSeparated( - Keyword.parse("repeat", "space", "round", "no-repeat"), - 1, - 2, - ), - ); - export const initialItem = List.of([Keyword.of("repeat")], " "); +type Specified = List; + +/** + * @internal + */ +export namespace Specified { + export type Item = + | Keyword<"repeat-x"> + | Keyword<"repeat-y"> + | List< + | Keyword<"repeat"> + | Keyword<"space"> + | Keyword<"round"> + | Keyword<"no-repeat"> + >; } -type Specified = List; type Computed = Specified; +/** + * @internal + */ +export const parse = either( + Keyword.parse("repeat-x", "repeat-y"), + List.parseSpaceSeparated( + Keyword.parse("repeat", "space", "round", "no-repeat"), + 1, + 2, + ), +); + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = List.of([Keyword.of("repeat")], " "); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-repeat} * @@ -44,7 +57,7 @@ type Computed = Specified; * @internal */ export default Longhand.of( - List.of([RepeatStyle.initialItem], ", "), - List.parseCommaSeparated(RepeatStyle.parse), + List.of([initialItem], ", "), + parseList, (value, style) => value.map(Resolver.layers(style, "mask-image")), ); diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index d7e556b7e3..1578385ab3 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -1,9 +1,4 @@ -import { - Keyword, - LengthPercentage, - List, - type Parser as CSSParser, -} from "@siteimprove/alfa-css"; +import { Keyword, LengthPercentage, List } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; @@ -11,36 +6,49 @@ import { Resolver } from "../resolver.js"; const { either } = Parser; -export type BgSize = - | List> - | Keyword<"cover"> - | Keyword<"contain">; - -export namespace BgSize { - export const parse: CSSParser = either( - List.parseSpaceSeparated( - either(LengthPercentage.parse, Keyword.parse("auto")), - 1, - 2, - ), - Keyword.parse("cover", "contain"), - ); - export const initialItem = List.of([Keyword.of("auto")], " "); +type Specified = List; + +/** + * @internal + */ +export namespace Specified { + export type Item = + | List> + | Keyword<"cover"> + | Keyword<"contain">; } -type Specified = List; type Computed = Specified; +/** + * @internal + */ +export const parse = either( + List.parseSpaceSeparated( + either(LengthPercentage.parse, Keyword.parse("auto")), + 1, + 2, + ), + Keyword.parse("cover", "contain"), +); + +const parseList = List.parseCommaSeparated(parse); + +/** + * @internal + */ +export const initialItem = List.of([Keyword.of("auto")], " "); + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-size} * * @internal */ export default Longhand.of( - List.of([BgSize.initialItem], ", "), - List.parseCommaSeparated(BgSize.parse), + List.of([initialItem], ", "), + parseList, (value, style) => { - const layers = Resolver.layers(style, "mask-image"); + const layers = Resolver.layers(style, "mask-image"); return value.map((sizes) => layers( diff --git a/packages/alfa-style/src/property/mask.ts b/packages/alfa-style/src/property/mask.ts index 5007d3b0fa..f382f1d44b 100644 --- a/packages/alfa-style/src/property/mask.ts +++ b/packages/alfa-style/src/property/mask.ts @@ -1,10 +1,6 @@ import { Token, List, - type Parser as CSSParser, - Position, - Box, - Keyword, } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import type { Slice } from "@siteimprove/alfa-slice"; @@ -12,32 +8,21 @@ import { Option } from "@siteimprove/alfa-option"; import { Shorthand } from "../shorthand.js"; -import { MaskReference } from "./mask-image.js"; -import { BgSize } from "./mask-size.js"; -import { RepeatStyle } from "./mask-repeat.js"; -import { CompositingOperator } from "./mask-composite.js"; -import { MaskingMode } from "./mask-mode.js"; -import { MaskPosition } from "./mask-position.js"; -import { MaskClip } from "./mask-clip.js"; -import { MaskOrigin } from "./mask-origin.js"; +import * as Reference from "./mask-image.js"; +import * as Size from "./mask-size.js"; +import * as Repeat from "./mask-repeat.js"; +import * as Mode from "./mask-mode.js"; +import * as Position from "./mask-position.js"; +import * as Origin from "./mask-origin.js"; +import * as Clip from "./mask-clip.js"; +import * as Composite from "./mask-composite.js"; -const { - doubleBar, - either, - map, - option, - pair, - right, - delimited, - separatedList, -} = Parser; +const { doubleBar, map, option, pair, right, delimited, separatedList } = + Parser; const slash = delimited(option(Token.parseWhitespace), Token.parseDelim("/")); -const parsePosAndSize: CSSParser<[Position, Option]> = pair( - Position.parse(/* legacySyntax */ true), - option(right(slash, BgSize.parse)), -); +const parsePosAndSize = pair(Position.parse, option(right(slash, Size.parse))); /** * {@link https://drafts.fxtf.org/css-masking/#typedef-mask-layer} @@ -49,39 +34,28 @@ const parsePosAndSize: CSSParser<[Position, Option]> = pair( * Chrome and Firefox does not, at time of writing, allow `margin-box` in the shorthand. * Therefore we assume that the discrepancy is a spec-bug and that the intended type is . */ -const maskLayer: CSSParser< - [ - MaskReference | undefined, - Position | undefined, - BgSize | undefined, - RepeatStyle | undefined, - Box.CoordBox | undefined, - Box.CoordBox | Keyword<"no-clip"> | undefined, - CompositingOperator | undefined, - MaskingMode | undefined, - ] -> = map( +const parse = map( doubleBar< Slice, [ - MaskReference, - [Position, Option], - RepeatStyle, - Box.CoordBox, - Box.CoordBox | Keyword<"no-clip">, - CompositingOperator, - MaskingMode, + Reference.Specified.Item, + [Position.Specified.Item, Option], + Repeat.Specified.Item, + Origin.Specified.Item, + Clip.Specified.Item, + Composite.Specified.Item, + Mode.Specified.Item, ], string >( Token.parseWhitespace, - MaskReference.parse, + Reference.parse, parsePosAndSize, - RepeatStyle.parse, - Box.parseCoordBox, - either(Box.parseCoordBox, Keyword.parse("no-clip")), - CompositingOperator.parse, - MaskingMode.parse, + Repeat.parse, + Origin.parse, + Clip.parse, + Composite.parse, + Mode.parse, ), ([image, posAndSize, repeat, box1, box2, composite, mode]) => { const [pos, size] = @@ -96,13 +70,14 @@ const maskLayer: CSSParser< }, ); +// List.parseCommaSeparated(parse); const parseList = separatedList( - maskLayer, + parse, delimited(option(Token.parseWhitespace), Token.parseComma), ); /** - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/border-top} + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask} * * @internal */ @@ -118,25 +93,25 @@ export default Shorthand.of( "mask-mode", ], map(parseList, (layers) => { - const images: Array = []; - const positions: Array = []; - const sizes: Array = []; - const repeats: Array = []; - const origins: Array = []; - const clips: Array> = []; - const composites: Array = []; - const modes: Array = []; + const images: Array = []; + const positions: Array = []; + const sizes: Array = []; + const repeats: Array = []; + const origins: Array = []; + const clips: Array = []; + const composites: Array = []; + const modes: Array = []; for (const layer of layers) { const [image, pos, size, repeat, origin, clip, composite, mode] = layer; - images.push(image ?? MaskReference.initialItem); - positions.push(pos ?? MaskPosition.initialItem); - sizes.push(size ?? BgSize.initialItem); - repeats.push(repeat ?? RepeatStyle.initialItem); - origins.push(origin ?? MaskOrigin.initialItem); - clips.push(clip ?? MaskClip.initialItem); - composites.push(composite ?? CompositingOperator.initialItem); - modes.push(mode ?? MaskingMode.initialItem); + images.push(image ?? Reference.initialItem); + positions.push(pos ?? Position.initialItem); + sizes.push(size ?? Size.initialItem); + repeats.push(repeat ?? Repeat.initialItem); + origins.push(origin ?? Origin.initialItem); + clips.push(clip ?? Clip.initialItem); + composites.push(composite ?? Composite.initialItem); + modes.push(mode ?? Mode.initialItem); } return [ diff --git a/packages/alfa-style/test/property/mask-clip.spec.tsx b/packages/alfa-style/test/property/mask-clip.spec.tsx index ab7474f101..3f64d8dc0e 100644 --- a/packages/alfa-style/test/property/mask-clip.spec.tsx +++ b/packages/alfa-style/test/property/mask-clip.spec.tsx @@ -59,48 +59,56 @@ test("#computed parses multiple layers", (t) => { }, ); }); + test("#computed discards excess values when there are more values than layers", (t) => { - const element = ( -
- ); - t.deepEqual(computed(element, "mask-clip"), { - value: { - type: "list", - separator: ", ", - values: [ - { type: "keyword", value: "view-box" }, - { type: "keyword", value: "fill-box" }, - ], + t.deepEqual( + computed( +
, + "mask-clip", + ), + { + value: { + type: "list", + separator: ", ", + values: [ + { type: "keyword", value: "view-box" }, + { type: "keyword", value: "fill-box" }, + ], + }, + source: h + .declaration("mask-clip", "view-box, fill-box, border-box") + .toJSON(), }, - source: h - .declaration("mask-clip", "view-box, fill-box, border-box") - .toJSON(), - }); + ); }); + test("#computed repeats values when there are more layers than values", (t) => { - const element = ( -
- ); - t.deepEqual(computed(element, "mask-clip"), { - value: { - type: "list", - separator: ", ", - values: [ - { type: "keyword", value: "view-box" }, - { type: "keyword", value: "fill-box" }, - { type: "keyword", value: "view-box" }, - ], + t.deepEqual( + computed( +
, + "mask-clip", + ), + { + value: { + type: "list", + separator: ", ", + values: [ + { type: "keyword", value: "view-box" }, + { type: "keyword", value: "fill-box" }, + { type: "keyword", value: "view-box" }, + ], + }, + source: h.declaration("mask-clip", "view-box, fill-box").toJSON(), }, - source: h.declaration("mask-clip", "view-box, fill-box").toJSON(), - }); + ); }); From e7bcbd18900795d11249f786d2b596fe194df8a2 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:21:16 +0000 Subject: [PATCH 34/40] Extract API --- docs/review/api/alfa-css.api.md | 3 +++ docs/review/api/alfa-style.api.md | 33 ++++++++++++++++--------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index 36abe1963f..96329c2336 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -1258,6 +1258,7 @@ export namespace Lexer { export class List extends Value<"list", Value.HasCalculation<[V]>> implements Iterable_2, Resolvable>, Resolvable.Resolver>, PartiallyResolvable>, Resolvable.PartialResolver> { // (undocumented) [Symbol.iterator](): Iterator; + cutOrExtend(length: number): List; // (undocumented) equals(value: List): boolean; // (undocumented) @@ -1273,6 +1274,8 @@ export class List extends Value<"list", Value.HasCalculation<[V // (undocumented) resolve(resolver?: Resolvable.Resolver): List>; // (undocumented) + get size(): number; + // (undocumented) toJSON(): List.JSON; // (undocumented) toString(): string; diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 1c11c795ee..1d6e234367 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -6,11 +6,8 @@ import type { Applicative } from '@siteimprove/alfa-applicative'; import { Array as Array_2 } from '@siteimprove/alfa-array'; -import { BgSize } from './property/mask-size.js'; import { Box } from '@siteimprove/alfa-css'; import { Color } from '@siteimprove/alfa-css'; -import { Component } from '@siteimprove/alfa-css/dist/value/position/component.js'; -import { CompositingOperator } from './property/mask-composite.js'; import { Computed } from './property/line-height.js'; import { Contain } from '@siteimprove/alfa-css'; import { Context } from '@siteimprove/alfa-selector'; @@ -31,8 +28,6 @@ import { LengthPercentage } from '@siteimprove/alfa-css'; import { List } from '@siteimprove/alfa-css'; import { Map as Map_2 } from '@siteimprove/alfa-map'; import type { Mapper } from '@siteimprove/alfa-mapper'; -import { MaskingMode } from './property/mask-mode.js'; -import { MaskReference } from './property/mask-image.js'; import type { Monad } from '@siteimprove/alfa-monad'; import { Node } from '@siteimprove/alfa-dom'; import { Number as Number_2 } from '@siteimprove/alfa-css'; @@ -43,10 +38,8 @@ import { Parser } from '@siteimprove/alfa-css'; import * as parser from '@siteimprove/alfa-parser'; import { Percentage } from '@siteimprove/alfa-css'; import { Perspective } from '@siteimprove/alfa-css'; -import { Position } from '@siteimprove/alfa-css'; import { Predicate } from '@siteimprove/alfa-predicate'; import { Rectangle } from '@siteimprove/alfa-css'; -import { RepeatStyle } from './property/mask-repeat.js'; import type { Resolvable } from '@siteimprove/alfa-css'; import { Rotate } from '@siteimprove/alfa-css'; import { Scale } from '@siteimprove/alfa-css'; @@ -63,7 +56,14 @@ import { Specified as Specified_13 } from './property/font-stretch.js'; import { Specified as Specified_14 } from './property/font-variant-east-asian.js'; import { Specified as Specified_15 } from './property/font-variant-ligatures.js'; import { Specified as Specified_16 } from './property/font-variant-numeric.js'; +import { Specified as Specified_17 } from './property/mask-clip.js'; +import { Specified as Specified_18 } from './property/mask-composite.js'; +import { Specified as Specified_19 } from './property/mask-image.js'; import { Specified as Specified_2 } from './property/background-image.js'; +import { Specified as Specified_20 } from './property/mask-mode.js'; +import { Specified as Specified_21 } from './property/mask-position.js'; +import { Specified as Specified_22 } from './property/mask-repeat.js'; +import { Specified as Specified_23 } from './property/mask-size.js'; import { Specified as Specified_3 } from './property/background-position-x.js'; import { Specified as Specified_4 } from './property/background-position-y.js'; import { Specified as Specified_5 } from './property/background-repeat-x.js'; @@ -231,14 +231,14 @@ export namespace Longhands { readonly "margin-left": Longhand, Length | Percentage | Keyword<"auto">>; readonly "margin-right": Longhand, Length | Percentage | Keyword<"auto">>; readonly "margin-top": Longhand, Length | Percentage | Keyword<"auto">>; - readonly "mask-clip": Longhand>, List>>; - readonly "mask-composite": Longhand, List>; - readonly "mask-image": Longhand, List>; - readonly "mask-mode": Longhand, List>; + readonly "mask-clip": Longhand, List>; + readonly "mask-composite": Longhand, List>; + readonly "mask-image": Longhand, List>; + readonly "mask-mode": Longhand, List>; readonly "mask-origin": Longhand, List>; - readonly "mask-position": Longhand, Component>>, List>>; - readonly "mask-repeat": Longhand, List>; - readonly "mask-size": Longhand, List>; + readonly "mask-position": Longhand, List>; + readonly "mask-repeat": Longhand, List>; + readonly "mask-size": Longhand, List>; readonly "min-height": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "min-width": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "mix-blend-mode": Longhand, Keyword.ToKeywords<"screen" | "color" | "hue" | "saturation" | "normal" | "multiply" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "luminosity" | "plus-darker" | "plus-lighter">>; @@ -285,6 +285,7 @@ export namespace Longhands { // // @internal export namespace Resolver { + export function layers(style: Style, name: "mask-image" | "background-image"): Mapper, List>; // (undocumented) export function length(style: Style): Length.Resolver; // (undocumented) @@ -320,7 +321,7 @@ export namespace Shorthands { export function isName(name: string): name is Name; const // (undocumented) shortHands: { - readonly background: Shorthand<"background-attachment" | "background-clip" | "background-color" | "background-image" | "background-origin" | "background-position-x" | "background-position-y" | "background-repeat-x" | "background-repeat-y" | "background-size">; + readonly background: Shorthand<"background-image" | "background-attachment" | "background-clip" | "background-color" | "background-origin" | "background-position-x" | "background-position-y" | "background-repeat-x" | "background-repeat-y" | "background-size">; readonly "background-position": Shorthand<"background-position-x" | "background-position-y">; readonly "background-repeat": Shorthand<"background-repeat-x" | "background-repeat-y">; readonly "border-block-color": Shorthand<"border-block-end-color" | "border-block-start-color">; @@ -352,7 +353,7 @@ export namespace Shorthands { readonly "inset-inline": Shorthand<"inset-inline-end" | "inset-inline-start">; readonly inset: Shorthand<"top" | "bottom" | "left" | "right">; readonly margin: Shorthand<"margin-bottom" | "margin-left" | "margin-right" | "margin-top">; - readonly mask: Shorthand<"mask-clip" | "mask-composite" | "mask-image" | "mask-mode" | "mask-origin" | "mask-position" | "mask-repeat" | "mask-size">; + readonly mask: Shorthand<"mask-image" | "mask-clip" | "mask-composite" | "mask-mode" | "mask-origin" | "mask-position" | "mask-repeat" | "mask-size">; readonly outline: Shorthand<"outline-color" | "outline-style" | "outline-width">; readonly overflow: Shorthand<"overflow-x" | "overflow-y">; readonly "text-decoration": Shorthand<"text-decoration-color" | "text-decoration-line" | "text-decoration-style" | "text-decoration-thickness">; From 243074807768b999f22ec24dffe581463786935d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:50:58 +0100 Subject: [PATCH 35/40] Make the returned layers resolver typed --- packages/alfa-style/src/property/mask-position.ts | 5 +---- packages/alfa-style/src/property/mask-size.ts | 2 +- packages/alfa-style/src/resolver.ts | 6 +++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 8d4b11e0c3..9b1b4a4d68 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -43,10 +43,7 @@ export default Longhand.of( List.of([initialItem], ", "), parseList, (value, style) => { - const layers = Resolver.layers( - style, - "mask-image", - ); + const layers = Resolver.layers(style, "mask-image"); return value.map((positions) => layers( diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index 1578385ab3..c6fd85c78e 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -48,7 +48,7 @@ export default Longhand.of( List.of([initialItem], ", "), parseList, (value, style) => { - const layers = Resolver.layers(style, "mask-image"); + const layers = Resolver.layers(style, "mask-image"); return value.map((sizes) => layers( diff --git a/packages/alfa-style/src/resolver.ts b/packages/alfa-style/src/resolver.ts index be3b53045a..f529c24186 100644 --- a/packages/alfa-style/src/resolver.ts +++ b/packages/alfa-style/src/resolver.ts @@ -65,11 +65,11 @@ export namespace Resolver { * * @internal */ - export function layers( + export function layers( style: Style, name: "mask-image" | "background-image", - ): Mapper, List> { - return (value) => + ) { + return (value: List): List => value.cutOrExtend(Math.max(style.computed(name).value.size, 1)); } } From d8618a935bc1dd580c5a807f30661a7b52be3e53 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:01:15 +0000 Subject: [PATCH 36/40] Extract API --- docs/review/api/alfa-style.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 1d6e234367..3c4fa25a7e 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -285,7 +285,7 @@ export namespace Longhands { // // @internal export namespace Resolver { - export function layers(style: Style, name: "mask-image" | "background-image"): Mapper, List>; + export function layers(style: Style, name: "mask-image" | "background-image"): (value: List) => List; // (undocumented) export function length(style: Style): Length.Resolver; // (undocumented) From beafecfbb38a804517b3fe4f0ec4e5963848906a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:41:44 +0100 Subject: [PATCH 37/40] Use `Tuple` in `mask-repeat` --- .../alfa-style/src/property/mask-repeat.ts | 57 ++++++++------ .../test/property/mask-repeat.spec.tsx | 77 +++++++++++++------ .../alfa-style/test/property/mask.spec.tsx | 10 ++- 3 files changed, 98 insertions(+), 46 deletions(-) diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index afcf28540e..5711ac37f7 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -1,13 +1,20 @@ -import { Keyword, List } from "@siteimprove/alfa-css"; +import { Keyword, List, Tuple } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; +import { Selective } from "@siteimprove/alfa-selective"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -const { either } = Parser; +const { either, map } = Parser; type Specified = List; +type LonghandValue = + | Keyword<"repeat"> + | Keyword<"space"> + | Keyword<"round"> + | Keyword<"no-repeat">; + /** * @internal */ @@ -15,25 +22,23 @@ export namespace Specified { export type Item = | Keyword<"repeat-x"> | Keyword<"repeat-y"> - | List< - | Keyword<"repeat"> - | Keyword<"space"> - | Keyword<"round"> - | Keyword<"no-repeat"> - >; + | Tuple<[LonghandValue, LonghandValue]>; } -type Computed = Specified; +type Computed = List>; /** * @internal */ export const parse = either( Keyword.parse("repeat-x", "repeat-y"), - List.parseSpaceSeparated( - Keyword.parse("repeat", "space", "round", "no-repeat"), - 1, - 2, + map( + List.parseSpaceSeparated( + Keyword.parse("repeat", "space", "round", "no-repeat"), + 1, + 2, + ), + ([horizontal, vertical = horizontal]) => Tuple.of(horizontal, vertical), ), ); @@ -42,22 +47,30 @@ const parseList = List.parseCommaSeparated(parse); /** * @internal */ -export const initialItem = List.of([Keyword.of("repeat")], " "); +export const initialItem = Tuple.of(Keyword.of("repeat"), Keyword.of("repeat")); /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-repeat} * - * @privateRemarks - * The spec says that the computed value "Consists of: two keywords, one per dimension", - * which could be taken to mean that the one-keyword shorthand values should be expanded to their two-keyword longhands, - * e.g. `repeat-x` would be expanded to `repeat no-repeat` in the computed style, - * but that is not the current behavior observed in the DevTools of Chrome and Firefox. - * We mimic the behavior of the browsers and do not expand the shorthands. - * * @internal */ export default Longhand.of( List.of([initialItem], ", "), parseList, - (value, style) => value.map(Resolver.layers(style, "mask-image")), + (value, style) => { + const layers = Resolver.layers(style, "mask-image"); + return value.map((values) => + layers( + values.map((value) => + Selective.of(value) + .if(Keyword.isKeyword, (keyword) => + keyword.is("repeat-x") + ? Tuple.of(Keyword.of("repeat"), Keyword.of("no-repeat")) + : Tuple.of(Keyword.of("no-repeat"), Keyword.of("repeat")), + ) + .get(), + ), + ), + ); + }, ); diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index 9e52655b7d..08df2306cb 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -10,9 +10,11 @@ test("initial value is repeat", (t) => { separator: ", ", values: [ { - type: "list", - values: [{ type: "keyword", value: "repeat" }], - separator: " ", + type: "tuple", + values: [ + { type: "keyword", value: "repeat" }, + { type: "keyword", value: "repeat" }, + ], }, ], }, @@ -21,19 +23,45 @@ test("initial value is repeat", (t) => { }); test("#computed parses single keywords", (t) => { - for (const kw of ["repeat-x", "repeat-y"] as const) { - t.deepEqual( - computed(
, "mask-repeat"), - { - value: { - type: "list", - separator: ", ", - values: [{ type: "keyword", value: kw }], - }, - source: h.declaration("mask-repeat", kw).toJSON(), + t.deepEqual( + computed(
, "mask-repeat"), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "tuple", + values: [ + { type: "keyword", value: "repeat" }, + { type: "keyword", value: "no-repeat" }, + ], + }, + ], }, - ); - } + source: h.declaration("mask-repeat", "repeat-x").toJSON(), + }, + ); + + t.deepEqual( + computed(
, "mask-repeat"), + { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "tuple", + values: [ + { type: "keyword", value: "no-repeat" }, + { type: "keyword", value: "repeat" }, + ], + }, + ], + }, + source: h.declaration("mask-repeat", "repeat-y").toJSON(), + }, + ); for (const kw of ["repeat", "space", "round", "no-repeat"] as const) { t.deepEqual( @@ -44,9 +72,11 @@ test("#computed parses single keywords", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", - values: [{ type: "keyword", value: kw }], + type: "tuple", + values: [ + { type: "keyword", value: kw }, + { type: "keyword", value: kw }, + ], }, ], }, @@ -65,8 +95,7 @@ test("#computed parses at most two space separated values", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", + type: "tuple", values: [ { type: "keyword", value: "repeat" }, { type: "keyword", value: "space" }, @@ -89,9 +118,11 @@ test("#computed parses at most two space separated values", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", - values: [{ type: "keyword", value: "repeat" }], + type: "tuple", + values: [ + { type: "keyword", value: "repeat" }, + { type: "keyword", value: "repeat" }, + ], }, ], }, diff --git a/packages/alfa-style/test/property/mask.spec.tsx b/packages/alfa-style/test/property/mask.spec.tsx index d6cae97cf5..8a4d6b27e9 100644 --- a/packages/alfa-style/test/property/mask.spec.tsx +++ b/packages/alfa-style/test/property/mask.spec.tsx @@ -60,7 +60,15 @@ test("longhands resolve correctly from shorthand", (t) => { value: { type: "list", separator: ", ", - values: [{ type: "keyword", value: "repeat-x" }], + values: [ + { + type: "tuple", + values: [ + { type: "keyword", value: "repeat" }, + { type: "keyword", value: "no-repeat" }, + ], + }, + ], }, source: decl.toJSON(), }); From 978b09fc9e5911d35290f3a94e935b19f86551fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:28:13 +0100 Subject: [PATCH 38/40] Use `Tuple` in `mask-size` --- packages/alfa-style/src/property/mask-size.ts | 43 +++++++++++-------- .../test/property/mask-size.spec.tsx | 43 +++++++++++-------- .../alfa-style/test/property/mask.spec.tsx | 24 +++++++---- 3 files changed, 67 insertions(+), 43 deletions(-) diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index c6fd85c78e..8d5482f7f9 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -1,10 +1,11 @@ -import { Keyword, LengthPercentage, List } from "@siteimprove/alfa-css"; +import { Keyword, LengthPercentage, List, Tuple } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; +import { Selective } from "@siteimprove/alfa-selective"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -const { either } = Parser; +const { either, map } = Parser; type Specified = List; @@ -13,7 +14,9 @@ type Specified = List; */ export namespace Specified { export type Item = - | List> + | Tuple< + [LengthPercentage | Keyword<"auto">, LengthPercentage | Keyword<"auto">] + > | Keyword<"cover"> | Keyword<"contain">; } @@ -24,10 +27,13 @@ type Computed = Specified; * @internal */ export const parse = either( - List.parseSpaceSeparated( - either(LengthPercentage.parse, Keyword.parse("auto")), - 1, - 2, + map( + List.parseSpaceSeparated( + either(LengthPercentage.parse, Keyword.parse("auto")), + 1, + 2, + ), + ([horizontal, vertical = horizontal]) => Tuple.of(horizontal, vertical), ), Keyword.parse("cover", "contain"), ); @@ -37,7 +43,7 @@ const parseList = List.parseCommaSeparated(parse); /** * @internal */ -export const initialItem = List.of([Keyword.of("auto")], " "); +export const initialItem = Tuple.of(Keyword.of("auto"), Keyword.of("auto")); /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-size} @@ -49,19 +55,22 @@ export default Longhand.of( parseList, (value, style) => { const layers = Resolver.layers(style, "mask-image"); + const lengthResolver = LengthPercentage.partiallyResolve( + Resolver.length(style), + ); return value.map((sizes) => layers( sizes.map((size) => - Keyword.isKeyword(size) - ? size - : size.map((value) => - Keyword.isKeyword(value) - ? value - : LengthPercentage.partiallyResolve(Resolver.length(style))( - value, - ), - ), + Selective.of(size) + .if(Tuple.isTuple, (tuple) => { + const [h, v] = tuple.values; + return Tuple.of( + Keyword.isKeyword(h) ? h : lengthResolver(h), + Keyword.isKeyword(v) ? v : lengthResolver(v), + ); + }) + .get(), ), ), ); diff --git a/packages/alfa-style/test/property/mask-size.spec.tsx b/packages/alfa-style/test/property/mask-size.spec.tsx index 155c59abb8..e7b37221f5 100644 --- a/packages/alfa-style/test/property/mask-size.spec.tsx +++ b/packages/alfa-style/test/property/mask-size.spec.tsx @@ -15,14 +15,18 @@ test("initial value is auto", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", - values: [{ type: "keyword", value: "auto" }], + type: "tuple", + values: [ + { type: "keyword", value: "auto" }, + { type: "keyword", value: "auto" }, + ], }, { - type: "list", - separator: " ", - values: [{ type: "keyword", value: "auto" }], + type: "tuple", + values: [ + { type: "keyword", value: "auto" }, + { type: "keyword", value: "auto" }, + ], }, ], }, @@ -51,9 +55,11 @@ test("#computed parses percentage width", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", - values: [{ type: "percentage", value: 0.5 }], + type: "tuple", + values: [ + { type: "percentage", value: 0.5 }, + { type: "percentage", value: 0.5 }, + ], }, ], }, @@ -68,9 +74,11 @@ test("#computed resolves em width", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", - values: [{ type: "length", unit: "px", value: 48 }], + type: "tuple", + values: [ + { type: "length", unit: "px", value: 48 }, + { type: "length", unit: "px", value: 48 }, + ], }, ], }, @@ -85,9 +93,11 @@ test("#computed parses pixel width", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", - values: [{ type: "length", unit: "px", value: 12 }], + type: "tuple", + values: [ + { type: "length", unit: "px", value: 12 }, + { type: "length", unit: "px", value: 12 }, + ], }, ], }, @@ -104,8 +114,7 @@ test("#computed parses width and height", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", + type: "tuple", values: [ { type: "length", unit: "px", value: 48 }, { type: "percentage", value: 0.25 }, diff --git a/packages/alfa-style/test/property/mask.spec.tsx b/packages/alfa-style/test/property/mask.spec.tsx index 8a4d6b27e9..ca70af7073 100644 --- a/packages/alfa-style/test/property/mask.spec.tsx +++ b/packages/alfa-style/test/property/mask.spec.tsx @@ -47,9 +47,11 @@ test("longhands resolve correctly from shorthand", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", - values: [{ type: "length", unit: "px", value: 12 }], + type: "tuple", + values: [ + { type: "length", unit: "px", value: 12 }, + { type: "length", unit: "px", value: 12 }, + ], }, ], }, @@ -217,14 +219,18 @@ test("longhands resolves correctly from shorthand with layers", (t) => { separator: ", ", values: [ { - type: "list", - separator: " ", - values: [{ type: "length", unit: "px", value: 12 }], + type: "tuple", + values: [ + { type: "length", unit: "px", value: 12 }, + { type: "length", unit: "px", value: 12 }, + ], }, { - type: "list", - separator: " ", - values: [{ type: "keyword", value: "auto" }], + type: "tuple", + values: [ + { type: "keyword", value: "auto" }, + { type: "keyword", value: "auto" }, + ], }, ], }, From 671d55240ead17ea17da81eaf9670656dcaf011f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:51:17 +0100 Subject: [PATCH 39/40] Add note about discrepancy with spec in position computation --- packages/alfa-style/src/property/mask-position.ts | 7 +++++++ packages/alfa-style/test/property/mask-position.spec.tsx | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 9b1b4a4d68..7bbd69d4b1 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -37,6 +37,13 @@ export const initialItem = Position.of( /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-position} * + * @remarks + * We do not currently fully follow the definition of computed value given in the specification. + * Accordingly the computed value should consist of two keywords and two offsets. + * E.g specifying a single keyword `right` would compute to `top 50% left 100%` or similar. + * But in the current implementation keyword based values are not resolved into keywords with offsets. + * So the single keyword `right` will just compute unaltered to `right`. + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx index 40bd9b3e02..d3ca08803a 100644 --- a/packages/alfa-style/test/property/mask-position.spec.tsx +++ b/packages/alfa-style/test/property/mask-position.spec.tsx @@ -28,8 +28,6 @@ test("initial value is 0% 0%", (t) => { }); }); -// TODO: The spec requires the computed value to be two lengths or percentages, not a keyword value. -// E.g. the keyword `left` should be computes to `0% 50%` in Chrome and Firefox. test("#computed parses single keywords", (t) => { for (const kw of ["top", "bottom"] as const) { t.deepEqual( From dcaddde2c02d0e0d9e3ede727745f46652597c7b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:55:26 +0000 Subject: [PATCH 40/40] Extract API --- docs/review/api/alfa-style.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 3c4fa25a7e..3fda69cdc9 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -237,7 +237,7 @@ export namespace Longhands { readonly "mask-mode": Longhand, List>; readonly "mask-origin": Longhand, List>; readonly "mask-position": Longhand, List>; - readonly "mask-repeat": Longhand, List>; + readonly "mask-repeat": Longhand, List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">, Keyword<"repeat"> | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">]>>>; readonly "mask-size": Longhand, List>; readonly "min-height": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; readonly "min-width": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>;