Skip to content

Commit

Permalink
Add test of shorthand as well as clean up and minor fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
rcj-siteimprove committed Dec 9, 2024
1 parent f6ac94f commit 071c468
Show file tree
Hide file tree
Showing 23 changed files with 633 additions and 283 deletions.
5 changes: 5 additions & 0 deletions .changeset/few-clouds-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-style": minor
---

**Added:** CSS shorthand property `mask` and corresponding longhand properties is now supported.
39 changes: 39 additions & 0 deletions packages/alfa-style/src/property/helpers/match-layers.ts
Original file line number Diff line number Diff line change
@@ -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<V extends Value>(
value: List<V>,
style: Style,
): List<V> {
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),
", ",
);
}
9 changes: 6 additions & 3 deletions packages/alfa-style/src/property/mask-clip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ 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<Box.CoordBox | Keyword<"no-clip">>;
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}
*
* @internal
*/
export default Longhand.of<Specified, Computed>(
List.of([initialItem]),
List.of([MaskClip.initialItem]),
List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))),
(value, style) => value.map((value) => matchLayers(value, style)),
);
3 changes: 2 additions & 1 deletion packages/alfa-style/src/property/mask-composite.ts
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
13 changes: 12 additions & 1 deletion packages/alfa-style/src/property/mask-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -33,5 +35,14 @@ type Computed = Specified;
export default Longhand.of<Specified, Computed>(
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(),
),
),
);
3 changes: 2 additions & 1 deletion packages/alfa-style/src/property/mask-mode.ts
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
9 changes: 6 additions & 3 deletions packages/alfa-style/src/property/mask-origin.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
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<Box.CoordBox>;
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}
*
* @internal
*/
export default Longhand.of<Specified, Computed>(
List.of([initialItem], ", "),
List.of([MaskOrigin.initialItem], ", "),
List.parseCommaSeparated(Box.parseCoordBox),
(value, style) => value.map((value) => matchLayers(value, style)),
);
15 changes: 9 additions & 6 deletions packages/alfa-style/src/property/mask-position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@ 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<Position>;
type Computed = List<Position.PartiallyResolved>;

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}
*
* @internal
*/
export default Longhand.of<Specified, Computed>(
List.of([initialItem], ", "),
List.of([MaskPosition.initialItem], ", "),
List.parseCommaSeparated(Position.parse(/* legacySyntax */ true)),
(value, style) =>
value.map((positions) =>
Expand Down
3 changes: 2 additions & 1 deletion packages/alfa-style/src/property/mask-repeat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion packages/alfa-style/src/property/mask-size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
51 changes: 6 additions & 45 deletions packages/alfa-style/src/property/mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
List,
type Parser as CSSParser,
Position,
type Value,
Box,
Keyword,
} from "@siteimprove/alfa-css";
Expand All @@ -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,
Expand All @@ -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<V extends Value>(
value: List<V>,
style: Style,
): List<V> {
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<BgSize>]> = pair(
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions packages/alfa-style/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions packages/alfa-style/test/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export function specified<N extends Longhands.Name>(
return Style.from(element, device, context).specified(name).toJSON();
}

/**
* @internal
*/
export function computed<N extends Longhands.Name>(
element: Element,
name: N,
context: Context = Context.empty(),
): Value.JSON<Style.Specified<N>> {
return Style.from(element, device, context).computed(name).toJSON();
}

/**
* @internal
*/
Expand Down
Loading

0 comments on commit 071c468

Please sign in to comment.