diff --git a/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index afffc4ecff..d3a32f3e59 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -135,9 +135,11 @@ export class Calculation, resolver: Calculation.LengthResolver): Option>; + // (undocumented) resolve(this: Calculation<"length-percentage">, resolver: Calculation.Resolver<"px", Length<"px">>): Option>; // (undocumented) - resolve(this: Calculation<"number">, resolver: Calculation.Resolver<"px", Length<"px">>): Option; + resolve(this: Calculation<"number">, resolver: Calculation.PercentageResolver): Option; // (undocumented) toJSON(): Calculation.JSON; // (undocumented) @@ -156,8 +158,6 @@ export namespace Calculation { abstract equals(value: unknown): value is this; // (undocumented) abstract get kind(): Kind; - // Warning: (ae-incompatible-release-tags) The symbol "reduce" is marked as @public, but its signature references "Resolver" which is marked as @internal - // // (undocumented) abstract reduce(resolver: Resolver): Expression; // (undocumented) @@ -189,8 +189,6 @@ export namespace Calculation { get kind(): Kind; // (undocumented) static of(operand: Expression): Invert; - // Warning: (ae-incompatible-release-tags) The symbol "reduce" is marked as @public, but its signature references "Resolver" which is marked as @internal - // // (undocumented) reduce(resolver: Resolver): Expression; // (undocumented) @@ -268,12 +266,15 @@ export namespace Calculation { [K in Base]: number; }>; } + // @internal + export interface LengthResolver { + // (undocumented) + length(value: Length): Length; + } // (undocumented) export class Negate extends Operation.Unary { // (undocumented) static of(operand: Expression): Negate; - // Warning: (ae-incompatible-release-tags) The symbol "reduce" is marked as @public, but its signature references "Resolver" which is marked as @internal - // // (undocumented) reduce(resolver: Resolver): Expression; // (undocumented) @@ -316,11 +317,14 @@ export namespace Calculation { } } // (undocumented) + export interface PercentageResolver

{ + // (undocumented) + percentage(value: Percentage): P; + } + // (undocumented) export class Product extends Operation.Binary { // (undocumented) static of(...operands: [Expression, Expression]): Result; - // Warning: (ae-incompatible-release-tags) The symbol "reduce" is marked as @public, but its signature references "Resolver" which is marked as @internal - // // (undocumented) reduce(resolver: Resolver): Expression; // (undocumented) @@ -328,19 +332,14 @@ export namespace Calculation { // (undocumented) get type(): "product"; } - // @internal - export interface Resolver { - // (undocumented) - length(value: Length): Length; - // (undocumented) - percentage(value: Percentage): P; - } + // Warning: (ae-incompatible-release-tags) The symbol "Resolver" is marked as @public, but its signature references "LengthResolver" which is marked as @internal + // + // (undocumented) + export type Resolver = LengthResolver & PercentageResolver

; // (undocumented) export class Sum extends Operation.Binary { // (undocumented) static of(...operands: [Expression, Expression]): Result; - // Warning: (ae-incompatible-release-tags) The symbol "reduce" is marked as @public, but its signature references "Resolver" which is marked as @internal - // // (undocumented) reduce(resolver: Resolver): Expression; // (undocumented) @@ -356,8 +355,6 @@ export namespace Calculation { get kind(): Kind; // (undocumented) static of(value: Numeric): Value; - // Warning: (ae-incompatible-release-tags) The symbol "reduce" is marked as @public, but its signature references "Resolver" which is marked as @internal - // // (undocumented) reduce(resolver: Resolver): Value; // (undocumented) @@ -384,6 +381,8 @@ export namespace Calculation { // (undocumented) parse: Parser, Calculation, string, []>; const // (undocumented) + parseLength: Parser, Calculation<"length">, string, []>; + const // (undocumented) parseLengthPercentage: Parser, Calculation<"length-percentage">, string, []>; const // (undocumented) parseLengthNumberPercentage: Parser, Calculation<"number"> | Calculation<"length-percentage">, string, []>; diff --git a/packages/alfa-css/src/value/calculation.ts b/packages/alfa-css/src/value/calculation.ts index 0f5e8eaeb9..e4634b244a 100644 --- a/packages/alfa-css/src/value/calculation.ts +++ b/packages/alfa-css/src/value/calculation.ts @@ -110,9 +110,14 @@ export class Calculation< // Other resolvers should be added when needed. /** - * Resolves a calculation typed as a length-percentage or number. + * Resolves a calculation typed as a length, length-percentage or number. * Needs a resolver to handle relative lengths and percentages. */ + public resolve( + this: Calculation<"length">, + resolver: Calculation.LengthResolver + ): Option>; + public resolve( this: Calculation<"length-percentage">, resolver: Calculation.Resolver<"px", Length<"px">> @@ -120,7 +125,7 @@ export class Calculation< public resolve( this: Calculation<"number">, - resolver: Calculation.Resolver<"px", Length<"px">> + resolver: Calculation.PercentageResolver ): Option; public resolve( @@ -134,7 +139,8 @@ export class Calculation< const expression = this._expression.reduce(resolver); return this.isDimensionPercentage("length") - ? expression.toLength() + ? // length are also length-percentage, so this catches both. + expression.toLength() : this.isNumber() ? expression.toNumber() : None; @@ -188,14 +194,19 @@ export namespace Calculation { * * @internal */ - export interface Resolver< - L extends Unit.Length = "px", - P extends Numeric = Numeric - > { + export interface LengthResolver { length(value: Length): Length; + } + + export interface PercentageResolver

{ percentage(value: Percentage): P; } + export type Resolver< + L extends Unit.Length = "px", + P extends Numeric = Numeric + > = LengthResolver & PercentageResolver

; + function angleResolver(angle: Angle): Angle<"deg"> { return angle.withUnit("deg"); } @@ -971,6 +982,13 @@ export namespace Calculation { export const parse = map(parseCalc, Calculation.of); // other parsers + filters can be added when needed + export const parseLength = filter( + parse, + (calculation): calculation is Calculation<"length"> => + calculation.isDimension("length"), + () => `calc() expression must be of type "length"` + ); + export const parseLengthPercentage = filter( parse, (calculation): calculation is Calculation<"length-percentage"> => diff --git a/packages/alfa-style/src/property/font-size.ts b/packages/alfa-style/src/property/font-size.ts index 34f5e69ab6..52d8d2766a 100644 --- a/packages/alfa-style/src/property/font-size.ts +++ b/packages/alfa-style/src/property/font-size.ts @@ -111,25 +111,20 @@ export default Property.register( parse, (fontSize, style) => fontSize.map((fontSize) => { - const percentageResolver = Resolver.percentage( + const percentage = Resolver.percentage( style.parent.computed("font-size").value ); - const lengthResolver = Resolver.length(style.parent); + const length = Resolver.length(style.parent); switch (fontSize.type) { case "calculation": - return fontSize - .resolve({ - length: lengthResolver, - percentage: percentageResolver, - }) - .get(); + return fontSize.resolve({ length, percentage }).get(); case "length": - return lengthResolver(fontSize); + return length(fontSize); case "percentage": { - return percentageResolver(fontSize); + return percentage(fontSize); } case "keyword": { diff --git a/packages/alfa-style/src/property/line-height.ts b/packages/alfa-style/src/property/line-height.ts index 5172fce3ba..6fae60c4d4 100644 --- a/packages/alfa-style/src/property/line-height.ts +++ b/packages/alfa-style/src/property/line-height.ts @@ -58,10 +58,10 @@ export default Property.register( parse, (value, style) => value.map((height) => { - const percentageResolver = Resolver.percentage( + const percentage = Resolver.percentage( style.parent.computed("font-size").value ); - const lengthResolver = Resolver.length(style); + const length = Resolver.length(style); switch (height.type) { case "keyword": @@ -69,24 +69,16 @@ export default Property.register( return height; case "length": - return lengthResolver(height); + return length(height); case "percentage": - return percentageResolver(height); + return percentage(height); case "calculation": - // TS can't see that the union is exactly covered by the overloads - // so we have to do this ugly split :-/ return ( height.isNumber() - ? height.resolve({ - length: lengthResolver, - percentage: percentageResolver, - }) - : height.resolve({ - length: lengthResolver, - percentage: percentageResolver, - }) + ? height.resolve({ percentage }) + : height.resolve({ length, percentage }) ).get(); } }), diff --git a/packages/alfa-style/src/property/outline-offset.ts b/packages/alfa-style/src/property/outline-offset.ts index e10ca59e92..7f56b15858 100644 --- a/packages/alfa-style/src/property/outline-offset.ts +++ b/packages/alfa-style/src/property/outline-offset.ts @@ -1,8 +1,11 @@ -import { Length } from "@siteimprove/alfa-css"; +import { Calculation, Length } from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; import { Property } from "../property"; import { Resolver } from "../resolver"; +const { either } = Parser; + declare module "../property" { interface Longhands { "outline-offset": Property; @@ -12,7 +15,7 @@ declare module "../property" { /** * @internal */ -export type Specified = Length; +export type Specified = Length | Calculation<"length">; /** * @internal @@ -22,17 +25,23 @@ export type Computed = Length<"px">; /** * @internal */ -export const parse = Length.parse; +export const parse = either(Length.parse, Calculation.parseLength); /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/outline-offset} */ export default Property.register( "outline-offset", - Property.of( - Length.of(0, "px"), - parse, - (outlineOffset, style) => - outlineOffset.map((offset) => Resolver.length(offset, style)) + Property.of(Length.of(0, "px"), parse, (value, style) => + value.map((offset) => { + const length = Resolver.length(style); + + switch (offset.type) { + case "length": + return length(offset); + case "calculation": + return offset.resolve({ length }).get(); + } + }) ) ); diff --git a/packages/alfa-style/src/property/outline-width.ts b/packages/alfa-style/src/property/outline-width.ts index f6f9a89313..225a6d92ed 100644 --- a/packages/alfa-style/src/property/outline-width.ts +++ b/packages/alfa-style/src/property/outline-width.ts @@ -1,5 +1,6 @@ -import { Keyword, Length } from "@siteimprove/alfa-css"; +import { Calculation, Keyword, Length, Token } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; +import { Slice } from "@siteimprove/alfa-slice"; import { Property } from "../property"; import { Resolver } from "../resolver"; @@ -18,6 +19,7 @@ declare module "../property" { */ export type Specified = | Length + | Calculation<"length"> | Keyword<"thin"> | Keyword<"medium"> | Keyword<"thick">; @@ -30,9 +32,10 @@ export type Computed = Length<"px">; /** * @internal */ -export const parse = either( +export const parse = either, Specified, string>( Keyword.parse("thin", "medium", "thick"), - Length.parse + Length.parse, + Calculation.parseLength ); /** @@ -44,17 +47,25 @@ export default Property.register( Property.of( Length.of(3, "px"), parse, - (outlineWidth, style) => { + (value, style) => { if ( style.computed("outline-style").some(({ value }) => value === "none") ) { return Value.of(Length.of(0, "px")); } - return outlineWidth.map((value) => { - switch (value.type) { + return value.map((width) => { + const length = Resolver.length(style); + + switch (width.type) { + case "length": + return length(width); + + case "calculation": + return width.resolve({ length }).get(); + case "keyword": - switch (value.value) { + switch (width.value) { case "thin": return Length.of(1, "px"); @@ -64,9 +75,6 @@ export default Property.register( case "thick": return Length.of(5, "px"); } - - case "length": - return Resolver.length(value, style); } }); } diff --git a/packages/alfa-style/test/property/outline.spec.tsx b/packages/alfa-style/test/property/outline.spec.tsx new file mode 100755 index 0000000000..2596464430 --- /dev/null +++ b/packages/alfa-style/test/property/outline.spec.tsx @@ -0,0 +1,103 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom/h"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../src/style"; + +const device = Device.standard(); + +test("#computed() resolves `outline-offset: calc(1em + 2px)`", (t) => { + const element =

; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("outline-offset").toJSON(), { + value: { + type: "length", + value: 18, + unit: "px", + }, + source: h.declaration("outline-offset", "calc(1em + 2px)").toJSON(), + }); +}); + +test("#computed() rejects `outline-offset: calc(1 + 2)`", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("outline-offset").toJSON(), { + // initial value + value: { + type: "length", + value: 0, + unit: "px", + }, + source: null, + }); +}); + +test("#computed() resolves `outline-width: calc(1em + 2px)`", (t) => { + const element = ( +
+ ); + + const style = Style.from(element, device); + + t.deepEqual(style.computed("outline-width").toJSON(), { + value: { + type: "length", + value: 18, + unit: "px", + }, + source: h.declaration("outline-width", "calc(1em + 2px)").toJSON(), + }); +}); + +test("#computed() resolves `outline: solid calc(1em + 2px) red`", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("outline-style").toJSON(), { + value: { + type: "keyword", + value: "solid", + }, + source: h.declaration("outline", "solid calc(1em + 2px) red").toJSON(), + }); + + t.deepEqual(style.computed("outline-width").toJSON(), { + value: { + type: "length", + value: 18, + unit: "px", + }, + source: h.declaration("outline", "solid calc(1em + 2px) red").toJSON(), + }); + + t.deepEqual(style.computed("outline-color").toJSON(), { + value: { + type: "color", + format: "rgb", + red: { + type: "percentage", + value: 1, + }, + green: { + type: "percentage", + value: 0, + }, + blue: { + type: "percentage", + value: 0, + }, + alpha: { + type: "percentage", + value: 1, + }, + }, + source: h.declaration("outline", "solid calc(1em + 2px) red").toJSON(), + }); +}); diff --git a/packages/alfa-style/tsconfig.json b/packages/alfa-style/tsconfig.json index ec941733cc..33160f9c7f 100644 --- a/packages/alfa-style/tsconfig.json +++ b/packages/alfa-style/tsconfig.json @@ -212,6 +212,7 @@ "test/property/font-variant.spec.tsx", "test/property/inset-block.spec.tsx", "test/property/line-height.spec.tsx", + "test/property/outline.spec.tsx", "test/property/text-decoration.spec.tsx", "test/property/text-indent.spec.tsx", "test/property/text-shadow.spec.tsx",