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. diff --git a/.changeset/shy-schools-flash.md b/.changeset/shy-schools-flash.md index 9a40209bcb..6cef9b5e69 100644 --- a/.changeset/shy-schools-flash.md +++ b/.changeset/shy-schools-flash.md @@ -2,8 +2,6 @@ "@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`). - -The type also accepts a `CALC` paramter indicating whether it may have calculations. +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`). 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/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index 739d284f60..71bcfcbff5 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -232,7 +232,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) @@ -242,11 +242,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) @@ -255,6 +255,8 @@ export class Circle; // (undocumented) export function isCircle(value: unknown): value is Circle; // (undocumented) @@ -264,6 +266,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; } @@ -476,7 +486,7 @@ export namespace Dimension { } // @public (undocumented) -export class Ellipse extends BasicShape<"ellipse"> { +export class Ellipse extends BasicShape<"ellipse", Value.HasCalculation<[R, P]>> { // (undocumented) get center(): P; // (undocumented) @@ -486,9 +496,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) @@ -501,6 +511,8 @@ export class Ellipse; // (undocumented) export function isEllipse(value: unknown): value is Ellipse; // (undocumented) @@ -512,6 +524,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; } @@ -714,8 +734,11 @@ export namespace Image { parse: Parser; } +// 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) -export class Inset extends BasicShape<"inset"> { +export class Inset extends BasicShape<"inset", HasCalculation> { // (undocumented) get bottom(): O; // (undocumented) @@ -733,11 +756,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) @@ -755,18 +778,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; } @@ -967,7 +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 { @@ -1028,9 +1059,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, []>; {}; } @@ -1594,7 +1623,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) @@ -1604,9 +1633,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) @@ -1617,23 +1646,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) @@ -1641,7 +1685,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) @@ -1658,9 +1702,7 @@ export namespace Position { // Warning: (ae-forgotten-export) The symbol "Component_2" 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) @@ -1672,8 +1714,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) @@ -1689,7 +1729,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) @@ -1697,9 +1737,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) @@ -1710,14 +1750,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 { @@ -1731,7 +1781,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) @@ -1747,9 +1797,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) @@ -1769,18 +1819,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; } @@ -1981,7 +2035,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) @@ -1993,7 +2047,7 @@ export class Shape(shape: S, box: B): Shape; // (undocumented) - resolve(): Shape; + resolve(resolver: Shape.Resolver): Shape.Canonical; // (undocumented) get shape(): S; // (undocumented) @@ -2007,14 +2061,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, Circle, Position, Component_2>> | Ellipse, Position, Component_2>> | Inset | 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) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 17ff5922ad..1d4bfca929 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -161,9 +161,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">>; @@ -197,8 +197,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">>; 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 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/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); } 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-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..bba4a454fc 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 partiallyResolve( + 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/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/ellipse.ts b/packages/alfa-css/src/value/shape/ellipse.ts index 6e275eb9b7..8c27576169 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 partiallyResolve( + 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..7d8dd886c4 100644 --- a/packages/alfa-css/src/value/shape/inset.ts +++ b/packages/alfa-css/src/value/shape/inset.ts @@ -7,11 +7,13 @@ 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 { either, map, filter, option, pair, right, takeAtMost } = Parser; +const { delimited, filter, map, option, pair, right, separatedList } = Parser; const { parseDelim, parseWhitespace } = Token; /** @@ -21,11 +23,11 @@ const { parseDelim, parseWhitespace } = Token; */ export class Inset< O extends Inset.Offset = Inset.Offset, - C extends Inset.Corner = Inset.Corner -> extends BasicShape<"inset"> { + 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 @@ -40,7 +42,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; } @@ -85,8 +93,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; @@ -156,11 +181,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"> { @@ -168,57 +189,88 @@ 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 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; + } const parseOffsets = map( - pair( - parseLengthPercentage, - takeAtMost(right(option(Token.parseWhitespace), parseLengthPercentage), 3) - ), - ([top, [right = top, bottom = top, left = right]]) => + 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" ); 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 ); - const parseCorners = map( - pair( - parseRadii, - option( - right( - option(parseWhitespace), - right(parseDelim("/"), right(option(parseWhitespace), 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( @@ -236,6 +288,22 @@ export namespace Inset { ) ) ), - ([_, [offsets, corners]]) => Inset.of(offsets, corners) + ([_, [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/src/value/shape/polygon.ts b/packages/alfa-css/src/value/shape/polygon.ts index 3196518ce0..0d8c9779f0 100644 --- a/packages/alfa-css/src/value/shape/polygon.ts +++ b/packages/alfa-css/src/value/shape/polygon.ts @@ -8,12 +8,12 @@ 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"; -const { either, left, map, option, pair, right, separated, separatedList } = - Parser; +const { left, map, option, pair, right, separated, separatedList } = Parser; const { parseComma, parseWhitespace } = Token; /** @@ -23,11 +23,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"> { + 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 +36,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 +55,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 +105,56 @@ 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 + ) + ); + } + + export function isPolygon(value: unknown): value is Polygon { + return value instanceof Polygon; + } const parseVertex = separated( - parseLengthPercentage, + LengthPercentage.parse, parseWhitespace, - parseLengthPercentage + LengthPercentage.parse ); export const parse: CSSParser = map( diff --git a/packages/alfa-css/src/value/shape/radius.ts b/packages/alfa-css/src/value/shape/radius.ts index b0a7487b4e..ebe763a872 100644 --- a/packages/alfa-css/src/value/shape/radius.ts +++ b/packages/alfa-css/src/value/shape/radius.ts @@ -1,14 +1,16 @@ 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"; -const { either, map, filter } = Parser; +const { either, filter, map } = Parser; /** * {@link https://drafts.csswg.org/css-shapes/#typedef-shape-radius} @@ -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..e0b3108521 100644 --- a/packages/alfa-css/src/value/shape/rectangle.ts +++ b/packages/alfa-css/src/value/shape/rectangle.ts @@ -1,14 +1,15 @@ 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, separatedList } = Parser; /** * {@link https://drafts.fxtf.org/css-masking/#funcdef-clip-rect} @@ -17,11 +18,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"> { - 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 +35,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 +58,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 +109,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) ); } 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(); 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 cdb75ddb8d..02d14aa64d 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) => { @@ -115,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), + } + ); }); diff --git a/packages/alfa-css/tsconfig.json b/packages/alfa-css/tsconfig.json index 00a9b65d09..4c7b2f88c6 100644 --- a/packages/alfa-css/tsconfig.json +++ b/packages/alfa-css/tsconfig.json @@ -87,6 +87,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", 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 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";