Skip to content

Commit

Permalink
Add mask-border-slice
Browse files Browse the repository at this point in the history
  • Loading branch information
rcj-siteimprove committed Dec 13, 2024
1 parent 656b523 commit 9d49141
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 17 deletions.
2 changes: 2 additions & 0 deletions packages/alfa-style/src/longhands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ 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 MaskBorderSlice from "./property/mask-border-slice.js";
import MaskBorderSource from "./property/mask-border-source.js";
import MaskClip from "./property/mask-clip.js";
import MaskComposite from "./property/mask-composite.js";
Expand Down Expand Up @@ -277,6 +278,7 @@ export namespace Longhands {
"margin-left": MarginLeft,
"margin-right": MarginRight,
"margin-top": MarginTop,
"mask-border-slice": MaskBorderSlice,
"mask-border-source": MaskBorderSource,
"mask-clip": MaskClip,
"mask-composite": MaskComposite,
Expand Down
134 changes: 134 additions & 0 deletions packages/alfa-style/src/property/mask-border-slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
Keyword,
Number,
Percentage,
Token,
Tuple,
} from "@siteimprove/alfa-css";
import { Parser } from "@siteimprove/alfa-parser";
import { Slice } from "@siteimprove/alfa-slice";

import { Longhand } from "../longhand.js";
import { Resolver } from "../resolver.js";

const { doubleBar, either, filter, map } = Parser;

type NumberPercentage = Number | Percentage;

/**
* @internal
*/
export type Specified =
| Tuple<
[NumberPercentage, NumberPercentage, NumberPercentage, NumberPercentage]
>
| Tuple<
[
NumberPercentage,
NumberPercentage,
NumberPercentage,
NumberPercentage,
Keyword<"fill">,
]
>;

const parseNumberPercentage = either(Number.parse, Percentage.parse);

/**
* @internal
*
* @privateRemarks
* The `doubleBar` parser is used because the `fill` keyword may appear at any position.
*/
export const parse = map(
filter(
doubleBar<
Slice<Token>,
[
NumberPercentage,
NumberPercentage,
NumberPercentage,
NumberPercentage,
Keyword<"fill">,
],
string
>(
Token.parseWhitespace,
parseNumberPercentage,
parseNumberPercentage,
parseNumberPercentage,
parseNumberPercentage,
Keyword.parse("fill"),
),
(values) =>
values.some((value) => value !== undefined && !Keyword.isKeyword(value)),
() => "At least one non-keyword value must be present.",
),
([p1, p2, p3, p4, fill]) => {
// At least one number is guaranteed to be defined by the filter above
// and we assume that `doubleBar` is implemented such that the defined values appears first.
p1 = p1 as NumberPercentage;

// Represents the postions in order of [top, right, bottom, left]
let positions: [
NumberPercentage,
NumberPercentage,
NumberPercentage,
NumberPercentage,
];
if (p2 === undefined) {
positions = [p1, p1, p1, p1];
} else if (p3 === undefined) {
// when two positions are specified, the first creates slices measured from the **top and bottom**,
// the second creates slices measured from the **left and right**.
positions = [p1, p2, p1, p2];
} else if (p4 === undefined) {
// when three positions are specified, the first creates a slice measured from the **top**,
// the second creates slices measured from the **left and right**,
// the third creates a slice measured from the **bottom**.
positions = [p1, p2, p3, p2];
} else {
positions = [p1, p2, p3, p4];
}

return fill !== undefined
? Tuple.of(...positions, fill)
: Tuple.of(...positions);
},
);

/**
* @internal
*/
export const initialItem = Tuple.of(
Number.of(0),
Number.of(0),
Number.of(0),
Number.of(0),
);

type Computed = Specified;

/**
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-border-slice}
*
* @internal
*/
export default Longhand.of<Specified, Computed>(
initialItem,
parse,
(value, style) =>
value.map((tuple) => {
const [p1, p2, p3, p4, fill] = tuple.values;
const positions = [
p1.resolve(Resolver.length(style)),
p2.resolve(Resolver.length(style)),
p3.resolve(Resolver.length(style)),
p4.resolve(Resolver.length(style)),
] as const;

return fill !== undefined
? Tuple.of(...positions, fill)
: Tuple.of(...positions);
}),
);
35 changes: 18 additions & 17 deletions packages/alfa-style/src/property/mask-border-source.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
Image,
Keyword,
URL,
type Parser as CSSParser,
} from "@siteimprove/alfa-css";
import { Image, Keyword, URL } from "@siteimprove/alfa-css";
import { Parser } from "@siteimprove/alfa-parser";
import { Selective } from "@siteimprove/alfa-selective";

Expand All @@ -12,18 +7,24 @@ import { Resolver } from "../resolver.js";

const { either } = Parser;

export type MaskBorderSource = Keyword<"none"> | Image | URL;
/**
* @internal
*/
export type Specified = Keyword<"none"> | Image | URL;

export namespace MaskBorderSource {
export const parse: CSSParser<MaskBorderSource> = either(
Keyword.parse("none"),
either(Image.parse, URL.parse),
);
/**
* @internal
*/
export const parse = either(
Keyword.parse("none"),
either(Image.parse, URL.parse),
);

export const initialItem = Keyword.of("none");
}
/**
* @internal
*/
export const initialItem = Keyword.of("none");

type Specified = MaskBorderSource;
type Computed = Specified;

/**
Expand All @@ -32,8 +33,8 @@ type Computed = Specified;
* @internal
*/
export default Longhand.of<Specified, Computed>(
MaskBorderSource.initialItem,
MaskBorderSource.parse,
initialItem,
parse,
(value, style) =>
value.map((image) =>
Selective.of(image)
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 @@ -163,6 +163,7 @@
"./property/margin-left.ts",
"./property/margin-right.ts",
"./property/margin-top.ts",
"./property/mask-border-slice.ts",
"./property/mask-border-source.ts",
"./property/min-height.ts",
"./property/min-width.ts",
Expand Down
125 changes: 125 additions & 0 deletions packages/alfa-style/test/property/mask-border-slice.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { test } from "@siteimprove/alfa-test";
import { h } from "@siteimprove/alfa-dom";

import { computed } from "../common.js";

test("initial value is none", (t) => {
t.deepEqual(computed(<div></div>, "mask-border-slice"), {
value: {
type: "tuple",
values: [
{ type: "number", value: 0 },
{ type: "number", value: 0 },
{ type: "number", value: 0 },
{ type: "number", value: 0 },
],
},
source: null,
});
});

test("#computed parses one value", (t) => {
t.deepEqual(
computed(
<div style={{ maskBorderSlice: "30%" }}></div>,
"mask-border-slice",
),
{
value: {
type: "tuple",
values: [
{ type: "percentage", value: 0.3 },
{ type: "percentage", value: 0.3 },
{ type: "percentage", value: 0.3 },
{ type: "percentage", value: 0.3 },
],
},
source: h.declaration("mask-border-slice", "30%").toJSON(),
},
);
});

test("#computed parses two values", (t) => {
t.deepEqual(
computed(
<div style={{ maskBorderSlice: "10% 30%" }}></div>,
"mask-border-slice",
),
{
value: {
type: "tuple",
values: [
{ type: "percentage", value: 0.1 },
{ type: "percentage", value: 0.3 },
{ type: "percentage", value: 0.1 },
{ type: "percentage", value: 0.3 },
],
},
source: h.declaration("mask-border-slice", "10% 30%").toJSON(),
},
);
});

test("#computed parses three values", (t) => {
t.deepEqual(
computed(
<div style={{ maskBorderSlice: "30 30% 45" }}></div>,
"mask-border-slice",
),
{
value: {
type: "tuple",
values: [
{ type: "number", value: 30 },
{ type: "percentage", value: 0.3 },
{ type: "number", value: 45 },
{ type: "percentage", value: 0.3 },
],
},
source: h.declaration("mask-border-slice", "30 30% 45").toJSON(),
},
);
});

test("#computed parses four values", (t) => {
t.deepEqual(
computed(
<div style={{ maskBorderSlice: "7 12 14 5" }}></div>,
"mask-border-slice",
),
{
value: {
type: "tuple",
values: [
{ type: "number", value: 7 },
{ type: "number", value: 12 },
{ type: "number", value: 14 },
{ type: "number", value: 5 },
],
},
source: h.declaration("mask-border-slice", "7 12 14 5").toJSON(),
},
);
});

test("#computed parses the `fill` keyword", (t) => {
t.deepEqual(
computed(
<div style={{ maskBorderSlice: "10 fill 7 12" }}></div>,
"mask-border-slice",
),
{
value: {
type: "tuple",
values: [
{ type: "number", value: 10 },
{ type: "number", value: 7 },
{ type: "number", value: 12 },
{ type: "number", value: 7 },
{ type: "keyword", value: "fill" },
],
},
source: h.declaration("mask-border-slice", "10 fill 7 12").toJSON(),
},
);
});
1 change: 1 addition & 0 deletions packages/alfa-style/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"./property/isolation.spec.tsx",
"./property/line-height.spec.tsx",
"./property/margin.spec.tsx",
"./property/mask-border-slice.spec.tsx",
"./property/mask-border-source.spec.tsx",
"./property/mask-clip.spec.tsx",
"./property/mask-composite.spec.tsx",
Expand Down

0 comments on commit 9d49141

Please sign in to comment.