import { Array } from "@siteimprove/alfa-array" ; import { Cache } from "@siteimprove/alfa-cache" ; import { Device } from "@siteimprove/alfa-device" ; import { Attribute , Element , Node , Text } from "@siteimprove/alfa-dom" ; import { Equatable } from "@siteimprove/alfa-equatable" ; import { Iterable } from "@siteimprove/alfa-iterable" ; import { Serializable } from "@siteimprove/alfa-json" ; import { Option , None } from "@siteimprove/alfa-option" ; import { Predicate } from "@siteimprove/alfa-predicate" ; import { Refinement } from "@siteimprove/alfa-refinement" ; import { Sequence } from "@siteimprove/alfa-sequence" ; import { Style } from "@siteimprove/alfa-style" ; import { Thunk } from "@siteimprove/alfa-thunk" ; import * as json from "@siteimprove/alfa-json" ; import { Feature } from "./feature" ; import { Role } from "./role" ; import * as predicate from "./name/predicate" ; const { hasId , isElement } = Element ; const { isText } = Text ; const { equals } = Predicate ; const { or } = Refinement ; public export class Name implements Equatable , Serializable < Name . JSON > { public static of ( value , sources = [ ] ) { return new Name ( value , Array . from ( sources ) ) ; } private readonly _value : string ; private readonly _sources : Array < Name . Source > ; private constructor ( value , sources ) { this . _value = value ; this . _sources = sources ; } public get value ( ) : string { return this . _value ; } public get source ( ) : ReadonlyArray < Name . Source > { return this . _sources ; } public isEmpty ( ) { return this . _value . length === 0 ; } public equals ( value ) { return ( value instanceof Name && value . _value === this . _value && value . _sources . length === this . _sources . length && value . _sources . every ( ( source , i ) => source . equals ( this . _sources [ i ] ) ) ) ; } public toJSON ( ) { return { value : this . _value , sources : this . _sources . map ( ( source ) => source . toJSON ( ) ) , } ; } public toString ( ) { return this . _value ; } } public export namespace Name { export interface JSON { [ key ] : json . JSON ; value : string ; sources : Array < Source . JSON > ; } export type Source = Source . Data | Source . Descendant | Source . Ancestor | Source . Label | Source . Reference ; export namespace Source { export type JSON = Data . JSON | Descendant . JSON | Ancestor . JSON | Label . JSON | Reference . JSON ; export class Data implements Equatable , Serializable < Data . JSON > { public static of ( text ) { return new Data ( text ) ; } private readonly _text : Text ; private constructor ( text ) { this . _text = text ; } public get type ( ) : "data" { return "data" ; } public get text ( ) : Text { return this . _text ; } public equals ( value ) { return value instanceof Data && value . _text . equals ( this . _text ) ; } public toJSON ( ) { return { type : "data" , text : this . _text . path ( ) , } ; } } export namespace Data { export interface JSON { [ key ] : json . JSON ; type : "data" ; text : string ; } } export function data ( text ) { return Data . of ( text ) ; } export class Descendant implements Equatable , Serializable < Descendant . JSON > { public static of ( element , name ) { return new Descendant ( element , name ) ; } private readonly _element : Element ; private readonly _name : Name ; private constructor ( element , name ) { this . _element = element ; this . _name = name ; } public get type ( ) : "descendants" { return "descendants" ; } public get element ( ) : Element { return this . _element ; } public get name ( ) : Name { return this . _name ; } public equals ( value ) { return ( value instanceof Descendant && value . _element . equals ( this . _element ) && value . _name . equals ( this . _name ) ) ; } public toJSON ( ) { return { type : "descendant" , element : this . _element . path ( ) , name : this . _name . toJSON ( ) , } ; } } export namespace Descendant { export interface JSON { [ key ] : json . JSON ; type : "descendant" ; element : string ; name : Name . JSON ; } } export function descendant ( element , name ) { return Descendant . of ( element , name ) ; } export class Ancestor implements Equatable , Serializable < Ancestor . JSON > { public static of ( element , name ) { return new Ancestor ( element , name ) ; } private readonly _element : Element ; private readonly _name : Name ; private constructor ( element , name ) { this . _element = element ; this . _name = name ; } public get type ( ) : "ancestor" { return "ancestor" ; } public get element ( ) : Element { return this . _element ; } public get name ( ) : Name { return this . _name ; } public equals ( value ) { return ( value instanceof Ancestor && value . _element . equals ( this . _element ) && value . _name . equals ( this . _name ) ) ; } public toJSON ( ) { return { type : "ancestor" , element : this . _element . path ( ) , name : this . _name . toJSON ( ) , } ; } } export namespace Ancestor { export interface JSON { [ key ] : json . JSON ; type : "ancestor" ; element : string ; name : Name . JSON ; } } export function ancestor ( element , name ) { return Ancestor . of ( element , name ) ; } export class Label implements Equatable , Serializable < Label . JSON > { public static of ( attribute ) { return new Label ( attribute ) ; } private readonly _attribute : Attribute ; private constructor ( attribute ) { this . _attribute = attribute ; } public get type ( ) : "label" { return "label" ; } public get attribute ( ) : Attribute { return this . _attribute ; } public equals ( value ) { return ( value instanceof Label && value . _attribute . equals ( this . _attribute ) ) ; } public toJSON ( ) { return { type : "label" , attribute : this . _attribute . path ( ) , } ; } } export namespace Label { export interface JSON { [ key ] : json . JSON ; type : "label" ; attribute : string ; } } export function label ( attribute ) { return Label . of ( attribute ) ; } export class Reference implements Equatable , Serializable < Reference . JSON > { public static of ( attribute , name ) { return new Reference ( attribute , name ) ; } private readonly _attribute : Attribute ; private readonly _name : Name ; private constructor ( attribute , name ) { this . _attribute = attribute ; this . _name = name ; } public get type ( ) : "reference" { return "reference" ; } public get attribute ( ) : Attribute { return this . _attribute ; } public get name ( ) : Name { return this . _name ; } public equals ( value ) { return ( value instanceof Reference && value . _attribute . equals ( this . _attribute ) ) ; } public toJSON ( ) { return { type : "reference" , attribute : this . _attribute . path ( ) , name : this . _name . toJSON ( ) , } ; } } export namespace Reference { export interface JSON { [ key ] : json . JSON ; type : "reference" ; attribute : string ; name : Name . JSON ; } } export function reference ( attribute , name ) { return Reference . of ( attribute , name ) ; } } internal export class State implements Equatable , Serializable < State . JSON > { private static _empty = new State ( [ ] , None , false , false ) ; public static empty ( ) { return this . _empty ; } private readonly _visited : Array < Element > ; private readonly _referrer : Option < Element > ; private readonly _isRecursing : boolean ; private readonly _isDescending : boolean ; private constructor ( visited , referrer , isRecursing , isDescending ) { this . _visited = visited ; this . _referrer = referrer ; this . _isRecursing = isRecursing ; this . _isDescending = isDescending ; } /**
         * The elements that have been seen by the name computation so far. This is
         * used for detecting circular references resulting from things such as the
         * `aria-labelledby` attribute and form controls that get their name from
         * a containing `<label>` element.
         */ public get visited ( ) : Iterable < Element > { return this . _visited ; } /**
         * The element that referenced the name computation.
         */ public get referrer ( ) : Option < Element > { return this . _referrer ; } /**
         * Whether or not the name computation is the result of recursion.
         */ public get isRecursing ( ) : boolean { return this . _isRecursing ; } /**
         * Whether or not the name computation is the result of a reference.
         */ public get isReferencing ( ) : boolean { return this . _referrer . isSome ( ) ; } /**
         * Whether or not the name computation is descending into a subtree.
         */ public get isDescending ( ) : boolean { return this . _isDescending ; } public hasVisited ( element ) { return this . _visited . includes ( element ) ; } public visit ( element ) { if ( this . _visited . includes ( element ) ) { return this ; } return new State ( [ ... this . _visited , element ] , this . _referrer , this . _isRecursing , this . _isDescending ) ; } public recurse ( isRecursing ) { if ( this . _isRecursing === isRecursing ) { return this ; } return new State ( this . _visited , this . _referrer , isRecursing , this . _isDescending ) ; } public reference ( referrer ) { if ( this . _referrer . equals ( referrer ) ) { return this ; } return new State ( this . _visited , referrer , this . _isRecursing , this . _isDescending ) ; } public descend ( isDescending ) { if ( this . _isDescending === isDescending ) { return this ; } return new State ( this . _visited , this . _referrer , this . _isRecursing , isDescending ) ; } public equals ( state ) ; public equals ( value ) ; public equals ( value ) { return ( value instanceof State && Array . equals ( value . _visited , this . _visited ) && value . _referrer . equals ( this . _referrer ) && value . _isRecursing === this . _isRecursing && value . _isDescending === this . _isDescending ) ; } public toJSON ( ) { return { visited : this . _visited . map ( ( element ) => element . path ( ) ) , referrer : this . _referrer . map ( ( element ) => element . path ( ) ) . getOr ( null ) , isRecursing : this . _isRecursing , isDescending : this . _isDescending , } ; } } export namespace State { export interface JSON { [ key ] : json . JSON ; visited : Array < string > ; referrer : string | null ; isRecursing : boolean ; isDescending : boolean ; } } export function from ( node , device ) { return fromNode ( node , device , State . empty ( ) ) ; } const names = Cache . empty < Device , Cache < Node , Option < Name > > > ( ) ; internal export function fromNode ( node , device , state ) { const name = ( ) => isElement ( node ) ? fromElement ( node , device , state ) : fromText ( node ) ; if ( isElement ( node ) ) { if ( state . isReferencing ) { return name ( ) ; } if ( state . isDescending ) { return name ( ) ; } } return names . get ( device , Cache . empty ) . get ( node , name ) ; } internal export function fromElement ( element , device , state ) { if ( state . hasVisited ( element ) ) { if ( ! state . referrer . includes ( element ) ) { return None ; } } else { state = state . visit ( element ) ; } const role = Role . from ( element ) ; if ( ! state . isReferencing && ! state . isDescending && role . some ( ( role ) => role . isNameProhibited ( ) ) ) { return None ; } if ( ! state . isReferencing && isHidden ( element , device ) ) { return None ; } return fromSteps ( ( ) => { if ( state . isReferencing ) { return None ; } const attribute = element . attribute ( "aria-labelledby" ) ; if ( attribute . isNone ( ) ) { return None ; } return fromReferences ( attribute . get ( ) , element , device , state ) ; } , ( ) => { const attribute = element . attribute ( "aria-label" ) ; if ( attribute . isNone ( ) ) { return None ; } return fromLabel ( attribute . get ( ) ) ; } , ( ) => { if ( role . some ( ( role ) => role . isPresentational ( ) ) || element . namespace . isNone ( ) ) { return None ; } const feature = Feature . from ( element . namespace . get ( ) , element . name ) ; if ( feature . isNone ( ) ) { return None ; } return feature . get ( ) . name ( element , device , state ) ; } , ( ) => { if ( ! state . isReferencing && ! role . some ( ( role ) => role . isNamedBy ( "contents" ) ) ) { return None ; } return fromDescendants ( element , device , state ) ; } , ( ) => { if ( ! state . isDescending ) { return None ; } return fromDescendants ( element , device , state ) ; } ) ; } internal export function fromText ( text ) { return fromData ( text ) ; } internal export function fromDescendants ( element , device , state ) { const names = element . children ( ) . filter ( or ( isText , isElement ) ) . collect ( ( element ) => fromNode ( element , device , state . reference ( None ) . recurse ( true ) . descend ( true ) ) ) ; const name = flatten ( names . map ( ( name ) => name . value ) . join ( "" ) ) . trim ( ) ; if ( name === "" ) { return None ; } return Option . of ( Name . of ( name , names . map ( ( name ) => Source . descendant ( element , name ) ) ) ) ; } internal export function fromLabel ( attribute ) { const name = flatten ( attribute . value ) ; if ( name === "" ) { return None ; } return Option . of ( Name . of ( name , [ Source . label ( attribute ) ] ) ) ; } internal export function fromReferences ( attribute , referrer , device , state ) { const root = attribute . owner . get ( ) . root ( ) ; const references = root . descendants ( ) . filter ( isElement ) . filter ( hasId ( equals ( ... attribute . tokens ( ) ) ) ) ; const names = references . collect ( ( element ) => fromNode ( element , device , state . reference ( Option . of ( referrer ) ) . recurse ( true ) . descend ( false ) ) ) ; const name = flatten ( names . map ( ( name ) => name . value ) . join ( " " ) ) ; if ( name === "" ) { return None ; } return Option . of ( Name . of ( name , [ Source . reference ( attribute , Name . of ( name , names . flatMap ( ( name ) => Sequence . from ( name . source ) ) ) ) , ] ) ) ; } internal export function fromData ( text ) { const name = flatten ( text . data ) ; if ( name === "" ) { return None ; } return Option . of ( Name . of ( name , [ Source . data ( text ) ] ) ) ; } internal export function fromSteps ( ... steps ) { return Array . collectFirst ( steps , ( step ) => step ( ) ) ; } export const { hasValue } = predicate ; } function flatten ( string ) { return string . replace ( /\s+/g , " " ) ; } function isRendered ( node , device ) { if ( Element . isElement ( node ) ) { const display = Style . from ( node , device ) . computed ( "display" ) . value ; const { values : [ outside ] , } = display ; if ( outside . value === "none" ) { return false ; } } return node . parent ( { flattened : true } ) . every ( ( parent ) => isRendered ( parent , device ) ) ; } /**
 *  https *  https function isHidden ( element , device ) { return ( ! isRendered ( element , device ) || element . attribute ( "aria-hidden" ) . some ( ( attribute ) => attribute . value . toLowerCase ( ) === "true" ) ) ; }