From 421373670099a05a5b3c3ad8b24a322c2068adb6 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 11 Oct 2023 15:11:29 +0200 Subject: [PATCH 01/18] Make Circle and Ellipse calculatable --- .../alfa-css/src/value/shape/basic-shape.ts | 10 +-- packages/alfa-css/src/value/shape/circle.ts | 40 ++++++++-- packages/alfa-css/src/value/shape/ellipse.ts | 55 +++++++++++--- packages/alfa-css/src/value/shape/inset.ts | 5 +- packages/alfa-css/src/value/shape/polygon.ts | 2 +- packages/alfa-css/src/value/shape/radius.ts | 75 +++++++++++++++---- .../alfa-css/src/value/shape/rectangle.ts | 2 +- 7 files changed, 150 insertions(+), 39 deletions(-) diff --git a/packages/alfa-css/src/value/shape/basic-shape.ts b/packages/alfa-css/src/value/shape/basic-shape.ts index dcae64d3c0..55341c2621 100644 --- a/packages/alfa-css/src/value/shape/basic-shape.ts +++ b/packages/alfa-css/src/value/shape/basic-shape.ts @@ -3,12 +3,12 @@ import { Value } from "../value"; /** * @internal */ -export abstract class BasicShape extends Value< - "basic-shape", - false -> { +export abstract class BasicShape< + K extends string = string, + CALC extends boolean = boolean +> extends Value<"basic-shape", CALC> { private readonly _kind: K; - protected constructor(kind: K, hasCalculation: false) { + protected constructor(kind: K, hasCalculation: CALC) { super("basic-shape", hasCalculation); this._kind = kind; } diff --git a/packages/alfa-css/src/value/shape/circle.ts b/packages/alfa-css/src/value/shape/circle.ts index 1c7f9a3cd2..c0335a338e 100644 --- a/packages/alfa-css/src/value/shape/circle.ts +++ b/packages/alfa-css/src/value/shape/circle.ts @@ -5,6 +5,7 @@ import { Function, type Parser as CSSParser, Token } from "../../syntax"; import { Keyword } from "../keyword"; import { Position } from "../position"; +import { Value } from "../value"; import { BasicShape } from "./basic-shape"; import { Radius } from "./radius"; @@ -18,9 +19,9 @@ const { map, option, pair, right } = Parser; */ export class Circle< R extends Radius = Radius, - P extends Position.Fixed = Position.Fixed -> extends BasicShape<"circle"> { - public static of( + P extends Position = Position +> extends BasicShape<"circle", Value.HasCalculation<[R, P]>> { + public static of( radius: R, center: P ): Circle { @@ -31,7 +32,7 @@ export class Circle< private readonly _center: P; private constructor(radius: R, center: P) { - super("circle", false); + super("circle", Value.hasCalculation(radius, center)); this._radius = radius; this._center = center; } @@ -44,8 +45,11 @@ export class Circle< return this._center; } - public resolve(): Circle { - return this; + public resolve(resolver: Circle.Resolver): Circle.Canonical { + return new Circle( + this._radius.resolve(resolver), + this._center.resolve(resolver) + ); } public equals(value: Circle): boolean; @@ -81,11 +85,33 @@ export class Circle< * @public */ export namespace Circle { + export type Canonical = Circle; + export interface JSON extends BasicShape.JSON<"circle"> { radius: Radius.JSON; center: Position.JSON; } + export type Resolver = Radius.Resolver & Position.Resolver; + + export type PartiallyResolved = Circle< + Radius.PartiallyResolved, + Position.PartiallyResolved + >; + + export type PartialResolver = Radius.PartialResolver & + Position.PartialResolver; + + export function partiallyResolved( + resolver: PartialResolver + ): (value: Circle) => PartiallyResolved { + return (value) => + Circle.of( + Radius.PartiallyResolve(resolver)(value.radius), + Position.partiallyResolve(resolver)(value.center) + ); + } + export function isCircle(value: unknown): value is Circle { return value instanceof Circle; } @@ -100,7 +126,7 @@ export namespace Circle { option(Token.parseWhitespace), right( Keyword.parse("at"), - right(Token.parseWhitespace, Position.parseBase()) + right(Token.parseWhitespace, Position.parse()) ) ) ) diff --git a/packages/alfa-css/src/value/shape/ellipse.ts b/packages/alfa-css/src/value/shape/ellipse.ts index 6e275eb9b7..34d70ab8c2 100644 --- a/packages/alfa-css/src/value/shape/ellipse.ts +++ b/packages/alfa-css/src/value/shape/ellipse.ts @@ -5,6 +5,7 @@ import { Function, type Parser as CSSParser, Token } from "../../syntax"; import { Keyword } from "../keyword"; import { Position } from "../position"; +import { Value } from "../value"; import { BasicShape } from "./basic-shape"; import { Radius } from "./radius"; @@ -18,12 +19,13 @@ const { map, option, pair, right } = Parser; */ export class Ellipse< R extends Radius = Radius, - P extends Position.Fixed = Position.Fixed -> extends BasicShape<"ellipse"> { - public static of< - R extends Radius = Radius, - P extends Position.Fixed = Position.Fixed - >(rx: R, ry: R, center: P): Ellipse { + P extends Position = Position +> extends BasicShape<"ellipse", Value.HasCalculation<[R, P]>> { + public static of( + rx: R, + ry: R, + center: P + ): Ellipse { return new Ellipse(rx, ry, center); } @@ -32,7 +34,13 @@ export class Ellipse< private readonly _center: P; private constructor(rx: R, ry: R, center: P) { - super("ellipse", false); + super( + "ellipse", + // TS sees the first as Value.HasCalculation<[R, R, P]> + Value.hasCalculation(rx, ry, center) as unknown as Value.HasCalculation< + [R, P] + > + ); this._rx = rx; this._ry = ry; this._center = center; @@ -50,8 +58,12 @@ export class Ellipse< return this._center; } - public resolve(): Ellipse { - return this; + public resolve(resolver: Ellipse.Resolver): Ellipse.Canonical { + return new Ellipse( + this._rx.resolve(resolver), + this._ry.resolve(resolver), + this._center.resolve(resolver) + ); } public equals(value: Ellipse): boolean; @@ -92,12 +104,35 @@ export class Ellipse< * @public */ export namespace Ellipse { + export type Canonical = Ellipse; + export interface JSON extends BasicShape.JSON<"ellipse"> { rx: Radius.JSON; ry: Radius.JSON; center: Position.JSON; } + export type Resolver = Radius.Resolver & Position.Resolver; + + export type PartiallyResolved = Ellipse< + Radius.PartiallyResolved, + Position.PartiallyResolved + >; + + export type PartialResolver = Radius.PartialResolver & + Position.PartialResolver; + + export function partiallyResolved( + resolver: PartialResolver + ): (value: Ellipse) => PartiallyResolved { + return (value) => + Ellipse.of( + Radius.PartiallyResolve(resolver)(value.rx), + Radius.PartiallyResolve(resolver)(value.ry), + Position.partiallyResolve(resolver)(value.center) + ); + } + export function isEllipse(value: unknown): value is Ellipse { return value instanceof Ellipse; } @@ -112,7 +147,7 @@ export namespace Ellipse { option(Token.parseWhitespace), right( Keyword.parse("at"), - right(Token.parseWhitespace, Position.parseBase()) + right(Token.parseWhitespace, Position.parse()) ) ) ) diff --git a/packages/alfa-css/src/value/shape/inset.ts b/packages/alfa-css/src/value/shape/inset.ts index 4bf133b9e0..58d2f3b931 100644 --- a/packages/alfa-css/src/value/shape/inset.ts +++ b/packages/alfa-css/src/value/shape/inset.ts @@ -11,7 +11,8 @@ import { Length, Percentage } from "../numeric"; import { BasicShape } from "./basic-shape"; -const { either, map, filter, option, pair, right, takeAtMost } = Parser; +const { either, map, filter, option, pair, right, separatedList, takeAtMost } = + Parser; const { parseDelim, parseWhitespace } = Token; /** @@ -22,7 +23,7 @@ const { parseDelim, parseWhitespace } = Token; export class Inset< O extends Inset.Offset = Inset.Offset, C extends Inset.Corner = Inset.Corner -> extends BasicShape<"inset"> { +> extends BasicShape<"inset", false> { public static of< O extends Inset.Offset = Inset.Offset, C extends Inset.Corner = Inset.Corner diff --git a/packages/alfa-css/src/value/shape/polygon.ts b/packages/alfa-css/src/value/shape/polygon.ts index 3196518ce0..10f0985938 100644 --- a/packages/alfa-css/src/value/shape/polygon.ts +++ b/packages/alfa-css/src/value/shape/polygon.ts @@ -24,7 +24,7 @@ const { parseComma, parseWhitespace } = Token; export class Polygon< F extends Polygon.Fill = Polygon.Fill, V extends Length.Fixed | Percentage.Fixed = Length.Fixed | Percentage.Fixed -> extends BasicShape<"polygon"> { +> extends BasicShape<"polygon", false> { public static of< F extends Polygon.Fill = Polygon.Fill, V extends Length.Fixed | Percentage.Fixed = Length.Fixed | Percentage.Fixed diff --git a/packages/alfa-css/src/value/shape/radius.ts b/packages/alfa-css/src/value/shape/radius.ts index b0a7487b4e..a9c95cc31c 100644 --- a/packages/alfa-css/src/value/shape/radius.ts +++ b/packages/alfa-css/src/value/shape/radius.ts @@ -1,10 +1,12 @@ import { Hash } from "@siteimprove/alfa-hash"; +import { Real } from "@siteimprove/alfa-math"; import { Parser } from "@siteimprove/alfa-parser"; import type { Parser as CSSParser } from "../../syntax"; import { Keyword } from "../keyword"; -import { Length, Percentage } from "../numeric"; +import { LengthPercentage } from "../numeric"; +import { Value } from "../value"; import { BasicShape } from "./basic-shape"; @@ -16,12 +18,9 @@ const { either, map, filter } = Parser; * @public */ export class Radius< - R extends Length.Fixed | Percentage.Fixed | Radius.Side = - | Length.Fixed - | Percentage.Fixed - | Radius.Side -> extends BasicShape<"radius"> { - public static of( + R extends LengthPercentage | Radius.Side = LengthPercentage | Radius.Side +> extends BasicShape<"radius", Value.HasCalculation<[R]>> { + public static of( value: R ): Radius { return new Radius(value); @@ -30,7 +29,7 @@ export class Radius< private readonly _value: R; private constructor(value: R) { - super("radius", false); + super("radius", Value.hasCalculation(value)); this._value = value; } @@ -38,8 +37,21 @@ export class Radius< return this._value; } - public resolve(): Radius { - return this; + public resolve(resolver: Radius.Resolver): Radius.Canonical { + if (Keyword.isKeyword(this._value)) { + // TS lose the fact that if this._value is a Side, then this must be a + // Radius… + return this as Radius; + } + + const resolved = LengthPercentage.resolve(resolver)(this._value); + + return new Radius( + LengthPercentage.of( + Real.clamp(resolved.value, 0, Infinity), + resolved.unit + ) + ); } public equals(value: Radius): boolean; @@ -70,8 +82,44 @@ export class Radius< * @public */ export namespace Radius { + export type Canonical = Radius; + export interface JSON extends BasicShape.JSON<"radius"> { - value: Length.Fixed.JSON | Percentage.Fixed.JSON | Keyword.JSON; + value: LengthPercentage.JSON | Keyword.JSON; + } + + export type Resolver = LengthPercentage.Resolver; + + export type PartiallyResolved = Radius< + LengthPercentage.PartiallyResolved | Side + >; + + export type PartialResolver = LengthPercentage.PartialResolver; + + export function PartiallyResolve( + resolver: PartialResolver + ): (value: Radius) => PartiallyResolved { + return (value) => { + if (Keyword.isKeyword(value.value)) { + // TS lose the fact that if this._value is a Side, then this must be a + // Radius… + return value as Radius; + } + + const resolved = LengthPercentage.partiallyResolve(resolver)(value.value); + + if (resolved.hasCalculation()) { + return Radius.of(resolved); + } + + const clamped = Real.clamp(resolved.value, 0, Infinity); + + return Radius.of( + LengthPercentage.isPercentage(resolved) + ? LengthPercentage.of(clamped) + : LengthPercentage.of(clamped, resolved.unit) + ); + }; } export type Side = Side.Closest | Side.Farthest; @@ -95,8 +143,9 @@ export namespace Radius { export const parse: CSSParser = map( either( filter( - either(Length.parseBase, Percentage.parseBase), - ({ value }) => value >= 0, + LengthPercentage.parse, + // https://drafts.csswg.org/css-values/#calc-range + (value) => value.hasCalculation() || value.value >= 0, () => "Radius cannot be negative" ), Keyword.parse("closest-side", "farthest-side") diff --git a/packages/alfa-css/src/value/shape/rectangle.ts b/packages/alfa-css/src/value/shape/rectangle.ts index 842030efec..3b9493a7fc 100644 --- a/packages/alfa-css/src/value/shape/rectangle.ts +++ b/packages/alfa-css/src/value/shape/rectangle.ts @@ -18,7 +18,7 @@ const { either, map, option, pair, take, right, delimited } = Parser; */ export class Rectangle< O extends Length.Fixed | Rectangle.Auto = Length.Fixed | Rectangle.Auto -> extends BasicShape<"rectangle"> { +> extends BasicShape<"rectangle", false> { public static of< O extends Length.Fixed | Rectangle.Auto = Length.Fixed | Rectangle.Auto >(top: O, right: O, bottom: O, left: O): Rectangle { From 3631c9c8482b140eb5292a8c13e3003154aab152 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 11 Oct 2023 15:19:26 +0200 Subject: [PATCH 02/18] Streamline parser --- packages/alfa-css/src/value/shape/inset.ts | 23 ++++------- .../alfa-css/test/value/shape/inset.spec.ts | 40 ++++++++++--------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/packages/alfa-css/src/value/shape/inset.ts b/packages/alfa-css/src/value/shape/inset.ts index 58d2f3b931..cbc287ead0 100644 --- a/packages/alfa-css/src/value/shape/inset.ts +++ b/packages/alfa-css/src/value/shape/inset.ts @@ -11,7 +11,7 @@ import { Length, Percentage } from "../numeric"; import { BasicShape } from "./basic-shape"; -const { either, map, filter, option, pair, right, separatedList, takeAtMost } = +const { delimited, either, map, filter, option, pair, right, separatedList } = Parser; const { parseDelim, parseWhitespace } = Token; @@ -172,11 +172,8 @@ export namespace Inset { const parseLengthPercentage = either(Length.parseBase, Percentage.parseBase); const parseOffsets = map( - pair( - parseLengthPercentage, - takeAtMost(right(option(Token.parseWhitespace), parseLengthPercentage), 3) - ), - ([top, [right = top, bottom = top, left = right]]) => + separatedList(parseLengthPercentage, option(parseWhitespace), 1, 4), + ([top, right = top, bottom = top, left = right]) => [top, right, bottom, left] as const ); @@ -187,13 +184,12 @@ export namespace Inset { ); const parseRadii = map( - pair( - parseRadius, - takeAtMost(right(option(Token.parseWhitespace), parseRadius), 3) - ), + separatedList(parseRadius, option(parseWhitespace), 1, 4), ([ topLeft, - [topRight = topLeft, bottomRight = topLeft, bottomLeft = topRight], + topRight = topLeft, + bottomRight = topLeft, + bottomLeft = topRight, ]) => [topLeft, topRight, bottomRight, bottomLeft] as const ); @@ -201,10 +197,7 @@ export namespace Inset { pair( parseRadii, option( - right( - option(parseWhitespace), - right(parseDelim("/"), right(option(parseWhitespace), parseRadii)) - ) + right(delimited(option(parseWhitespace), parseDelim("/")), parseRadii) ) ), ([horizontal, vertical]) => diff --git a/packages/alfa-css/test/value/shape/inset.spec.ts b/packages/alfa-css/test/value/shape/inset.spec.ts index cdb75ddb8d..736c525adf 100644 --- a/packages/alfa-css/test/value/shape/inset.spec.ts +++ b/packages/alfa-css/test/value/shape/inset.spec.ts @@ -1,7 +1,6 @@ import { test } from "@siteimprove/alfa-test"; -import { Lexer } from "../../../src/syntax/lexer"; -import { Inset } from "../../../src/value/shape/inset"; +import { Inset, Lexer } from "../../../src"; function parse(input: string) { return Inset.parse(Lexer.lex(input)).map(([_, circle]) => circle.toJSON()); @@ -22,25 +21,28 @@ test("parse() parses an inset with square corners", (t) => { }); test("parse() parses an inset with evenly rounded corners", (t) => { - t.deepEqual(parse("inset(1px 2px 3px 4px round 1px 1px 1px 1px)").getUnsafe(), { - type: "basic-shape", - kind: "inset", - offsets: [ - { type: "length", value: 1, unit: "px" }, - { type: "length", value: 2, unit: "px" }, - { type: "length", value: 3, unit: "px" }, - { type: "length", value: 4, unit: "px" }, - ], - corners: { - type: "some", - value: [ - { type: "length", value: 1, unit: "px" }, - { type: "length", value: 1, unit: "px" }, - { type: "length", value: 1, unit: "px" }, + t.deepEqual( + parse("inset(1px 2px 3px 4px round 1px 1px 1px 1px)").getUnsafe(), + { + type: "basic-shape", + kind: "inset", + offsets: [ { type: "length", value: 1, unit: "px" }, + { type: "length", value: 2, unit: "px" }, + { type: "length", value: 3, unit: "px" }, + { type: "length", value: 4, unit: "px" }, ], - }, - }); + corners: { + type: "some", + value: [ + { type: "length", value: 1, unit: "px" }, + { type: "length", value: 1, unit: "px" }, + { type: "length", value: 1, unit: "px" }, + { type: "length", value: 1, unit: "px" }, + ], + }, + } + ); }); test("parse() parses an inset with unevenly rounded corners", (t) => { From 8f534f0b688deeca25904fe2d2f27c704b9aed1e Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Thu, 12 Oct 2023 14:16:58 +0200 Subject: [PATCH 03/18] Work on insets --- packages/alfa-css/src/value/shape/corner.ts | 75 +++++++++++++++++++ packages/alfa-css/src/value/shape/inset.ts | 81 +++++++++++++++++---- packages/alfa-css/tsconfig.json | 1 + 3 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 packages/alfa-css/src/value/shape/corner.ts diff --git a/packages/alfa-css/src/value/shape/corner.ts b/packages/alfa-css/src/value/shape/corner.ts new file mode 100644 index 0000000000..441d0f91d0 --- /dev/null +++ b/packages/alfa-css/src/value/shape/corner.ts @@ -0,0 +1,75 @@ +import { Real } from "@siteimprove/alfa-math"; + +import { LengthPercentage } from "../numeric"; +import { Value } from "../value"; + +type Radius = LengthPercentage; + +export type Corner = Radius | readonly [Radius, Radius]; + +/** + * @internal + */ +export namespace Corner { + export type Canonical = + | LengthPercentage.Canonical + | readonly [LengthPercentage.Canonical, LengthPercentage.Canonical]; + + export function hasCalculation(corner: Corner): boolean { + return LengthPercentage.isLengthPercentage(corner) + ? Value.hasCalculation(corner) + : Value.hasCalculation(corner[0] || Value.hasCalculation(corner[1])); + } + + export function resolve( + resolver: LengthPercentage.Resolver + ): (value: Corner) => Canonical { + function resolveAndClamp( + radius: LengthPercentage + ): LengthPercentage.Canonical { + const resolved = LengthPercentage.resolve(resolver)(radius); + + return LengthPercentage.of( + Real.clamp(resolved.value, 0, Infinity), + resolved.unit + ); + } + + return (value) => + LengthPercentage.isLengthPercentage(value) + ? resolveAndClamp(value) + : [resolveAndClamp(value[0]), resolveAndClamp(value[1])]; + } + + export type PartiallyResolved = + | LengthPercentage.PartiallyResolved + | readonly [ + LengthPercentage.PartiallyResolved, + LengthPercentage.PartiallyResolved + ]; + + export function partiallyResolve( + resolver: LengthPercentage.PartialResolver + ): (value: Corner) => PartiallyResolved { + function resolveAndClamp( + radius: LengthPercentage + ): LengthPercentage.PartiallyResolved { + const resolved = LengthPercentage.partiallyResolve(resolver)(radius); + + if (resolved.hasCalculation()) { + return resolved; + } + + const clamped = Real.clamp(resolved.value, 0, Infinity); + + return LengthPercentage.isPercentage(resolved) + ? LengthPercentage.of(clamped) + : LengthPercentage.of(clamped, resolved.unit); + } + + return (value) => + LengthPercentage.isLengthPercentage(value) + ? resolveAndClamp(value) + : [resolveAndClamp(value[0]), resolveAndClamp(value[1])]; + } +} diff --git a/packages/alfa-css/src/value/shape/inset.ts b/packages/alfa-css/src/value/shape/inset.ts index cbc287ead0..243f69f4ca 100644 --- a/packages/alfa-css/src/value/shape/inset.ts +++ b/packages/alfa-css/src/value/shape/inset.ts @@ -7,9 +7,11 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Function, type Parser as CSSParser, Token } from "../../syntax"; import { Keyword } from "../keyword"; -import { Length, Percentage } from "../numeric"; +import { LengthPercentage } from "../numeric"; +import { Value } from "../value"; import { BasicShape } from "./basic-shape"; +import { Corner } from "./corner"; const { delimited, either, map, filter, option, pair, right, separatedList } = Parser; @@ -22,11 +24,11 @@ const { parseDelim, parseWhitespace } = Token; */ export class Inset< O extends Inset.Offset = Inset.Offset, - C extends Inset.Corner = Inset.Corner -> extends BasicShape<"inset", false> { + C extends Corner = Corner +> extends BasicShape<"inset", HasCalculation> { public static of< O extends Inset.Offset = Inset.Offset, - C extends Inset.Corner = Inset.Corner + C extends Corner = Corner >( offsets: readonly [O, O, O, O], corners: Option @@ -41,7 +43,13 @@ export class Inset< offsets: readonly [O, O, O, O], corners: Option ) { - super("inset", false); + super( + "inset", + (Value.hasCalculation(...offsets) || + corners.some((corners) => + corners.some(Corner.hasCalculation) + )) as unknown as HasCalculation + ); this._offsets = offsets; this._corners = corners; } @@ -86,8 +94,25 @@ export class Inset< return this._corners.map((corners) => corners[3]); } - public resolve(): Inset { - return this; + public resolve(resolver: Inset.Resolver): Inset.Canonical { + // map is losing the length of the arrays + return new Inset( + this._offsets.map(LengthPercentage.resolve(resolver)) as [ + LengthPercentage.Canonical, + LengthPercentage.Canonical, + LengthPercentage.Canonical, + LengthPercentage.Canonical + ], + this._corners.map( + (corners) => + corners.map(Corner.resolve(resolver)) as [ + Corner.Canonical, + Corner.Canonical, + Corner.Canonical, + Corner.Canonical + ] + ) + ); } public equals(value: Inset): boolean; @@ -157,11 +182,7 @@ export class Inset< * @public */ export namespace Inset { - export type Offset = Length.Fixed | Percentage.Fixed; - - export type Radius = Length.Fixed | Percentage.Fixed; - - export type Corner = Radius | readonly [Radius, Radius]; + export type Canonical = Inset; export interface JSON extends BasicShape.JSON<"inset"> { @@ -169,17 +190,29 @@ export namespace Inset { corners: Option.JSON; } - const parseLengthPercentage = either(Length.parseBase, Percentage.parseBase); + export type Offset = LengthPercentage; + + export type Resolver = LengthPercentage.Resolver; + + export type PartiallyResolved = Inset< + LengthPercentage.PartiallyResolved, + Corner.PartiallyResolved + >; + + export function isInset(value: unknown): value is Inset { + return value instanceof Inset; + } const parseOffsets = map( - separatedList(parseLengthPercentage, option(parseWhitespace), 1, 4), + separatedList(LengthPercentage.parse, option(parseWhitespace), 1, 4), ([top, right = top, bottom = top, left = right]) => [top, right, bottom, left] as const ); const parseRadius = filter( - parseLengthPercentage, - ({ value }) => value >= 0, + LengthPercentage.parse, + // https://drafts.csswg.org/css-values/#calc-range + (value) => value.hasCalculation() || value.value >= 0, () => "Radius cannot be negative" ); @@ -233,3 +266,19 @@ export namespace Inset { ([_, [offsets, corners]]) => Inset.of(offsets, corners) ); } + +/** + * Putting this in the Inset namespace clashes with the one in the parent + * Value namespace that it inherit from (through the classes of the same names) + */ +type HasCalculation< + O extends Inset.Offset, + C extends Corner +> = Value.HasCalculation< + [ + O, + // It seems we can't really spread ...[R1, R2] in a conditional. + C extends readonly [infer R extends LengthPercentage, any] ? R : C, + C extends readonly [any, infer R extends LengthPercentage] ? R : C + ] +>; diff --git a/packages/alfa-css/tsconfig.json b/packages/alfa-css/tsconfig.json index 68d324d497..e7ea8cadba 100644 --- a/packages/alfa-css/tsconfig.json +++ b/packages/alfa-css/tsconfig.json @@ -71,6 +71,7 @@ "src/value/shadow.ts", "src/value/shape/basic-shape.ts", "src/value/shape/circle.ts", + "src/value/shape/corner.ts", "src/value/shape/ellipse.ts", "src/value/shape/index.ts", "src/value/shape/inset.ts", From 6b61e91b865d5f448a0dc7dbe36c5ebdb4a4d50d Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Thu, 12 Oct 2023 15:50:37 +0200 Subject: [PATCH 04/18] Make Inset, Polygon and Rectangle calculatable --- packages/alfa-css/src/value/shape/inset.ts | 41 +++++------ packages/alfa-css/src/value/shape/polygon.ts | 70 +++++++++++++++---- .../alfa-css/src/value/shape/rectangle.ts | 63 +++++++++-------- 3 files changed, 109 insertions(+), 65 deletions(-) diff --git a/packages/alfa-css/src/value/shape/inset.ts b/packages/alfa-css/src/value/shape/inset.ts index 243f69f4ca..88cf199acd 100644 --- a/packages/alfa-css/src/value/shape/inset.ts +++ b/packages/alfa-css/src/value/shape/inset.ts @@ -226,26 +226,27 @@ export namespace Inset { ]) => [topLeft, topRight, bottomRight, bottomLeft] as const ); - const parseCorners = map( - pair( - parseRadii, - option( - right(delimited(option(parseWhitespace), parseDelim("/")), parseRadii) - ) - ), - ([horizontal, vertical]) => - vertical - .map( - (vertical) => - [ - [horizontal[0], vertical[0]], - [horizontal[1], vertical[1]], - [horizontal[2], vertical[2]], - [horizontal[3], vertical[3]], - ] as const + const parseCorners: CSSParser = + map( + pair( + parseRadii, + option( + right(delimited(option(parseWhitespace), parseDelim("/")), parseRadii) ) - .getOr(horizontal) - ); + ), + ([horizontal, vertical]) => + vertical + .map( + (vertical) => + [ + [horizontal[0], vertical[0]], + [horizontal[1], vertical[1]], + [horizontal[2], vertical[2]], + [horizontal[3], vertical[3]], + ] as const + ) + .getOr(horizontal) + ); export const parse: CSSParser = map( Function.parse( @@ -263,7 +264,7 @@ export namespace Inset { ) ) ), - ([_, [offsets, corners]]) => Inset.of(offsets, corners) + ([_, [offsets, corners]]) => Inset.of(offsets, corners) ); } diff --git a/packages/alfa-css/src/value/shape/polygon.ts b/packages/alfa-css/src/value/shape/polygon.ts index 10f0985938..9067c14157 100644 --- a/packages/alfa-css/src/value/shape/polygon.ts +++ b/packages/alfa-css/src/value/shape/polygon.ts @@ -8,7 +8,8 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Function, type Parser as CSSParser, Token } from "../../syntax"; import { Keyword } from "../keyword"; -import { Length, Percentage } from "../numeric"; +import { LengthPercentage } from "../numeric"; +import { Value } from "../value"; import { BasicShape } from "./basic-shape"; @@ -23,11 +24,11 @@ const { parseComma, parseWhitespace } = Token; */ export class Polygon< F extends Polygon.Fill = Polygon.Fill, - V extends Length.Fixed | Percentage.Fixed = Length.Fixed | Percentage.Fixed -> extends BasicShape<"polygon", false> { + V extends LengthPercentage = LengthPercentage +> extends BasicShape<"polygon", Value.HasCalculation<[V]>> { public static of< F extends Polygon.Fill = Polygon.Fill, - V extends Length.Fixed | Percentage.Fixed = Length.Fixed | Percentage.Fixed + V extends LengthPercentage = LengthPercentage >(fill: Option, vertices: Iterable>): Polygon { return new Polygon(fill, Array.from(vertices)); } @@ -36,7 +37,13 @@ export class Polygon< private readonly _vertices: Array>; private constructor(fill: Option, vertices: Array>) { - super("polygon", false); + super( + "polygon", + vertices.reduce( + (calc, vertex) => calc || Value.hasCalculation(...vertex), + false + ) as unknown as Value.HasCalculation<[V]> + ); this._fill = fill; this._vertices = vertices; } @@ -49,8 +56,17 @@ export class Polygon< return this._vertices; } - public resolve(): Polygon { - return this; + public resolve(resolver: Polygon.Resolver): Polygon.Canonical { + return new Polygon( + this._fill, + this._vertices.map( + (vertex) => + // map loses the fact that vertex has exactly two elements. + vertex.map( + LengthPercentage.resolve(resolver) + ) as unknown as Polygon.Vertex + ) + ); } public equals(value: Polygon): boolean; @@ -90,26 +106,52 @@ export class Polygon< * @public */ export namespace Polygon { + export type Canonical = Polygon; + export type Fill = Keyword<"nonzero"> | Keyword<"evenodd">; - export type Vertex< - V extends Length.Fixed | Percentage.Fixed = Length.Fixed | Percentage.Fixed - > = readonly [V, V]; + export type Vertex = readonly [ + V, + V + ]; export interface JSON< F extends Fill = Fill, - V extends Length.Fixed | Percentage.Fixed = Length.Fixed | Percentage.Fixed + V extends LengthPercentage = LengthPercentage > extends BasicShape.JSON<"polygon"> { fill: Option.JSON; vertices: Array>>; } - const parseLengthPercentage = either(Length.parseBase, Percentage.parseBase); + export type Resolver = LengthPercentage.Resolver; + + export type PartiallyResolved = Polygon< + Fill, + LengthPercentage.PartiallyResolved + >; + + export type PartialResolver = LengthPercentage.PartialResolver; + + export function partiallyResolve( + resolver: PartialResolver + ): (value: Polygon) => PartiallyResolved { + return (value) => + Polygon.of( + value.fill, + value.vertices.map( + (vertex) => + // map loses the fact that vertex has exactly two elements. + vertex.map( + LengthPercentage.partiallyResolve(resolver) + ) as unknown as Polygon.Vertex + ) + ); + } const parseVertex = separated( - parseLengthPercentage, + LengthPercentage.parse, parseWhitespace, - parseLengthPercentage + LengthPercentage.parse ); export const parse: CSSParser = map( diff --git a/packages/alfa-css/src/value/shape/rectangle.ts b/packages/alfa-css/src/value/shape/rectangle.ts index 3b9493a7fc..706186c531 100644 --- a/packages/alfa-css/src/value/shape/rectangle.ts +++ b/packages/alfa-css/src/value/shape/rectangle.ts @@ -1,14 +1,16 @@ import { Hash } from "@siteimprove/alfa-hash"; import { Parser } from "@siteimprove/alfa-parser"; -import { Function, type Parser as CSSParser, Token } from "../../syntax"; +import { Comma, Function, type Parser as CSSParser, Token } from "../../syntax"; import { Keyword } from "../keyword"; import { Length } from "../numeric"; +import { Value } from "../value"; import { BasicShape } from "./basic-shape"; -const { either, map, option, pair, take, right, delimited } = Parser; +const { either, map, option, pair, take, right, delimited, separatedList } = + Parser; /** * {@link https://drafts.fxtf.org/css-masking/#funcdef-clip-rect} @@ -17,11 +19,14 @@ const { either, map, option, pair, take, right, delimited } = Parser; * @deprecated Deprecated as of CSS Masking Module Level 1 */ export class Rectangle< - O extends Length.Fixed | Rectangle.Auto = Length.Fixed | Rectangle.Auto -> extends BasicShape<"rectangle", false> { - public static of< - O extends Length.Fixed | Rectangle.Auto = Length.Fixed | Rectangle.Auto - >(top: O, right: O, bottom: O, left: O): Rectangle { + O extends Length | Rectangle.Auto = Length | Rectangle.Auto +> extends BasicShape<"rectangle", Value.HasCalculation<[O, O, O, O]>> { + public static of( + top: O, + right: O, + bottom: O, + left: O + ): Rectangle { return new Rectangle(top, right, bottom, left); } @@ -31,7 +36,7 @@ export class Rectangle< public readonly _left: O; private constructor(top: O, right: O, bottom: O, left: O) { - super("rectangle", false); + super("rectangle", Value.hasCalculation(top, right, bottom, left)); this._top = top; this._right = right; this._bottom = bottom; @@ -54,8 +59,13 @@ export class Rectangle< return this._left; } - public resolve(): Rectangle { - return this; + public resolve(resolver: Rectangle.Resolver): Rectangle.Canonical { + return new Rectangle( + this._top.resolve(resolver), + this._right.resolve(resolver), + this._bottom.resolve(resolver), + this._left.resolve(resolver) + ); } public equals(value: Rectangle): boolean; @@ -100,42 +110,33 @@ export class Rectangle< * @deprecated Deprecated as of CSS Masking Module Level 1 */ export namespace Rectangle { + export type Canonical = Rectangle; + export type Auto = Keyword<"auto">; export interface JSON extends BasicShape.JSON<"rectangle"> { - top: Length.Fixed.JSON | Keyword.JSON; - right: Length.Fixed.JSON | Keyword.JSON; - bottom: Length.Fixed.JSON | Keyword.JSON; - left: Length.Fixed.JSON | Keyword.JSON; + top: Length.JSON | Keyword.JSON; + right: Length.JSON | Keyword.JSON; + bottom: Length.JSON | Keyword.JSON; + left: Length.JSON | Keyword.JSON; } + export type Resolver = Length.Resolver; + export function isRectangle(value: unknown): value is Rectangle { return value instanceof Rectangle; } - const parseLengthAuto = either(Length.parseBase, Keyword.parse("auto")); + const parseLengthAuto = either(Length.parse, Keyword.parse("auto")); export const parse: CSSParser = map( Function.parse( "rect", either( - pair( - parseLengthAuto, - take(right(option(Token.parseWhitespace), parseLengthAuto), 3) - ), - pair( - parseLengthAuto, - take( - right( - delimited(option(Token.parseWhitespace), Token.parseComma), - parseLengthAuto - ), - 3 - ) - ) + separatedList(parseLengthAuto, option(Token.parseWhitespace), 4, 4), + separatedList(parseLengthAuto, Comma.parse, 4, 4) ) ), - ([_, [top, [right, bottom, left]]]) => - Rectangle.of(top, right, bottom, left) + ([_, [top, right, bottom, left]]) => Rectangle.of(top, right, bottom, left) ); } From 5b30066ccbaff16e1116694655bcba5503d02907 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 13 Oct 2023 11:38:49 +0200 Subject: [PATCH 05/18] Make Shape calculatable --- packages/alfa-css/src/value/shape/circle.ts | 2 +- packages/alfa-css/src/value/shape/ellipse.ts | 2 +- packages/alfa-css/src/value/shape/inset.ts | 25 ++++++ packages/alfa-css/src/value/shape/polygon.ts | 4 + packages/alfa-css/src/value/shape/shape.ts | 93 ++++++++++++++++---- 5 files changed, 108 insertions(+), 18 deletions(-) diff --git a/packages/alfa-css/src/value/shape/circle.ts b/packages/alfa-css/src/value/shape/circle.ts index c0335a338e..bba4a454fc 100644 --- a/packages/alfa-css/src/value/shape/circle.ts +++ b/packages/alfa-css/src/value/shape/circle.ts @@ -102,7 +102,7 @@ export namespace Circle { export type PartialResolver = Radius.PartialResolver & Position.PartialResolver; - export function partiallyResolved( + export function partiallyResolve( resolver: PartialResolver ): (value: Circle) => PartiallyResolved { return (value) => diff --git a/packages/alfa-css/src/value/shape/ellipse.ts b/packages/alfa-css/src/value/shape/ellipse.ts index 34d70ab8c2..8c27576169 100644 --- a/packages/alfa-css/src/value/shape/ellipse.ts +++ b/packages/alfa-css/src/value/shape/ellipse.ts @@ -122,7 +122,7 @@ export namespace Ellipse { export type PartialResolver = Radius.PartialResolver & Position.PartialResolver; - export function partiallyResolved( + export function partiallyResolve( resolver: PartialResolver ): (value: Ellipse) => PartiallyResolved { return (value) => diff --git a/packages/alfa-css/src/value/shape/inset.ts b/packages/alfa-css/src/value/shape/inset.ts index 88cf199acd..7f89191d46 100644 --- a/packages/alfa-css/src/value/shape/inset.ts +++ b/packages/alfa-css/src/value/shape/inset.ts @@ -199,6 +199,31 @@ export namespace Inset { Corner.PartiallyResolved >; + export type PartialResolver = LengthPercentage.PartialResolver; + + export function partiallyResolve( + resolver: PartialResolver + ): (value: Inset) => PartiallyResolved { + return (value) => + Inset.of( + value.offsets.map(LengthPercentage.partiallyResolve(resolver)) as [ + LengthPercentage.PartiallyResolved, + LengthPercentage.PartiallyResolved, + LengthPercentage.PartiallyResolved, + LengthPercentage.PartiallyResolved + ], + value.corners.map( + (corners) => + corners.map(Corner.partiallyResolve(resolver)) as [ + Corner.PartiallyResolved, + Corner.PartiallyResolved, + Corner.PartiallyResolved, + Corner.PartiallyResolved + ] + ) + ); + } + export function isInset(value: unknown): value is Inset { return value instanceof Inset; } diff --git a/packages/alfa-css/src/value/shape/polygon.ts b/packages/alfa-css/src/value/shape/polygon.ts index 9067c14157..4dfcf84b2e 100644 --- a/packages/alfa-css/src/value/shape/polygon.ts +++ b/packages/alfa-css/src/value/shape/polygon.ts @@ -148,6 +148,10 @@ export namespace Polygon { ); } + export function isPolygon(value: unknown): value is Polygon { + return value instanceof Polygon; + } + const parseVertex = separated( LengthPercentage.parse, parseWhitespace, diff --git a/packages/alfa-css/src/value/shape/shape.ts b/packages/alfa-css/src/value/shape/shape.ts index 8408e27ee2..cb155415cd 100644 --- a/packages/alfa-css/src/value/shape/shape.ts +++ b/packages/alfa-css/src/value/shape/shape.ts @@ -1,6 +1,7 @@ import { Hash } from "@siteimprove/alfa-hash"; import { Parser } from "@siteimprove/alfa-parser"; import { Err, Result } from "@siteimprove/alfa-result"; +import { Selective } from "@siteimprove/alfa-selective"; import { Slice } from "@siteimprove/alfa-slice"; import { Value } from "../value"; @@ -23,7 +24,7 @@ const { either } = Parser; export class Shape< S extends Shape.Basic = Shape.Basic, B extends Box.Geometry = Box.Geometry -> extends Value<"shape", false> { +> extends Value<"shape", Value.HasCalculation<[S]>> { public static of< S extends Shape.Basic = Shape.Basic, B extends Box.Geometry = Box.Geometry @@ -35,7 +36,7 @@ export class Shape< private readonly _box: B; private constructor(shape: S, box: B) { - super("shape", false); + super("shape", Value.hasCalculation(shape)); this._shape = shape; this._box = box; } @@ -48,8 +49,8 @@ export class Shape< return this._box; } - public resolve(): Shape { - return this; + public resolve(resolver: Shape.Resolver): Shape.Canonical { + return new Shape(this._shape.resolve(resolver), this._box); } public equals(value: Shape): boolean; @@ -85,30 +86,90 @@ export class Shape< * @public */ export namespace Shape { + export type Canonical = Shape; + /** * {@link https://drafts.csswg.org/css-shapes/#typedef-basic-shape} */ export type Basic = Circle | Ellipse | Inset | Polygon | Rectangle; - export interface JSON extends Value.JSON<"shape"> { - shape: + namespace Basic { + export type Canonical = + | Circle.Canonical + | Ellipse.Canonical + | Inset.Canonical + | Polygon.Canonical + | Rectangle.Canonical; + + export type JSON = | Circle.JSON | Ellipse.JSON | Inset.JSON | Polygon.JSON | Rectangle.JSON; + + export type Resolver = Circle.Resolver & + Ellipse.Resolver & + Inset.Resolver & + Polygon.Resolver & + Rectangle.Resolver; + + export type PartiallyResolved = + | Circle.PartiallyResolved + | Ellipse.PartiallyResolved + | Inset.PartiallyResolved + | Polygon.PartiallyResolved + | Rectangle.Canonical; + + export type PartialResolver = Circle.PartialResolver & + Ellipse.PartialResolver & + Inset.PartialResolver & + Polygon.PartialResolver & + Rectangle.Resolver; + + export function partiallyResolve( + resolver: PartialResolver + ): (value: Basic) => PartiallyResolved { + return (value) => + Selective.of(value) + .if(Circle.isCircle, Circle.partiallyResolve(resolver)) + .if(Ellipse.isEllipse, Ellipse.partiallyResolve(resolver)) + .if(Inset.isInset, Inset.partiallyResolve(resolver)) + .if(Polygon.isPolygon, Polygon.partiallyResolve(resolver)) + .else((rectangle) => rectangle.resolve(resolver)) + .get(); + } + + /** + * @remarks + * This does not parse the deprecated `rect()` shape. + * + * @internal + */ + export const parse = either< + Slice, + Circle | Ellipse | Inset | Polygon, + string + >(Circle.parse, Ellipse.parse, Inset.parse, Polygon.parse); + } + + export interface JSON extends Value.JSON<"shape"> { + shape: Basic.JSON; box: Box.Geometry.JSON; } - /** - * @remarks - * This does not parse the deprecated `rect()` shape. - */ - const parseBasicShape = either< - Slice, - Circle | Ellipse | Inset | Polygon, - string - >(Circle.parse, Ellipse.parse, Inset.parse, Polygon.parse); + export type Resolver = Basic.Resolver; + + export type PartiallyResolved = Shape; + + export type PartialResolver = Basic.PartialResolver; + + export function partiallyResolve( + resolver: PartialResolver + ): (value: Shape) => PartiallyResolved { + return (value) => + Shape.of(Basic.partiallyResolve(resolver)(value.shape), value.box); + } /** * @remarks @@ -130,7 +191,7 @@ export namespace Shape { skipWhitespace(); if (shape === undefined) { - const result = parseBasicShape(input); + const result = Basic.parse(input); if (result.isOk()) { [input, shape] = result.get(); From 200b86358785d97c26d7b9f0da671549f2d594f8 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 13 Oct 2023 13:59:07 +0200 Subject: [PATCH 06/18] Clean dirty Position hacks --- .../src/value/image/gradient/radial/radial.ts | 2 +- .../alfa-css/src/value/position/component.ts | 46 +--- .../alfa-css/src/value/position/position.ts | 246 +++++++----------- packages/alfa-css/src/value/position/side.ts | 116 +++------ .../src/property/background-position-x.ts | 4 +- .../src/property/background-position-y.ts | 2 +- 6 files changed, 147 insertions(+), 269 deletions(-) diff --git a/packages/alfa-css/src/value/image/gradient/radial/radial.ts b/packages/alfa-css/src/value/image/gradient/radial/radial.ts index b944daa294..611247ea91 100644 --- a/packages/alfa-css/src/value/image/gradient/radial/radial.ts +++ b/packages/alfa-css/src/value/image/gradient/radial/radial.ts @@ -150,7 +150,7 @@ export namespace Radial { export type Canonical = Radial< Item.Canonical, Shape.Canonical, - Position.Fixed + Position.Canonical >; export interface JSON extends Value.JSON<"gradient"> { diff --git a/packages/alfa-css/src/value/position/component.ts b/packages/alfa-css/src/value/position/component.ts index 8efe9f6253..373b3c48ff 100644 --- a/packages/alfa-css/src/value/position/component.ts +++ b/packages/alfa-css/src/value/position/component.ts @@ -1,7 +1,6 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Parser as CSSParser } from "../../syntax"; -import { Unit } from "../../unit"; import { Keyword } from "../keyword"; import { LengthPercentage } from "../numeric"; @@ -18,9 +17,8 @@ export type Component< S extends Keywords.Horizontal | Keywords.Vertical = | Keywords.Horizontal | Keywords.Vertical, - U extends Unit.Length = Unit.Length, - CALC extends boolean = boolean -> = Keywords.Center | Side; + O extends LengthPercentage = LengthPercentage +> = Keywords.Center | Side; /** * @public @@ -34,12 +32,6 @@ export namespace Component { S extends Keywords.Horizontal | Keywords.Vertical > = Keywords.Center | Side.PartiallyResolved; - /** - * @internal - */ - export type Fixed = - Component; - export type JSON = Keyword.JSON | Side.JSON; export type Resolver = Side.Resolver; @@ -66,37 +58,25 @@ export namespace Component { * @internal */ export function parseOffset< - T extends Keywords.Horizontal | Keywords.Vertical, - CALC extends boolean - >( - side: T, - withCalculation: CALC - ): CSSParser> { - const parser = ( - withCalculation ? LengthPercentage.parse : LengthPercentage.parseBase - ) as CSSParser>; - - return map(parser, (value) => Side.of(side, value)); + T extends Keywords.Horizontal | Keywords.Vertical + >(side: T): CSSParser> { + return map(LengthPercentage.parse, (value) => Side.of(side, value)); } // "center" is included in Side.parse[Horizontal, Vertical] /** * @internal */ - export const parseHorizontal = ( - withCalculation: CALC - ) => - either( - parseOffset(Keyword.of("left"), withCalculation), - Side.parseHorizontal(withCalculation) - ); + export const parseHorizontal = either( + parseOffset(Keyword.of("left")), + Side.parseHorizontal + ); /** * @internal */ - export const parseVertical = (withCalculation: CALC) => - either( - parseOffset(Keyword.of("top"), withCalculation), - Side.parseVertical(withCalculation) - ); + export const parseVertical = either( + parseOffset(Keyword.of("top")), + Side.parseVertical + ); } diff --git a/packages/alfa-css/src/value/position/position.ts b/packages/alfa-css/src/value/position/position.ts index d0c97cec7b..d3cfe36e2e 100644 --- a/packages/alfa-css/src/value/position/position.ts +++ b/packages/alfa-css/src/value/position/position.ts @@ -1,6 +1,7 @@ import { Hash } from "@siteimprove/alfa-hash"; import { Parser } from "@siteimprove/alfa-parser"; import { Err } from "@siteimprove/alfa-result"; +import { Slice } from "@siteimprove/alfa-slice"; import { type Parser as CSSParser, Token } from "../../syntax"; import { Unit } from "../../unit"; @@ -31,10 +32,9 @@ export class Position< H extends Position.Keywords.Horizontal = Position.Keywords.Horizontal, V extends Position.Keywords.Vertical = Position.Keywords.Vertical, HC extends Position.Component = Position.Component, - VC extends Position.Component = Position.Component, - CALC extends boolean = boolean + VC extends Position.Component = Position.Component > - extends Value<"position", CALC> + extends Value<"position", Value.HasCalculation<[HC, VC]>> implements Resolvable, Position.Resolver> { public static of< @@ -42,20 +42,15 @@ export class Position< V extends Position.Keywords.Vertical = Position.Keywords.Vertical, HC extends Position.Component = Position.Component, VC extends Position.Component = Position.Component - >( - horizontal: HC, - vertical: VC - ): Position> { - const calculation = (horizontal.hasCalculation() || - vertical.hasCalculation()) as Value.HasCalculation<[HC, VC]>; - return new Position(horizontal, vertical, calculation); + >(horizontal: HC, vertical: VC): Position { + return new Position(horizontal, vertical); } private readonly _horizontal: HC; private readonly _vertical: VC; - private constructor(horizontal: HC, vertical: VC, calculation: CALC) { - super("position", calculation); + private constructor(horizontal: HC, vertical: VC) { + super("position", Value.hasCalculation(horizontal, vertical)); this._horizontal = horizontal; this._vertical = vertical; } @@ -77,8 +72,7 @@ export class Position< Position.Component.resolve({ length: resolver.length, percentageBase: resolver.percentageVBase, - })(this._vertical), - false + })(this._vertical) ); } @@ -114,7 +108,7 @@ export namespace Position { export type Canonical< H extends Keywords.Horizontal = Keywords.Horizontal, V extends Keywords.Vertical = Keywords.Vertical - > = Position, Component.Canonical, false>; + > = Position, Component.Canonical>; export type PartiallyResolved< H extends Keywords.Horizontal = Keywords.Horizontal, @@ -126,14 +120,6 @@ export namespace Position { Component.PartiallyResolved >; - /** - * @internal - */ - export type Fixed< - H extends Keywords.Horizontal = Keywords.Horizontal, - V extends Keywords.Vertical = Keywords.Vertical - > = Position, Component.Fixed, false>; - export interface JSON extends Value.JSON<"position"> { horizontal: Component.JSON; vertical: Component.JSON; @@ -194,14 +180,14 @@ export namespace Position { * - 2 tokens: H V | H v | h V | h v | V H <- Obs! no V h | v H | v h * - 1 token: H | V | h */ - const mapHV = ([horizontal, vertical]: [ - Component, - Component + const mapHV = ([horizontal, vertical]: [ + Component, + Component ]) => Position.of(horizontal, vertical); - const mapVH = ([vertical, horizontal]: [ - Component, - Component + const mapVH = ([vertical, horizontal]: [ + Component, + Component ]) => Position.of(horizontal, vertical); const { @@ -211,125 +197,89 @@ export namespace Position { parseVerticalKeyword, } = Side; - const parse4 = (withCalculation: CALC) => - either( - map( - pair( - parseHorizontalKeywordValue(withCalculation), - right( - Token.parseWhitespace, - parseVerticalKeywordValue(withCalculation) - ) - ), - mapHV + const parse4 = either( + map( + pair( + parseHorizontalKeywordValue, + right(Token.parseWhitespace, parseVerticalKeywordValue) ), - map( - pair( - parseVerticalKeywordValue(withCalculation), - right( - Token.parseWhitespace, - parseHorizontalKeywordValue(withCalculation) - ) - ), - mapVH - ) - ); + mapHV + ), + map( + pair( + parseVerticalKeywordValue, + right(Token.parseWhitespace, parseHorizontalKeywordValue) + ), + mapVH + ) + ); // Hh V | H Vv | Vv H | V Hh - const parse3 = (withCalculation: CALC) => - either( - map( - either( - pair( - parseHorizontalKeywordValue(withCalculation), - right(Token.parseWhitespace, parseVerticalKeyword(withCalculation)) - ), - pair( - parseHorizontalKeyword(withCalculation), - right( - Token.parseWhitespace, - parseVerticalKeywordValue(withCalculation) - ) - ) + const parse3 = either( + map( + either( + pair( + parseHorizontalKeywordValue, + right(Token.parseWhitespace, parseVerticalKeyword) ), - mapHV + pair( + parseHorizontalKeyword, + right(Token.parseWhitespace, parseVerticalKeywordValue) + ) ), - map( - either( - pair( - parseVerticalKeywordValue(withCalculation), - right( - Token.parseWhitespace, - parseHorizontalKeyword(withCalculation) - ) - ), - pair( - parseVerticalKeyword(withCalculation), - right( - Token.parseWhitespace, - parseHorizontalKeywordValue(withCalculation) - ) - ) + mapHV + ), + map( + either( + pair( + parseVerticalKeywordValue, + right(Token.parseWhitespace, parseHorizontalKeyword) ), - mapVH - ) - ); + pair( + parseVerticalKeyword, + right(Token.parseWhitespace, parseHorizontalKeywordValue) + ) + ), + mapVH + ) + ); // H V | H v | h V | h v | V H = (H | h) (V | v) | V H - const parse2 = (withCalculation: CALC) => - either( - map( - pair( - either( - parseHorizontalKeyword(withCalculation), - Component.parseOffset(Keyword.of("left"), withCalculation) - ), - right( - Token.parseWhitespace, - either( - parseVerticalKeyword(withCalculation), - Component.parseOffset(Keyword.of("top"), withCalculation) - ) - ) + const parse2 = either( + map( + pair( + either( + parseHorizontalKeyword, + Component.parseOffset(Keyword.of("left")) ), - mapHV + right( + Token.parseWhitespace, + either(parseVerticalKeyword, Component.parseOffset(Keyword.of("top"))) + ) ), - map( - pair( - parseVerticalKeyword(withCalculation), - right(Token.parseWhitespace, parseHorizontalKeyword(withCalculation)) - ), - mapVH - ) - ); - - type withCalculation = Position< - Keywords.Horizontal, - Keywords.Vertical, - Component, - Component, - CALC - >; + mapHV + ), + map( + pair( + parseVerticalKeyword, + right(Token.parseWhitespace, parseHorizontalKeyword) + ), + mapVH + ) + ); // H | V | h - const parse1 = (withCalculation: CALC) => - either( - map( - parseHorizontalKeyword(withCalculation), - (horizontal) => - Position.of(horizontal, Keyword.of("center")) as withCalculation - ), - map( - parseVerticalKeyword(withCalculation), - (vertical) => - Position.of(Keyword.of("center"), vertical) as withCalculation - ), - map( - Component.parseOffset(Keyword.of("left"), withCalculation), - (horizontal) => - Position.of(horizontal, Keyword.of("center")) as withCalculation - ) - ); + const parse1 = either, Position, string>( + map(parseHorizontalKeyword, (horizontal) => + Position.of(horizontal, Keyword.of("center")) + ), + map(parseVerticalKeyword, (vertical) => + Position.of(Keyword.of("center"), vertical) + ), + map(Component.parseOffset(Keyword.of("left")), (horizontal) => + Position.of(horizontal, Keyword.of("center")) + ) + ); /** * Parse a position, optionally accepting legacy 3-values syntax. @@ -342,26 +292,10 @@ export namespace Position { */ export function parse(legacySyntax: boolean = false): CSSParser { return either( - parse4(true), - legacySyntax - ? parse3(true) - : () => Err.of("Three-value syntax is not allowed"), - parse2(true), - parse1(true) - ); - } - - /** - * @internal - */ - export function parseBase(legacySyntax: boolean = false): CSSParser { - return either( - parse4(false), - legacySyntax - ? parse3(false) - : () => Err.of("Three-value syntax is not allowed"), - parse2(false), - parse1(false) + parse4, + legacySyntax ? parse3 : () => Err.of("Three-value syntax is not allowed"), + parse2, + parse1 ); } } diff --git a/packages/alfa-css/src/value/position/side.ts b/packages/alfa-css/src/value/position/side.ts index 90c3fc8e8e..a91a6b62d3 100644 --- a/packages/alfa-css/src/value/position/side.ts +++ b/packages/alfa-css/src/value/position/side.ts @@ -2,10 +2,9 @@ import { Hash } from "@siteimprove/alfa-hash"; import { Option } from "@siteimprove/alfa-option"; import { Parser } from "@siteimprove/alfa-parser"; import { Parser as CSSParser, Token } from "../../syntax"; -import { Unit } from "../../unit"; import { Keyword } from "../keyword"; -import { Length, LengthPercentage } from "../numeric"; +import { LengthPercentage } from "../numeric"; import { Resolvable } from "../resolvable"; import { Value } from "../value"; @@ -20,37 +19,29 @@ export class Side< S extends Keywords.Vertical | Keywords.Horizontal = | Keywords.Vertical | Keywords.Horizontal, - U extends Unit.Length = Unit.Length, - CALC extends boolean = boolean, - O extends LengthPercentage = LengthPercentage + O extends LengthPercentage = LengthPercentage > - extends Value<"side", CALC> + extends Value<"side", Value.HasCalculation<[O]>> implements Resolvable, Side.Resolver> { public static of( side: S - ): Side; + ): Side; public static of< S extends Keywords.Vertical | Keywords.Horizontal, - U extends Unit.Length, - CALC extends boolean, - O extends LengthPercentage - >(side: S, offset: O): Side; + O extends LengthPercentage + >(side: S, offset: O): Side; public static of< S extends Keywords.Vertical | Keywords.Horizontal, - U extends Unit.Length, - CALC extends boolean, - O extends LengthPercentage - >(side: S, offset: Option): Side; + O extends LengthPercentage + >(side: S, offset: Option): Side; public static of< S extends Keywords.Vertical | Keywords.Horizontal, - U extends Unit.Length, - CALC extends boolean, - O extends LengthPercentage - >(side: S, offset?: O | Option): Side { + O extends LengthPercentage + >(side: S, offset?: O | Option): Side { return new Side( side, Option.isOption(offset) ? offset : Option.from(offset) @@ -61,7 +52,10 @@ export class Side< private readonly _offset: Option; private constructor(side: S, offset: Option) { - super("side", offset.some((offset) => offset.hasCalculation()) as CALC); + super( + "side", + offset.some(Value.hasCalculation) as Value.HasCalculation<[O]> + ); this._side = side; this._offset = offset; } @@ -113,11 +107,11 @@ export class Side< */ export namespace Side { export type Canonical = - Side; + Side; export type PartiallyResolved< S extends Keywords.Vertical | Keywords.Horizontal - > = Side; + > = Side; export interface JSON extends Value.JSON<"side"> { side: Keyword.JSON; @@ -126,7 +120,7 @@ export namespace Side { export type Resolver = LengthPercentage.Resolver; - export type PartialResolver = Length.Resolver; + export type PartialResolver = LengthPercentage.PartialResolver; export function partiallyResolve< S extends Keywords.Vertical | Keywords.Horizontal @@ -145,74 +139,46 @@ export namespace Side { /** * Parse a side keyword (top/bottom/left/right) or "center" */ - function parseKeyword< - S extends Keywords.Horizontal | Keywords.Vertical, - CALC extends boolean - >( - parser: CSSParser, - // This is a useless parameter, temporarily used to enforce inference of CALC - // at call sites. - withCalculation: CALC - ): CSSParser | Side> { + function parseKeyword( + parser: CSSParser + ): CSSParser | Side> { return either( Keywords.parseCenter, - // This is asserting false => true i.e. losing the fact that there is - // no calculation in the Keyword. This is acceptable. - map(parser, (side) => Side.of(side) as Side) + map(parser, (side) => Side.of(side) as Side) ); } /** * Parse a side keyword followed by an offset (length-percentage). - * - * @TODO - * The withCalculation parameter (and CALC type parameter) is temporally needed - * until Shape and Gradient are properly migrated to calculatable values. */ - function parseKeywordValue< - S extends Keywords.Horizontal | Keywords.Vertical, - CALC extends boolean - >( - parser: CSSParser, - withCalculation: CALC - ): CSSParser> { - const offsetParser = ( - withCalculation ? LengthPercentage.parse : LengthPercentage.parseBase - ) as CSSParser>; - + function parseKeywordValue( + parser: CSSParser + ): CSSParser> { return map( - pair(parser, right(Token.parseWhitespace, offsetParser)), + pair(parser, right(Token.parseWhitespace, LengthPercentage.parse)), ([keyword, value]) => Side.of(keyword, value) ); } - export const parseHorizontalKeywordValue = ( - withCalculation: CALC - ) => parseKeywordValue(Keywords.parseHorizontal, withCalculation); + export const parseHorizontalKeywordValue = parseKeywordValue( + Keywords.parseHorizontal + ); - export const parseHorizontalKeyword = ( - withCalculation: CALC - ) => parseKeyword(Keywords.parseHorizontal, withCalculation); + export const parseHorizontalKeyword = parseKeyword(Keywords.parseHorizontal); - export const parseHorizontal = ( - withCalculation: CALC - ) => - either( - parseHorizontalKeyword(withCalculation), - parseHorizontalKeywordValue(withCalculation) - ); + export const parseHorizontal = either( + parseHorizontalKeyword, + parseHorizontalKeywordValue + ); - export const parseVerticalKeywordValue = ( - withCalculation: CALC - ) => parseKeywordValue(Keywords.parseVertical, withCalculation); + export const parseVerticalKeywordValue = parseKeywordValue( + Keywords.parseVertical + ); - export const parseVerticalKeyword = ( - withCalculation: CALC - ) => parseKeyword(Keywords.parseVertical, withCalculation); + export const parseVerticalKeyword = parseKeyword(Keywords.parseVertical); - export const parseVertical = (withCalculation: CALC) => - either( - parseVerticalKeyword(withCalculation), - parseVerticalKeywordValue(withCalculation) - ); + export const parseVertical = either( + parseVerticalKeyword, + parseVerticalKeywordValue + ); } diff --git a/packages/alfa-style/src/property/background-position-x.ts b/packages/alfa-style/src/property/background-position-x.ts index 77d69af10a..4eb9ddd790 100644 --- a/packages/alfa-style/src/property/background-position-x.ts +++ b/packages/alfa-style/src/property/background-position-x.ts @@ -19,9 +19,7 @@ namespace Computed { Position.Component.PartiallyResolved; } -const parse = List.parseCommaSeparated( - Position.Component.parseHorizontal(true) -); +const parse = List.parseCommaSeparated(Position.Component.parseHorizontal); /** * @internal diff --git a/packages/alfa-style/src/property/background-position-y.ts b/packages/alfa-style/src/property/background-position-y.ts index d0e623ca0a..3800404432 100644 --- a/packages/alfa-style/src/property/background-position-y.ts +++ b/packages/alfa-style/src/property/background-position-y.ts @@ -19,7 +19,7 @@ namespace Computed { Position.Component.PartiallyResolved; } -const parse = List.parseCommaSeparated(Position.Component.parseVertical(true)); +const parse = List.parseCommaSeparated(Position.Component.parseVertical); /** * @internal From d38ec150e3efb584c39b134d6647713805e21030 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 13 Oct 2023 14:35:38 +0200 Subject: [PATCH 07/18] Add calculation examples to all shapes --- .../alfa-css/test/value/shape/circle.spec.ts | 103 ++++++++------ .../alfa-css/test/value/shape/ellipse.spec.ts | 93 ++++++++++--- .../alfa-css/test/value/shape/inset.spec.ts | 50 +++++++ .../alfa-css/test/value/shape/polygon.spec.ts | 61 +++++++- .../test/value/shape/rectangle.spec.ts | 131 +++++++++--------- 5 files changed, 304 insertions(+), 134 deletions(-) diff --git a/packages/alfa-css/test/value/shape/circle.spec.ts b/packages/alfa-css/test/value/shape/circle.spec.ts index e35de693ee..af597eed05 100644 --- a/packages/alfa-css/test/value/shape/circle.spec.ts +++ b/packages/alfa-css/test/value/shape/circle.spec.ts @@ -1,99 +1,114 @@ import { test } from "@siteimprove/alfa-test"; -import { Lexer } from "../../../src/syntax/lexer"; -import { Circle } from "../../../src/value/shape/circle"; +import { Circle, Lexer } from "../../../src"; function parse(input: string) { - return Circle.parse(Lexer.lex(input)).map(([_, circle]) => circle.toJSON()); + return Circle.parse(Lexer.lex(input)).getUnsafe()[1].toJSON(); } test("parse() parses a circle with just a radius", (t) => { - t.deepEqual(parse("circle(farthest-side)").getUnsafe(), { + t.deepEqual(parse("circle(farthest-side)"), { type: "basic-shape", kind: "circle", radius: { type: "basic-shape", kind: "radius", - value: { - type: "keyword", - value: "farthest-side", - }, + value: { type: "keyword", value: "farthest-side" }, }, center: { type: "position", - vertical: { - type: "keyword", - value: "center", - }, - horizontal: { - type: "keyword", - value: "center", - }, + vertical: { type: "keyword", value: "center" }, + horizontal: { type: "keyword", value: "center" }, }, }); }); test("parse() parses a circle with just a center", (t) => { - t.deepEqual(parse("circle(at left)").getUnsafe(), { + t.deepEqual(parse("circle(at left)"), { type: "basic-shape", kind: "circle", radius: { type: "basic-shape", kind: "radius", - value: { - type: "keyword", - value: "closest-side", - }, + value: { type: "keyword", value: "closest-side" }, }, center: { type: "position", - vertical: { - type: "keyword", - value: "center", - }, + vertical: { type: "keyword", value: "center" }, horizontal: { type: "side", offset: null, - side: { - type: "keyword", - value: "left", - }, + side: { type: "keyword", value: "left" }, }, }, }); }); test("parse() parses a circle with both radius and center", (t) => { - t.deepEqual(parse("circle(10px at left)").getUnsafe(), { + t.deepEqual(parse("circle(10px at left)"), { type: "basic-shape", kind: "circle", radius: { type: "basic-shape", kind: "radius", - value: { - type: "length", - value: 10, - unit: "px", - }, + value: { type: "length", value: 10, unit: "px" }, }, center: { type: "position", - vertical: { - type: "keyword", - value: "center", - }, + vertical: { type: "keyword", value: "center" }, horizontal: { type: "side", offset: null, - side: { - type: "keyword", - value: "left", - }, + side: { type: "keyword", value: "left" }, }, }, }); }); test("parse() fails if there is a negative radius", (t) => { - t.deepEqual(parse("circle(-1px)").isErr(), true); + t.deepEqual(Circle.parse(Lexer.lex("circle(-1px)")).isErr(), true); +}); + +test("parse() accepts calculated radius", (t) => { + t.deepEqual(parse("circle(calc(10px + 1%) at left)"), { + type: "basic-shape", + kind: "circle", + radius: { + type: "basic-shape", + kind: "radius", + value: { + type: "length-percentage", + math: { + type: "math expression", + expression: { + type: "calculation", + arguments: [ + { + type: "sum", + operands: [ + { + type: "value", + value: { type: "length", value: 10, unit: "px" }, + }, + { + type: "value", + value: { type: "percentage", value: 0.01 }, + }, + ], + }, + ], + }, + }, + }, + }, + center: { + type: "position", + vertical: { type: "keyword", value: "center" }, + horizontal: { + type: "side", + offset: null, + side: { type: "keyword", value: "left" }, + }, + }, + }); }); diff --git a/packages/alfa-css/test/value/shape/ellipse.spec.ts b/packages/alfa-css/test/value/shape/ellipse.spec.ts index ca01180d5a..539ba45657 100644 --- a/packages/alfa-css/test/value/shape/ellipse.spec.ts +++ b/packages/alfa-css/test/value/shape/ellipse.spec.ts @@ -1,25 +1,67 @@ import { test } from "@siteimprove/alfa-test"; -import { Lexer } from "../../../src/syntax/lexer"; -import { Ellipse } from "../../../src/value/shape/ellipse"; +import { Ellipse, Lexer } from "../../../src"; function parse(input: string) { - return Ellipse.parse(Lexer.lex(input)).map(([_, ellipse]) => - ellipse.toJSON() - ); + return Ellipse.parse(Lexer.lex(input)).getUnsafe()[1].toJSON(); } test("parse() parses an ellipse", (t) => { - t.deepEqual(parse("ellipse(1px 3px at right)").getUnsafe(), { + t.deepEqual(parse("ellipse(1px 3px at right)"), { + type: "basic-shape", + kind: "ellipse", + rx: { + type: "basic-shape", + kind: "radius", + value: { type: "length", value: 1, unit: "px" }, + }, + ry: { + type: "basic-shape", + kind: "radius", + value: { type: "length", value: 3, unit: "px" }, + }, + center: { + type: "position", + vertical: { type: "keyword", value: "center" }, + horizontal: { + type: "side", + offset: null, + side: { type: "keyword", value: "right" }, + }, + }, + }); +}); + +test("parse() accepts calculated radii", (t) => { + t.deepEqual(parse("ellipse(calc(1em - 10%) calc(1px + 1ch) at right)"), { type: "basic-shape", kind: "ellipse", rx: { type: "basic-shape", kind: "radius", value: { - type: "length", - value: 1, - unit: "px", + type: "length-percentage", + math: { + type: "math expression", + expression: { + type: "calculation", + arguments: [ + { + type: "sum", + operands: [ + { + type: "value", + value: { type: "length", value: 1, unit: "em" }, + }, + { + type: "value", + value: { type: "percentage", value: -0.1 }, + }, + ], + }, + ], + }, + }, }, }, ry: { @@ -27,23 +69,36 @@ test("parse() parses an ellipse", (t) => { kind: "radius", value: { type: "length", - value: 3, - unit: "px", + math: { + type: "math expression", + expression: { + type: "calculation", + arguments: [ + { + type: "sum", + operands: [ + { + type: "value", + value: { type: "length", value: 1, unit: "px" }, + }, + { + type: "value", + value: { type: "length", value: 1, unit: "ch" }, + }, + ], + }, + ], + }, + }, }, }, center: { type: "position", - vertical: { - type: "keyword", - value: "center", - }, + vertical: { type: "keyword", value: "center" }, horizontal: { type: "side", offset: null, - side: { - type: "keyword", - value: "right", - }, + side: { type: "keyword", value: "right" }, }, }, }); diff --git a/packages/alfa-css/test/value/shape/inset.spec.ts b/packages/alfa-css/test/value/shape/inset.spec.ts index 736c525adf..02d14aa64d 100644 --- a/packages/alfa-css/test/value/shape/inset.spec.ts +++ b/packages/alfa-css/test/value/shape/inset.spec.ts @@ -117,3 +117,53 @@ test("parse() parses a partially specified inset", (t) => { }, }); }); + +test("parse() accepts calculated offsets and corners", (t) => { + const actual = (length: number) => `calc(${length}px + 1%)`; + const expected = (length: number) => ({ + type: "length-percentage", + math: { + type: "math expression", + expression: { + type: "calculation", + arguments: [ + { + type: "sum", + operands: [ + { + type: "value", + value: { type: "length", value: length, unit: "px" }, + }, + { + type: "value", + value: { type: "percentage", value: 0.01 }, + }, + ], + }, + ], + }, + }, + }); + + t.deepEqual( + parse( + `inset(${actual(1)} ${actual(2)} ${actual(3)} ${actual(4)} ` + + `round ${actual(1)} ${actual(1)} ${actual(1)} ${actual(1)} ` + + `/ ${actual(2)} ${actual(2)} ${actual(2)} ${actual(2)})` + ).getUnsafe(), + { + type: "basic-shape", + kind: "inset", + offsets: [expected(1), expected(2), expected(3), expected(4)], + corners: { + type: "some", + value: [ + [expected(1), expected(2)], + [expected(1), expected(2)], + [expected(1), expected(2)], + [expected(1), expected(2)], + ], + }, + } + ); +}); diff --git a/packages/alfa-css/test/value/shape/polygon.spec.ts b/packages/alfa-css/test/value/shape/polygon.spec.ts index f1a30e95de..89672ddaf9 100644 --- a/packages/alfa-css/test/value/shape/polygon.spec.ts +++ b/packages/alfa-css/test/value/shape/polygon.spec.ts @@ -1,14 +1,13 @@ import { test } from "@siteimprove/alfa-test"; -import { Lexer } from "../../../src/syntax/lexer"; -import { Polygon } from "../../../src/value/shape/polygon"; +import { Lexer, Polygon } from "../../../src"; function parse(input: string) { - return Polygon.parse(Lexer.lex(input)).map(([_, circle]) => circle.toJSON()); + return Polygon.parse(Lexer.lex(input)).getUnsafe()[1].toJSON(); } test(".parse() parses a polygon with no fill rule", (t) => { - t.deepEqual(parse("polygon(1px 0px 1px 1px 0px 1px)").getUnsafe(), { + t.deepEqual(parse("polygon(1px 0px 1px 1px 0px 1px)"), { type: "basic-shape", kind: "polygon", fill: { @@ -32,7 +31,7 @@ test(".parse() parses a polygon with no fill rule", (t) => { }); test(".parse() parses a polygon with a fill rule", (t) => { - t.deepEqual(parse("polygon(evenodd, 1px 0px 1px 1px 0px 1px)").getUnsafe(), { + t.deepEqual(parse("polygon(evenodd, 1px 0px 1px 1px 0px 1px)"), { type: "basic-shape", kind: "polygon", fill: { @@ -57,5 +56,55 @@ test(".parse() parses a polygon with a fill rule", (t) => { }); test(".parse() fails when there is an odd number of coordinates", (t) => { - t.deepEqual(parse("polygon(1px 0px 1px 1px 0px)").isErr(), true); + t.deepEqual( + Polygon.parse(Lexer.lex("polygon(1px 0px 1px 1px 0px)")).isErr(), + true + ); +}); + +test(".parse() accepts calculated vertices", (t) => { + const actual = (length: number) => `calc(${length}px + 1%)`; + const expected = (length: number) => ({ + type: "length-percentage", + math: { + type: "math expression", + expression: { + type: "calculation", + arguments: [ + { + type: "sum", + operands: [ + { + type: "value", + value: { type: "length", value: length, unit: "px" }, + }, + { + type: "value", + value: { type: "percentage", value: 0.01 }, + }, + ], + }, + ], + }, + }, + }); + + t.deepEqual( + parse( + `polygon(${actual(1)} ${actual(0)} ${actual(1)} ${actual(1)}` + + ` ${actual(0)} ${actual(1)})` + ), + { + type: "basic-shape", + kind: "polygon", + fill: { + type: "none", + }, + vertices: [ + [expected(1), expected(0)], + [expected(1), expected(1)], + [expected(0), expected(1)], + ], + } + ); }); diff --git a/packages/alfa-css/test/value/shape/rectangle.spec.ts b/packages/alfa-css/test/value/shape/rectangle.spec.ts index 16a53f12d7..70e3b34502 100644 --- a/packages/alfa-css/test/value/shape/rectangle.spec.ts +++ b/packages/alfa-css/test/value/shape/rectangle.spec.ts @@ -1,93 +1,94 @@ import { test } from "@siteimprove/alfa-test"; -import { Lexer } from "../../../src/syntax/lexer"; -import { Rectangle } from "../../../src/value/shape/rectangle"; +import { Length, Lexer, Rectangle } from "../../../src"; function parse(input: string) { - return Rectangle.parse(Lexer.lex(input)).map(([_, rectangle]) => - rectangle.toJSON() - ); + return Rectangle.parse(Lexer.lex(input)).getUnsafe()[1].toJSON(); } test(".parse() parses comma separated rectangles", (t) => { - t.deepEqual(parse("rect(1px, auto, 2em, auto)").getUnsafe(), { + t.deepEqual(parse("rect(1px, auto, 2em, auto)"), { type: "basic-shape", kind: "rectangle", - bottom: { - type: "length", - unit: "em", - value: 2, - }, - left: { - type: "keyword", - value: "auto", - }, - right: { - type: "keyword", - value: "auto", - }, - top: { - type: "length", - unit: "px", - value: 1, - }, + bottom: { type: "length", unit: "em", value: 2 }, + left: { type: "keyword", value: "auto" }, + right: { type: "keyword", value: "auto" }, + top: { type: "length", unit: "px", value: 1 }, }); - t.deepEqual(parse("rect(1px , auto , 2em,auto)").getUnsafe(), { + t.deepEqual(parse("rect(1px , auto , 2em,auto)"), { type: "basic-shape", kind: "rectangle", - bottom: { - type: "length", - unit: "em", - value: 2, - }, - left: { - type: "keyword", - value: "auto", - }, - right: { - type: "keyword", - value: "auto", - }, - top: { - type: "length", - unit: "px", - value: 1, - }, + bottom: { type: "length", unit: "em", value: 2 }, + left: { type: "keyword", value: "auto" }, + right: { type: "keyword", value: "auto" }, + top: { type: "length", unit: "px", value: 1 }, }); }); test(".parse() parses space separated rectangles", (t) => { - t.deepEqual(parse("rect(1px auto 2em auto)").getUnsafe(), { + t.deepEqual(parse("rect(1px auto 2em auto)"), { type: "basic-shape", kind: "rectangle", - bottom: { - type: "length", - unit: "em", - value: 2, - }, - left: { - type: "keyword", - value: "auto", - }, - right: { - type: "keyword", - value: "auto", - }, - top: { - type: "length", - unit: "px", - value: 1, - }, + bottom: { type: "length", unit: "em", value: 2 }, + left: { type: "keyword", value: "auto" }, + right: { type: "keyword", value: "auto" }, + top: { type: "length", unit: "px", value: 1 }, }); }); test(".parse() fails if there are more or less than 4 values", (t) => { - t.deepEqual(parse("rect(1px 1px 1px").isErr(), true); + t.deepEqual(Rectangle.parse(Lexer.lex("rect(1px 1px 1px")).isErr(), true); - t.deepEqual(parse("rect(1px 1px 1px 1px 1px").isErr(), true); + t.deepEqual( + Rectangle.parse(Lexer.lex("rect(1px 1px 1px 1px 1px")).isErr(), + true + ); }); test(".parse() fails when mixing comma and space separators", (t) => { - t.deepEqual(parse("rect(1px 1px, 1px 1px").isErr(), true); + t.deepEqual( + Rectangle.parse(Lexer.lex("rect(1px 1px, 1px 1px")).isErr(), + true + ); +}); + +test(".parse() accepts calculated lengths", (t) => { + const actual = (length: number) => `calc(${length}px + 1em)`; + const expected: (length: number) => Length.JSON = (length: number) => ({ + type: "length", + math: { + type: "math expression", + expression: { + type: "calculation", + arguments: [ + { + type: "sum", + operands: [ + { + type: "value", + value: { type: "length", value: length, unit: "px" }, + }, + { + type: "value", + value: { type: "length", value: 1, unit: "em" }, + }, + ], + }, + ], + }, + }, + }); + + t.deepEqual( + parse(`rect(${actual(1)}, ${actual(2)}, ${actual(3)}, ${actual(4)})`), + { + type: "basic-shape", + kind: "rectangle", + top: expected(1), + right: expected(2), + bottom: expected(3), + left: expected(4), + } + ); }); From 18eef3132c061ec6405647213f99dab6d300c6f0 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 13 Oct 2023 14:37:32 +0200 Subject: [PATCH 08/18] Add changeset --- .changeset/gold-buttons-notice.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/gold-buttons-notice.md diff --git a/.changeset/gold-buttons-notice.md b/.changeset/gold-buttons-notice.md new file mode 100644 index 0000000000..74392fe7c9 --- /dev/null +++ b/.changeset/gold-buttons-notice.md @@ -0,0 +1,8 @@ +--- +"@siteimprove/alfa-style": minor +"@siteimprove/alfa-css": minor +--- + +**Added:** CSS `Shape` now accept calculated values + +Shapes that accept length-percentage are only partially resolved at compute time. From c0c5ae27f347645162e5995d1b8c3e9a82d0ed9b Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 13 Oct 2023 14:38:10 +0200 Subject: [PATCH 09/18] Extract API --- docs/review/api/alfa-css.api.md | 165 ++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 39 deletions(-) diff --git a/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index acc515cfcf..fca52a8840 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -261,7 +261,7 @@ export namespace Box { // Warning: (ae-forgotten-export) The symbol "BasicShape" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export class Circle extends BasicShape<"circle"> { +export class Circle extends BasicShape<"circle", Value.HasCalculation<[R, P]>> { // (undocumented) get center(): P; // (undocumented) @@ -271,11 +271,11 @@ export class Circle(radius: R, center: P): Circle; + static of(radius: R, center: P): Circle; // (undocumented) get radius(): R; // (undocumented) - resolve(): Circle; + resolve(resolver: Circle.Resolver): Circle.Canonical; // (undocumented) toJSON(): Circle.JSON; // (undocumented) @@ -284,6 +284,8 @@ export class Circle; // (undocumented) export function isCircle(value: unknown): value is Circle; // (undocumented) @@ -293,6 +295,14 @@ export namespace Circle { // (undocumented) radius: Radius.JSON; } + // (undocumented) + export function partiallyResolve(resolver: PartialResolver): (value: Circle) => PartiallyResolved; + // (undocumented) + export type PartiallyResolved = Circle; + // (undocumented) + export type PartialResolver = Radius.PartialResolver & Position.PartialResolver; + // (undocumented) + export type Resolver = Radius.Resolver & Position.Resolver; const // (undocumented) parse: Parser; } @@ -534,7 +544,7 @@ namespace Dimension_2 { } // @public (undocumented) -export class Ellipse extends BasicShape<"ellipse"> { +export class Ellipse extends BasicShape<"ellipse", Value.HasCalculation<[R, P]>> { // (undocumented) get center(): P; // (undocumented) @@ -544,9 +554,9 @@ export class Ellipse(rx: R, ry: R, center: P): Ellipse; + static of(rx: R, ry: R, center: P): Ellipse; // (undocumented) - resolve(): Ellipse; + resolve(resolver: Ellipse.Resolver): Ellipse.Canonical; // (undocumented) get rx(): R; // (undocumented) @@ -559,6 +569,8 @@ export class Ellipse; // (undocumented) export function isEllipse(value: unknown): value is Ellipse; // (undocumented) @@ -570,6 +582,14 @@ export namespace Ellipse { // (undocumented) ry: Radius.JSON; } + // (undocumented) + export function partiallyResolve(resolver: PartialResolver): (value: Ellipse) => PartiallyResolved; + // (undocumented) + export type PartiallyResolved = Ellipse; + // (undocumented) + export type PartialResolver = Radius.PartialResolver & Position.PartialResolver; + // (undocumented) + export type Resolver = Radius.Resolver & Position.Resolver; const // (undocumented) parse: Parser; } @@ -772,8 +792,11 @@ export namespace Image { parse: Parser; } +// Warning: (ae-forgotten-export) The symbol "Corner" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "HasCalculation" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export class Inset extends BasicShape<"inset"> { +export class Inset extends BasicShape<"inset", HasCalculation> { // (undocumented) get bottom(): O; // (undocumented) @@ -791,11 +814,11 @@ export class Inset(offsets: readonly [O, O, O, O], corners: Option): Inset; + static of(offsets: readonly [O, O, O, O], corners: Option): Inset; // (undocumented) get offsets(): readonly [O, O, O, O]; // (undocumented) - resolve(): Inset; + resolve(resolver: Inset.Resolver): Inset.Canonical; // (undocumented) get right(): O; // (undocumented) @@ -813,18 +836,26 @@ export class Inset; + // (undocumented) + export function isInset(value: unknown): value is Inset; // (undocumented) - export interface JSON extends BasicShape.JSON<"inset"> { + export interface JSON extends BasicShape.JSON<"inset"> { // (undocumented) corners: Option.JSON; // (undocumented) offsets: Serializable.ToJSON; } // (undocumented) - export type Offset = Length.Fixed | Percentage.Fixed; + export type Offset = LengthPercentage; + // (undocumented) + export function partiallyResolve(resolver: PartialResolver): (value: Inset) => PartiallyResolved; // (undocumented) - export type Radius = Length.Fixed | Percentage.Fixed; + export type PartiallyResolved = Inset; + // (undocumented) + export type PartialResolver = LengthPercentage.PartialResolver; + // (undocumented) + export type Resolver = LengthPercentage.Resolver; const // (undocumented) parse: Parser; } @@ -1801,7 +1832,7 @@ export namespace Perspective { } // @public (undocumented) -export class Polygon extends BasicShape<"polygon"> { +export class Polygon extends BasicShape<"polygon", Value.HasCalculation<[V]>> { // (undocumented) equals(value: Polygon): boolean; // (undocumented) @@ -1811,9 +1842,9 @@ export class Polygon(fill: Option, vertices: Iterable_2>): Polygon; + static of(fill: Option, vertices: Iterable_2>): Polygon; // (undocumented) - resolve(): Polygon; + resolve(resolver: Polygon.Resolver): Polygon.Canonical; // (undocumented) toJSON(): Polygon.JSON; // (undocumented) @@ -1824,23 +1855,38 @@ export class Polygon; // (undocumented) export type Fill = Keyword<"nonzero"> | Keyword<"evenodd">; // (undocumented) - export interface JSON extends BasicShape.JSON<"polygon"> { + export function isPolygon(value: unknown): value is Polygon; + // (undocumented) + export interface JSON extends BasicShape.JSON<"polygon"> { // (undocumented) fill: Option.JSON; // (undocumented) vertices: Array_2>>; } // (undocumented) - export type Vertex = readonly [V, V]; + export function partiallyResolve(resolver: PartialResolver): (value: Polygon) => PartiallyResolved; + // (undocumented) + export type PartiallyResolved = Polygon; + // (undocumented) + export type PartialResolver = LengthPercentage.PartialResolver; + // (undocumented) + export type Resolver = LengthPercentage.Resolver; + // (undocumented) + export type Vertex = readonly [ + V, + V + ]; const // (undocumented) parse: Parser; } // @public (undocumented) -export class Position = Position.Component, VC extends Position.Component = Position.Component, CALC extends boolean = boolean> extends Value<"position", CALC> implements Resolvable, Position.Resolver> { +export class Position = Position.Component, VC extends Position.Component = Position.Component> extends Value<"position", Value.HasCalculation<[HC, VC]>> implements Resolvable, Position.Resolver> { // (undocumented) equals(value: unknown): value is this; // (undocumented) @@ -1848,7 +1894,7 @@ export class Position = Position.Component, VC extends Position.Component = Position.Component>(horizontal: HC, vertical: VC): Position>; + static of = Position.Component, VC extends Position.Component = Position.Component>(horizontal: HC, vertical: VC): Position; // (undocumented) resolve(resolver: Position.Resolver): Position.Canonical; // (undocumented) @@ -1865,9 +1911,7 @@ export namespace Position { // Warning: (ae-forgotten-export) The symbol "Component" needs to be exported by the entry point index.d.ts // // (undocumented) - export type Canonical = Position, Component_2.Canonical, false>; - // @internal (undocumented) - export type Fixed = Position, Component_2.Fixed, false>; + export type Canonical = Position, Component_2.Canonical>; // (undocumented) export interface JSON extends Value.JSON<"position"> { // (undocumented) @@ -1879,8 +1923,6 @@ export namespace Position { import Keywords = keywords.Keywords; import Side = side.Side; import Component = component.Component; - // @internal (undocumented) - export function parseBase(legacySyntax?: boolean): Parser; // (undocumented) export function partiallyResolve(resolver: PartialResolver): (value: Position) => PartiallyResolved; // (undocumented) @@ -1896,7 +1938,7 @@ export namespace Position { } // @public (undocumented) -export class Radius extends BasicShape<"radius"> { +export class Radius extends BasicShape<"radius", Value.HasCalculation<[R]>> { // (undocumented) equals(value: Radius): boolean; // (undocumented) @@ -1904,9 +1946,9 @@ export class Radius(value: R): Radius; + static of(value: R): Radius; // (undocumented) - resolve(): Radius; + resolve(resolver: Radius.Resolver): Radius.Canonical; // (undocumented) toJSON(): Radius.JSON; // (undocumented) @@ -1917,14 +1959,24 @@ export class Radius; // (undocumented) export function isRadius(value: unknown): value is Radius; // (undocumented) export interface JSON extends BasicShape.JSON<"radius"> { // (undocumented) - value: Length.Fixed.JSON | Percentage.Fixed.JSON | Keyword.JSON; + value: LengthPercentage.JSON | Keyword.JSON; } // (undocumented) + export function PartiallyResolve(resolver: PartialResolver): (value: Radius) => PartiallyResolved; + // (undocumented) + export type PartiallyResolved = Radius; + // (undocumented) + export type PartialResolver = LengthPercentage.PartialResolver; + // (undocumented) + export type Resolver = LengthPercentage.Resolver; + // (undocumented) export type Side = Side.Closest | Side.Farthest; // (undocumented) export namespace Side { @@ -1938,7 +1990,7 @@ export namespace Radius { } // @public @deprecated (undocumented) -export class Rectangle extends BasicShape<"rectangle"> { +export class Rectangle extends BasicShape<"rectangle", Value.HasCalculation<[O, O, O, O]>> { // (undocumented) get bottom(): O; // (undocumented) @@ -1954,9 +2006,9 @@ export class Rectangle(top: O, right: O, bottom: O, left: O): Rectangle; + static of(top: O, right: O, bottom: O, left: O): Rectangle; // (undocumented) - resolve(): Rectangle; + resolve(resolver: Rectangle.Resolver): Rectangle.Canonical; // (undocumented) get right(): O; // (undocumented) @@ -1976,18 +2028,22 @@ export namespace Rectangle { // (undocumented) export type Auto = Keyword<"auto">; // (undocumented) + export type Canonical = Rectangle; + // (undocumented) export function isRectangle(value: unknown): value is Rectangle; // (undocumented) export interface JSON extends BasicShape.JSON<"rectangle"> { // (undocumented) - bottom: Length.Fixed.JSON | Keyword.JSON; + bottom: Length.JSON | Keyword.JSON; // (undocumented) - left: Length.Fixed.JSON | Keyword.JSON; + left: Length.JSON | Keyword.JSON; // (undocumented) - right: Length.Fixed.JSON | Keyword.JSON; + right: Length.JSON | Keyword.JSON; // (undocumented) - top: Length.Fixed.JSON | Keyword.JSON; + top: Length.JSON | Keyword.JSON; } + // (undocumented) + export type Resolver = Length.Resolver; const // (undocumented) parse: Parser; } @@ -2188,7 +2244,7 @@ export namespace Shadow { } // @public (undocumented) -export class Shape extends Value<"shape", false> { +export class Shape extends Value<"shape", Value.HasCalculation<[S]>> { // (undocumented) get box(): B; // (undocumented) @@ -2200,7 +2256,7 @@ export class Shape(shape: S, box: B): Shape; // (undocumented) - resolve(): Shape; + resolve(resolver: Shape.Resolver): Shape.Canonical; // (undocumented) get shape(): S; // (undocumented) @@ -2214,14 +2270,45 @@ export namespace Shape { // (undocumented) export type Basic = Circle | Ellipse | Inset | Polygon | Rectangle; // (undocumented) + export namespace Basic { + // (undocumented) + export type Canonical = Circle.Canonical | Ellipse.Canonical | Inset.Canonical | Polygon.Canonical | Rectangle.Canonical; + // (undocumented) + export type JSON = Circle.JSON | Ellipse.JSON | Inset.JSON | Polygon.JSON | Rectangle.JSON; + // (undocumented) + export function partiallyResolve(resolver: PartialResolver): (value: Basic) => PartiallyResolved; + // (undocumented) + export type PartiallyResolved = Circle.PartiallyResolved | Ellipse.PartiallyResolved | Inset.PartiallyResolved | Polygon.PartiallyResolved | Rectangle.Canonical; + // (undocumented) + export type PartialResolver = Circle.PartialResolver & Ellipse.PartialResolver & Inset.PartialResolver & Polygon.PartialResolver & Rectangle.Resolver; + // (undocumented) + export type Resolver = Circle.Resolver & Ellipse.Resolver & Inset.Resolver & Polygon.Resolver & Rectangle.Resolver; + const // Warning: (ae-forgotten-export) The symbol "Keywords" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Keywords" needs to be exported by the entry point index.d.ts + // + // @internal (undocumented) + parse: Parser_2, Inset | Circle, Position, Component_2>> | Ellipse, Position, Component_2>> | Polygon, string, []>; + } + // (undocumented) + export type Canonical = Shape; + // (undocumented) export interface JSON extends Value.JSON<"shape"> { // (undocumented) box: Box.Geometry.JSON; // (undocumented) - shape: Circle.JSON | Ellipse.JSON | Inset.JSON | Polygon.JSON | Rectangle.JSON; + shape: Basic.JSON; } + // (undocumented) + export function partiallyResolve(resolver: PartialResolver): (value: Shape) => PartiallyResolved; + // (undocumented) + export type PartiallyResolved = Shape; + // (undocumented) + export type PartialResolver = Basic.PartialResolver; + // (undocumented) + export type Resolver = Basic.Resolver; const // (undocumented) parse: Parser>; + {}; } // @public (undocumented) From 0f70c20518ddac7906f5127b730e538e7afa28ae Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 16 Oct 2023 10:48:09 +0200 Subject: [PATCH 10/18] Clean up --- packages/alfa-css/src/value/shape/inset.ts | 3 +-- packages/alfa-css/src/value/shape/rectangle.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/alfa-css/src/value/shape/inset.ts b/packages/alfa-css/src/value/shape/inset.ts index 7f89191d46..f2932af4ed 100644 --- a/packages/alfa-css/src/value/shape/inset.ts +++ b/packages/alfa-css/src/value/shape/inset.ts @@ -13,8 +13,7 @@ import { Value } from "../value"; import { BasicShape } from "./basic-shape"; import { Corner } from "./corner"; -const { delimited, either, map, filter, option, pair, right, separatedList } = - Parser; +const { delimited, map, filter, option, pair, right, separatedList } = Parser; const { parseDelim, parseWhitespace } = Token; /** diff --git a/packages/alfa-css/src/value/shape/rectangle.ts b/packages/alfa-css/src/value/shape/rectangle.ts index 706186c531..e0b3108521 100644 --- a/packages/alfa-css/src/value/shape/rectangle.ts +++ b/packages/alfa-css/src/value/shape/rectangle.ts @@ -9,8 +9,7 @@ import { Value } from "../value"; import { BasicShape } from "./basic-shape"; -const { either, map, option, pair, take, right, delimited, separatedList } = - Parser; +const { either, map, option, separatedList } = Parser; /** * {@link https://drafts.fxtf.org/css-masking/#funcdef-clip-rect} From c9e57d814451c5d2b4def77bb1a29a1ff1522b96 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 08:53:00 +0000 Subject: [PATCH 11/18] Extract API --- docs/review/api/alfa-css.api.md | 2 +- docs/review/api/alfa-style.api.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index fca52a8840..37c55d21b4 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -2287,7 +2287,7 @@ export namespace Shape { // Warning: (ae-forgotten-export) The symbol "Keywords" needs to be exported by the entry point index.d.ts // // @internal (undocumented) - parse: Parser_2, Inset | Circle, Position, Component_2>> | Ellipse, Position, Component_2>> | Polygon, string, []>; + parse: Parser_2, Circle, Position, Component_2>> | Ellipse, Position, Component_2>> | Inset | Polygon, string, []>; } // (undocumented) export type Canonical = Shape; diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index ef206b5e36..3b09495d22 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -227,9 +227,9 @@ export namespace Longhands { readonly bottom: Longhand, LengthPercentage | Keyword<"auto">>; readonly "box-shadow": Longhand | List, boolean>, Keyword<"none"> | List>; readonly "clip-path": Longhand | Shape, URL | Keyword<"none"> | Shape>; - readonly clip: Longhand | Shape | Rectangle.Auto>, Keyword<"border-box">>, Keyword<"auto"> | Shape | Rectangle.Auto>, Keyword<"border-box">>>; + readonly clip: Longhand | Shape, Keyword<"border-box">>, Keyword<"auto"> | Shape, Keyword<"border-box">>>; readonly color: Longhand; - readonly cursor: Longhand, boolean>, Keyword<"none"> | Keyword<"auto"> | Keyword<"default"> | Keyword<"context-menu"> | Keyword<"help"> | Keyword<"pointer"> | Keyword<"progress"> | Keyword<"wait"> | Keyword<"cell"> | Keyword<"crosshair"> | Keyword<"text"> | Keyword<"vertical-text"> | Keyword<"alias"> | Keyword<"copy"> | Keyword<"move"> | Keyword<"no-drop"> | Keyword<"not-allowed"> | Keyword<"grab"> | Keyword<"grabbing"> | Keyword<"e-resize"> | Keyword<"n-resize"> | Keyword<"ne-resize"> | Keyword<"nw-resize"> | Keyword<"s-resize"> | Keyword<"se-resize"> | Keyword<"sw-resize"> | Keyword<"w-resize"> | Keyword<"ew-resize"> | Keyword<"ns-resize"> | Keyword<"nesw-resize"> | Keyword<"nwse-resize"> | Keyword<"col-resize"> | Keyword<"row-resize"> | Keyword<"all-scroll"> | Keyword<"zoom-in"> | Keyword<"zoom-out">], boolean>, Tuple<[List, boolean>, Keyword<"none"> | Keyword<"auto"> | Keyword<"default"> | Keyword<"context-menu"> | Keyword<"help"> | Keyword<"pointer"> | Keyword<"progress"> | Keyword<"wait"> | Keyword<"cell"> | Keyword<"crosshair"> | Keyword<"text"> | Keyword<"vertical-text"> | Keyword<"alias"> | Keyword<"copy"> | Keyword<"move"> | Keyword<"no-drop"> | Keyword<"not-allowed"> | Keyword<"grab"> | Keyword<"grabbing"> | Keyword<"e-resize"> | Keyword<"n-resize"> | Keyword<"ne-resize"> | Keyword<"nw-resize"> | Keyword<"s-resize"> | Keyword<"se-resize"> | Keyword<"sw-resize"> | Keyword<"w-resize"> | Keyword<"ew-resize"> | Keyword<"ns-resize"> | Keyword<"nesw-resize"> | Keyword<"nwse-resize"> | Keyword<"col-resize"> | Keyword<"row-resize"> | Keyword<"all-scroll"> | Keyword<"zoom-in"> | Keyword<"zoom-out">], boolean>>; + readonly cursor: Longhand, boolean>, Keyword<"auto"> | Keyword<"none"> | Keyword<"default"> | Keyword<"context-menu"> | Keyword<"help"> | Keyword<"pointer"> | Keyword<"progress"> | Keyword<"wait"> | Keyword<"cell"> | Keyword<"crosshair"> | Keyword<"text"> | Keyword<"vertical-text"> | Keyword<"alias"> | Keyword<"copy"> | Keyword<"move"> | Keyword<"no-drop"> | Keyword<"not-allowed"> | Keyword<"grab"> | Keyword<"grabbing"> | Keyword<"e-resize"> | Keyword<"n-resize"> | Keyword<"ne-resize"> | Keyword<"nw-resize"> | Keyword<"s-resize"> | Keyword<"se-resize"> | Keyword<"sw-resize"> | Keyword<"w-resize"> | Keyword<"ew-resize"> | Keyword<"ns-resize"> | Keyword<"nesw-resize"> | Keyword<"nwse-resize"> | Keyword<"col-resize"> | Keyword<"row-resize"> | Keyword<"all-scroll"> | Keyword<"zoom-in"> | Keyword<"zoom-out">], boolean>, Tuple<[List, boolean>, Keyword<"auto"> | Keyword<"none"> | Keyword<"default"> | Keyword<"context-menu"> | Keyword<"help"> | Keyword<"pointer"> | Keyword<"progress"> | Keyword<"wait"> | Keyword<"cell"> | Keyword<"crosshair"> | Keyword<"text"> | Keyword<"vertical-text"> | Keyword<"alias"> | Keyword<"copy"> | Keyword<"move"> | Keyword<"no-drop"> | Keyword<"not-allowed"> | Keyword<"grab"> | Keyword<"grabbing"> | Keyword<"e-resize"> | Keyword<"n-resize"> | Keyword<"ne-resize"> | Keyword<"nw-resize"> | Keyword<"s-resize"> | Keyword<"se-resize"> | Keyword<"sw-resize"> | Keyword<"w-resize"> | Keyword<"ew-resize"> | Keyword<"ns-resize"> | Keyword<"nesw-resize"> | Keyword<"nwse-resize"> | Keyword<"col-resize"> | Keyword<"row-resize"> | Keyword<"all-scroll"> | Keyword<"zoom-in"> | Keyword<"zoom-out">], boolean>>; readonly display: Longhand | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">], boolean> | Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">, listitem: Keyword<"list-item">], boolean> | Tuple<[outside: Keyword<"table-row-group"> | Keyword<"table-header-group"> | Keyword<"table-footer-group"> | Keyword<"table-row"> | Keyword<"table-cell"> | Keyword<"table-column-group"> | Keyword<"table-column"> | Keyword<"table-caption"> | Keyword<"ruby-base"> | Keyword<"ruby-text"> | Keyword<"ruby-base-container"> | Keyword<"ruby-text-container">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">], boolean> | Tuple<[Keyword<"none"> | Keyword<"contents">], boolean>, Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">], boolean> | Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">, listitem: Keyword<"list-item">], boolean> | Tuple<[outside: Keyword<"table-row-group"> | Keyword<"table-header-group"> | Keyword<"table-footer-group"> | Keyword<"table-row"> | Keyword<"table-cell"> | Keyword<"table-column-group"> | Keyword<"table-column"> | Keyword<"table-caption"> | Keyword<"ruby-base"> | Keyword<"ruby-text"> | Keyword<"ruby-base-container"> | Keyword<"ruby-text-container">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">], boolean> | Tuple<[Keyword<"none"> | Keyword<"contents">], boolean>>; readonly "flex-direction": Longhand, Keyword.ToKeywords<"row" | "row-reverse" | "column" | "column-reverse">>; readonly "flex-wrap": Longhand, Keyword.ToKeywords<"nowrap" | "wrap" | "wrap-reverse">>; @@ -263,8 +263,8 @@ export namespace Longhands { readonly "outline-offset": Longhand, Length>; readonly "outline-style": Longhand, Keyword.ToKeywords<"none" | "inset" | "auto" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "outline-width": Longhand | Keyword<"medium"> | Keyword<"thick">, Length>; - readonly "overflow-x": Longhand, Keyword<"scroll"> | Keyword<"auto"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; - readonly "overflow-y": Longhand, Keyword<"scroll"> | Keyword<"auto"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; + readonly "overflow-x": Longhand, Keyword<"auto"> | Keyword<"scroll"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; + readonly "overflow-y": Longhand, Keyword<"auto"> | Keyword<"scroll"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; readonly position: Longhand, Keyword.ToKeywords<"fixed" | "relative" | "static" | "absolute" | "sticky">>; readonly right: Longhand, LengthPercentage | Keyword<"auto">>; readonly rotate: Longhand, Rotate | Keyword<"none">>; From 6fe7e576af58f60f815b367af8e11c5dc92dc509 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 16 Oct 2023 10:51:02 +0200 Subject: [PATCH 12/18] Clean up --- packages/alfa-css/src/value/shape/inset.ts | 2 +- packages/alfa-css/src/value/shape/polygon.ts | 3 +-- packages/alfa-css/src/value/shape/radius.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/alfa-css/src/value/shape/inset.ts b/packages/alfa-css/src/value/shape/inset.ts index f2932af4ed..7d8dd886c4 100644 --- a/packages/alfa-css/src/value/shape/inset.ts +++ b/packages/alfa-css/src/value/shape/inset.ts @@ -13,7 +13,7 @@ import { Value } from "../value"; import { BasicShape } from "./basic-shape"; import { Corner } from "./corner"; -const { delimited, map, filter, option, pair, right, separatedList } = Parser; +const { delimited, filter, map, option, pair, right, separatedList } = Parser; const { parseDelim, parseWhitespace } = Token; /** diff --git a/packages/alfa-css/src/value/shape/polygon.ts b/packages/alfa-css/src/value/shape/polygon.ts index 4dfcf84b2e..0d8c9779f0 100644 --- a/packages/alfa-css/src/value/shape/polygon.ts +++ b/packages/alfa-css/src/value/shape/polygon.ts @@ -13,8 +13,7 @@ import { Value } from "../value"; import { BasicShape } from "./basic-shape"; -const { either, left, map, option, pair, right, separated, separatedList } = - Parser; +const { left, map, option, pair, right, separated, separatedList } = Parser; const { parseComma, parseWhitespace } = Token; /** diff --git a/packages/alfa-css/src/value/shape/radius.ts b/packages/alfa-css/src/value/shape/radius.ts index a9c95cc31c..ebe763a872 100644 --- a/packages/alfa-css/src/value/shape/radius.ts +++ b/packages/alfa-css/src/value/shape/radius.ts @@ -10,7 +10,7 @@ import { Value } from "../value"; import { BasicShape } from "./basic-shape"; -const { either, map, filter } = Parser; +const { either, filter, map } = Parser; /** * {@link https://drafts.csswg.org/css-shapes/#typedef-shape-radius} From f2d1e1aa631ac80cf12bbdc764827d52af3af4ec Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 16 Oct 2023 11:04:03 +0200 Subject: [PATCH 13/18] Typos --- .changeset/shy-schools-flash.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/shy-schools-flash.md b/.changeset/shy-schools-flash.md index 9a40209bcb..9b29754ef0 100644 --- a/.changeset/shy-schools-flash.md +++ b/.changeset/shy-schools-flash.md @@ -2,8 +2,8 @@ "@siteimprove/alfa-css": minor --- -**Changed:** The `Position` type requires more type paramters. +**Changed:** The `Position` type requires more type parameters. -Instead of just accepting the horizontal and vertical components, the type now also requires the horizontal and vertical keywords list (as first and second paramter). The components parameter default to `Position.Component` (reps. `V`) for keywords `H` (resp. `V`). +Instead of just accepting the horizontal and vertical components, the type now also requires the horizontal and vertical keywords list (as first and second parameter). The components parameter default to `Position.Component` (reps. `V`) for keywords `H` (resp. `V`). -The type also accepts a `CALC` paramter indicating whether it may have calculations. +The type also accepts a `CALC` parameter indicating whether it may have calculations. From f880e2c0d5d09b3ba12e7f6ebdc4c2f1de70724e Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 16 Oct 2023 11:10:36 +0200 Subject: [PATCH 14/18] Clean temporarily LengthPercentage hack --- .changeset/shy-schools-flash.md | 2 -- .changeset/thick-ways-clap.md | 5 ---- .../src/value/numeric/length-percentage.ts | 25 +++++-------------- 3 files changed, 6 insertions(+), 26 deletions(-) delete mode 100644 .changeset/thick-ways-clap.md diff --git a/.changeset/shy-schools-flash.md b/.changeset/shy-schools-flash.md index 9b29754ef0..6cef9b5e69 100644 --- a/.changeset/shy-schools-flash.md +++ b/.changeset/shy-schools-flash.md @@ -5,5 +5,3 @@ **Changed:** The `Position` type requires more type parameters. Instead of just accepting the horizontal and vertical components, the type now also requires the horizontal and vertical keywords list (as first and second parameter). The components parameter default to `Position.Component` (reps. `V`) for keywords `H` (resp. `V`). - -The type also accepts a `CALC` parameter indicating whether it may have calculations. diff --git a/.changeset/thick-ways-clap.md b/.changeset/thick-ways-clap.md deleted file mode 100644 index 8e40b2dc61..0000000000 --- a/.changeset/thick-ways-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@siteimprove/alfa-css": patch ---- - -**Added:** The `LengthPercentage` type now accepts an optional `CALC` boolean parameter to indicate whether it contains calculations. diff --git a/packages/alfa-css/src/value/numeric/length-percentage.ts b/packages/alfa-css/src/value/numeric/length-percentage.ts index a45d538cbd..dfa0120894 100644 --- a/packages/alfa-css/src/value/numeric/length-percentage.ts +++ b/packages/alfa-css/src/value/numeric/length-percentage.ts @@ -19,19 +19,12 @@ const { either, map } = Parser; /** * @public */ -export type LengthPercentage< - U extends Unit.Length = Unit.Length, - CALC extends boolean = boolean -> = CALC extends true - ? LengthPercentage.Calculated | Length.Calculated | Percentage.Calculated - : CALC extends false - ? Length.Fixed | Percentage.Fixed - : - | LengthPercentage.Calculated - | Length.Calculated - | Percentage.Calculated - | Length.Fixed - | Percentage.Fixed; +export type LengthPercentage = + | LengthPercentage.Calculated + | Length.Calculated + | Percentage.Calculated + | Length.Fixed + | Percentage.Fixed; /** * @public @@ -257,10 +250,4 @@ export namespace LengthPercentage { of ) ); - - /** - * @internal - */ - export const parseBase: CSSParser> = - either(Length.parseBase, Percentage.parseBase); } From 21b32504533261c15cd6a70fdaa5c64b1619c31b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 09:16:14 +0000 Subject: [PATCH 15/18] Extract API --- docs/review/api/alfa-css.api.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index 37c55d21b4..d4f82e7859 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -1110,7 +1110,7 @@ namespace Length_2 { } // @public (undocumented) -export type LengthPercentage = CALC extends true ? LengthPercentage.Calculated | Length.Calculated | Percentage.Calculated : CALC extends false ? Length.Fixed | Percentage.Fixed : LengthPercentage.Calculated | Length.Calculated | Percentage.Calculated | Length.Fixed | Percentage.Fixed; +export type LengthPercentage = LengthPercentage.Calculated | Length.Calculated | Percentage.Calculated | Length.Fixed | Percentage.Fixed; // @public (undocumented) export namespace LengthPercentage { @@ -1171,9 +1171,7 @@ export namespace LengthPercentage { // (undocumented) export type Resolver = Length.Resolver & Percentage.Resolver<"length", Canonical>; const // (undocumented) - parse: Parser_2, Length.Calculated | Length.Fixed | Calculated | Percentage.Calculated | Percentage.Fixed, string, []>; - const // @internal (undocumented) - parseBase: Parser>; + parse: Parser_2, LengthPercentage, string, []>; {}; } From 4966c799c5837956a613fee6819158c6a6b9be24 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 16 Oct 2023 11:35:25 +0200 Subject: [PATCH 16/18] Clean up --- packages/alfa-style/src/resolver.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/alfa-style/src/resolver.ts b/packages/alfa-style/src/resolver.ts index f9018d7d96..7477401b1c 100644 --- a/packages/alfa-style/src/resolver.ts +++ b/packages/alfa-style/src/resolver.ts @@ -1,13 +1,4 @@ -import { - Gradient, - Image, - Length, - LengthPercentage, - Position, - Unit, - URL, -} from "@siteimprove/alfa-css"; -import { Iterable } from "@siteimprove/alfa-iterable"; +import { Length, LengthPercentage, Unit } from "@siteimprove/alfa-css"; import { Mapper } from "@siteimprove/alfa-mapper"; import { Style } from "./style"; From 0ba57e33d4b5d9f12672b07f792c6ba8a73c33fa Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 16 Oct 2023 11:36:19 +0200 Subject: [PATCH 17/18] Clean up --- packages/alfa-css/src/value/image/gradient/linear/side.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alfa-css/src/value/image/gradient/linear/side.ts b/packages/alfa-css/src/value/image/gradient/linear/side.ts index 1f21b10cde..7e99e8f90e 100644 --- a/packages/alfa-css/src/value/image/gradient/linear/side.ts +++ b/packages/alfa-css/src/value/image/gradient/linear/side.ts @@ -7,7 +7,7 @@ import { Value } from "../../../value"; import { Position } from "./position"; -const { map, either, option, right } = Parser; +const { map, option, right } = Parser; /** * @internal From cba5c73229a4be2c88a0a53b0f60081ca62487e4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:30:07 +0000 Subject: [PATCH 18/18] Extract API --- docs/review/api/alfa-css.api.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index d81eb28d56..71bcfcbff5 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -734,7 +734,7 @@ export namespace Image { parse: Parser; } -// Warning: (ae-forgotten-export) The symbol "Corner" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Corner_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "HasCalculation" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -998,9 +998,7 @@ export namespace Length { } // @public (undocumented) - -export type LengthPercentage = CALC extends true ? LengthPercentage.Calculated | Length.Calculated | Percentage.Calculated : CALC extends false ? Length.Fixed | Percentage.Fixed : LengthPercentage.Calculated | Length.Calculated | Percentage.Calculated | Length.Fixed | Percentage.Fixed; - +export type LengthPercentage = LengthPercentage.Calculated | Length.Calculated | Percentage.Calculated | Length.Fixed | Percentage.Fixed; // @public (undocumented) export namespace LengthPercentage {