import { Rule , Diagnostic } from "@siteimprove/alfa-act" ; import { Cache } from "@siteimprove/alfa-cache" ; import { Color } from "@siteimprove/alfa-css" ; import { Device } from "@siteimprove/alfa-device" ; import { Element , Node , Text } from "@siteimprove/alfa-dom" ; import { Equatable } from "@siteimprove/alfa-equatable" ; import { Hash , Hashable } from "@siteimprove/alfa-hash" ; import { Serializable } from "@siteimprove/alfa-json" ; import { Map } from "@siteimprove/alfa-map" ; import { Option , None } from "@siteimprove/alfa-option" ; import { Predicate } from "@siteimprove/alfa-predicate" ; import { Refinement } from "@siteimprove/alfa-refinement" ; import { Err , Ok , Result } from "@siteimprove/alfa-result" ; import { Context } from "@siteimprove/alfa-selector" ; import { Sequence } from "@siteimprove/alfa-sequence" ; import { Set } from "@siteimprove/alfa-set" ; import { Property , Style } from "@siteimprove/alfa-style" ; import { Criterion } from "@siteimprove/alfa-wcag" ; import { Page } from "@siteimprove/alfa-web" ; import * as json from "@siteimprove/alfa-json" ; import { expectation } from "../common/expectation" ; import { hasBorder , hasComputedStyle , hasOutline , hasRole , hasTextDecoration , isVisible , hasBoxShadow , isWhitespace , } from "../common/predicate" ; import { Scope } from "../tags" ; import { Serialise } from "./serialise" ; const { isElement } = Element ; const { isText } = Text ; const { or , not , test } = Predicate ; const { and } = Refinement ; export default Rule . Atomic . of < Page , Element > ( { uri : "https://alfa.siteimprove.com/rules/sia-r62" , requirements : [ Criterion . of ( "1.4.1" ) ] , tags : [ Scope . Component ] , evaluate ( { device , document } ) { let containers = Map . empty ( ) ; return { applicability ( ) { return visit ( document , None ) ; function * visit ( node , container ) { if ( isElement ( node ) ) { if ( test ( hasRole ( device , ( role ) => role . is ( "link" ) ) , node ) ) { if ( container . isSome ( ) && node . descendants ( { flattened : true } ) . some ( and ( isText , isVisible ( device ) ) ) ) { containers = containers . set ( node , container . get ( ) ) ; return yield node ; } } else if ( test ( and ( hasRole ( device , "paragraph" ) , hasNonLinkText ( device ) ) , node ) ) { container = Option . of ( node ) ; } } const children = node . children ( { flattened : true , nested : true , } ) ; for ( const child of children ) { yield * visit ( child , container ) ; } } } , expectations ( target ) { const nonLinkElements = containers . get ( target ) . get ( ) . inclusiveDescendants ( { flattened : true , nested : true , } ) . filter ( and ( isElement , hasNonLinkText ( device ) ) ) ; const linkElements = target . inclusiveDescendants ( { flattened : true , nested : true , } ) . filter ( isElement ) . concat ( target . ancestors ( { flattened : true , nested : true , } ) . takeWhile ( and ( isElement , not ( hasNonLinkText ( device ) ) ) ) ) ; const hasDistinguishingStyle = ( context ? ) => Set . from ( linkElements . map ( ( link ) => nonLinkElements . some ( ( container ) => isDistinguishable ( container , device , context ) ( link ) ) ? Ok . of ( ComputedStyles . from ( link , device , context ) ) : Err . of ( ComputedStyles . from ( link , device , context ) ) ) ) . toArray ( ) . sort ( ( a , b ) => ( b . isOk ( ) ? 1 : - 1 ) ) ; const isDefaultDistinguishable = hasDistinguishingStyle ( ) ; const isHoverDistinguishable = hasDistinguishingStyle ( Context . hover ( target ) ) ; const isFocusDistinguishable = hasDistinguishingStyle ( Context . focus ( target ) ) ; return { 1 : expectation ( isDefaultDistinguishable [ 0 ] . isOk ( ) && isHoverDistinguishable [ 0 ] . isOk ( ) && isFocusDistinguishable [ 0 ] . isOk ( ) , ( ) => Outcomes . IsDistinguishable ( isDefaultDistinguishable , isHoverDistinguishable , isFocusDistinguishable ) , ( ) => Outcomes . IsNotDistinguishable ( isDefaultDistinguishable , isHoverDistinguishable , isFocusDistinguishable ) ) , } ; } , } ; } , } ) ; export namespace Outcomes { export const IsDistinguishable = ( defaultStyles , hoverStyles , focusStyles ) => Ok . of ( DistinguishingStyles . of ( `The link is distinguishable from the surrounding text` , defaultStyles , hoverStyles , focusStyles ) ) ; export const IsNotDistinguishable = ( defaultStyles , hoverStyles , focusStyles ) => Err . of ( DistinguishingStyles . of ( `The link is not distinguishable from the surrounding text` , defaultStyles , hoverStyles , focusStyles ) ) ; } const hasNonLinkTextCache = Cache . empty < Element , boolean > ( ) ; function hasNonLinkText ( device ) { return function hasNonLinkText ( element ) { return hasNonLinkTextCache . get ( element , ( ) => { if ( element . inclusiveAncestors ( { flattened : true , } ) . some ( and ( isElement , hasRole ( device , ( role ) => role . is ( "link" ) ) ) ) ) { return false ; } const children = element . children ( { flattened : true , } ) ; if ( children . some ( and ( isText , and < Text > ( isVisible ( device ) , ( text ) => ! isWhitespace ( text . data ) ) ) ) ) { return true ; } return children . filter ( isElement ) . reject ( hasRole ( device , ( role ) => role . is ( "link" ) ) ) . some ( hasNonLinkText ) ; } ) ; } ; } function isDistinguishable ( container , device , context = Context . empty ( ) ) { return or ( hasDistinguishableTextDecoration ( container , device , context ) , hasDistinguishableBackground ( container , device , context ) , hasDistinguishableFontWeight ( container , device , context ) , hasDistinguishableVerticalAlign ( container , device , context ) , hasOutline ( device , context ) , hasBorder ( device , context ) , hasBoxShadow ( device , context ) ) ; } function hasDistinguishableTextDecoration ( container , device , context ? ) { return ( element ) => test ( not ( hasTextDecoration ( device , context ) ) , container ) && test ( hasTextDecoration ( device , context ) , element ) ; } remarks function hasDistinguishableBackground ( container , device , context ? ) { const colorReference = Style . from ( container , device , context ) . computed ( "background-color" ) . value ; const imageReference = Style . from ( container , device , context ) . computed ( "background-image" ) . value ; return or ( hasComputedStyle ( "background-color" , not ( ( color ) => Color . isTransparent ( color ) || color . equals ( colorReference ) ) , device , context ) , hasComputedStyle ( "background-image" , not ( ( image ) => image . equals ( imageReference ) ) , device , context ) ) ; } /**
 * Check if an element has a different font weight than its container.
 *
 * This is brittle and imperfect but removes a strong pain point until we find
 * a better solution.
 */ function hasDistinguishableFontWeight ( container , device , context ? ) { const reference = Style . from ( container , device , context ) . computed ( "font-weight" ) . value ; return hasComputedStyle ( "font-weight" , not ( ( weight ) => weight . equals ( reference ) ) , device , context ) ; } function hasDistinguishableVerticalAlign ( container , device , context ? ) { const reference = Style . from ( container , device , context ) . computed ( "vertical-align" ) . value ; return hasComputedStyle ( "vertical-align" , not ( ( alignment ) => alignment . equals ( reference ) ) , device , context ) ; } type Name = Property . Name | Property . Shorthand . Name ; export class ComputedStyles implements Equatable , Hashable , Serializable { public static of ( style = [ ] ) { return new ComputedStyles ( Map . from ( style ) ) ; } private readonly _style : Map < Name , string > ; private constructor ( style ) { this . _style = style ; } public get style ( ) : Map < Name , string > { return this . _style ; } public equals ( value ) ; public equals ( value ) ; public equals ( value ) { return value instanceof ComputedStyles && value . _style . equals ( this . _style ) ; } public hash ( hash ) { this . _style . hash ( hash ) ; } public toJSON ( ) { return { style : this . _style . toJSON ( ) , } ; } } export namespace ComputedStyles { export interface JSON { [ key ] : json . JSON ; style : Map . JSON < Name , string > ; } export function isComputedStyles ( value ) { return value instanceof ComputedStyles ; } export function from ( element , device , context = Context . empty ( ) ) { const style = Style . from ( element , device , context ) ; const border = ( [ "color" , "style" , "width" ] as const ) . map ( ( property ) => Serialise . borderShorthand ( style , property ) ) ; return ComputedStyles . of ( [ ... border , [ "color" , Serialise . getLonghand ( style , "color" ) ] as const , [ "font-weight" , Serialise . getLonghand ( style , "font-weight" ) ] as const , [ "vertical-align" , Serialise . getLonghand ( style , "vertical-align" ) , ] as const , [ "background" , Serialise . background ( style ) ] as const , [ "outline" , Serialise . outline ( style ) ] as const , [ "text-decoration" , Serialise . textDecoration ( style ) ] as const , [ "box-shadow" , Serialise . boxShadow ( style ) ] as const , ] . filter ( ( [ _ , value ] ) => value !== "" ) ) ; } } export class DistinguishingStyles extends Diagnostic { public static of ( message , defaultStyles = Sequence . empty ( ) , hoverStyles = Sequence . empty ( ) , focusStyles = Sequence . empty ( ) ) { return new DistinguishingStyles ( message , Sequence . from ( defaultStyles ) , Sequence . from ( hoverStyles ) , Sequence . from ( focusStyles ) ) ; } private readonly _defaultStyles : Sequence < Result < ComputedStyles > > ; private readonly _hoverStyles : Sequence < Result < ComputedStyles > > ; private readonly _focusStyles : Sequence < Result < ComputedStyles > > ; private constructor ( message , defaultStyles , hoverStyles , focusStyles ) { super ( message ) ; this . _defaultStyles = defaultStyles ; this . _hoverStyles = hoverStyles ; this . _focusStyles = focusStyles ; } public get defaultStyles ( ) : Iterable < Result < ComputedStyles > > { return this . _defaultStyles ; } public get hoverStyles ( ) : Iterable < Result < ComputedStyles > > { return this . _hoverStyles ; } public get focusStyles ( ) : Iterable < Result < ComputedStyles > > { return this . _focusStyles ; } public equals ( value ) ; public equals ( value ) ; public equals ( value ) { return ( value instanceof DistinguishingStyles && value . _defaultStyles . equals ( this . _defaultStyles ) && value . _hoverStyles . equals ( this . _hoverStyles ) && value . _focusStyles . equals ( this . _focusStyles ) ) ; } public toJSON ( ) { return { ... super . toJSON ( ) , defaultStyle : this . _defaultStyles . toJSON ( ) , hoverStyle : this . _hoverStyles . toJSON ( ) , focusStyle : this . _focusStyles . toJSON ( ) , } ; } } export namespace DistinguishingStyles { export interface JSON extends Diagnostic . JSON { defaultStyle : Sequence . JSON < Result < ComputedStyles > > ; hoverStyle : Sequence . JSON < Result < ComputedStyles > > ; focusStyle : Sequence . JSON < Result < ComputedStyles > > ; } export function isDistinguishingStyles ( value ) { return value instanceof DistinguishingStyles ; } }