UCDL Accessibility Use Case Definition Language
Contents

Accessibility Use Case Definition Language (UCDL) and Runner

Appendix A. ABNF Grammar for Steps

This appendix is normative.

; A step entry is a YAML mapping with one key. After the YAML layer, what
; remains for the parser is a (keyword, rest) pair. Each keyword is bound to
; its own argument grammar: a step is well-formed only when the keyword and
; the rest belong to the same alternative below.

step           = "locate:" SP locate-rest
               / "focus:" SP focus-rest
               / "enter:" SP enter-rest
               / "select:" SP select-rest
               / "deselect:" SP deselect-rest
               / "activate:" SP activate-rest
               / "toggle:" SP toggle-rest
               / "verify:" SP verify-rest
               / "wait:" SP wait-rest
               / "wait_for:" SP wait-for-rest
               / "navigate:" SP navigate-rest
               / "screenshot:" SP screenshot-rest
               / "keyboard:" SP keyboard-rest
               / "type:" SP type-rest
               / "scroll:" SP scroll-rest
               / "hover:" SP hover-rest
               / "hover_out:" SP hover-out-rest
               / "note:" SP note-rest
               / "audit:" SP audit-rest
               / "contrast:" SP contrast-rest
               / "lang_check:" SP lang-check-rest
               / "read_image:" SP read-image-rest
               / "sr_says:" SP sr-says-rest
               / "viewport:" SP viewport-rest
               / "anchor:" SP anchor-rest

; The keyword and rest sets, named for the step-descriptor in
; `sr_says ... after` (which takes a bare keyword) and for the §5.1 rule that
; rejects a keyword outside the set. They do not license a step on their own:
; `focus: 2000` pairs a keyword with another keyword's rest and is not
; derivable from `step`.

keyword        = "locate" / "focus" / "enter" / "select" / "deselect"
               / "activate" / "toggle" / "verify" / "wait" / "wait_for"
               / "navigate" / "screenshot" / "keyboard" / "type" / "scroll"
               / "hover" / "hover_out" / "note" / "audit" / "contrast"
               / "lang_check" / "read_image" / "sr_says" / "viewport"
               / "anchor"

rest           = locate-rest / focus-rest / enter-rest / select-rest
               / deselect-rest / activate-rest / toggle-rest / verify-rest
               / wait-rest / wait-for-rest / navigate-rest / screenshot-rest
               / keyboard-rest / type-rest / scroll-rest / hover-rest
               / hover-out-rest / note-rest / audit-rest / contrast-rest
               / lang-check-rest / read-image-rest / sr-says-rest
               / viewport-rest / anchor-rest

; --- Targets ---
;
; The accessible name is optional for role-based targets (curated tokens
; and the `role "X"` form). `field`, `text`, `id`, and `data-*` still
; require their argument. `within` and `inside` are synonyms.

target         = role-and-name [ SP scope-kw SP target ]
scope-kw       = "within" / "inside"
role-and-name  = curated-role [ SP [ "contains" SP ] qstring ]
               / "role" SP qstring [ SP "name" SP [ "contains" SP ] qstring ]
               / "field" SP [ "contains" SP ] qstring
               / "text" SP [ "contains" SP ] qstring
               / "id" SP qstring
               / data-role SP qstring
               / "self"
curated-role   = ; ARIA 1.2 widget, composite, structure, landmark,
                 ; live-region, and window roles (excluding `field` and
                 ; `text`, which appear above with required names).
                 "link" / "button" / "checkbox" / "radio" / "switch"
               / "option" / "select" / "combobox" / "searchbox" / "textbox"
               / "slider" / "spinbutton" / "progressbar" / "meter"
               / "scrollbar" / "separator" / "heading" / "image" / "dialog"
               / "alertdialog" / "tab" / "tabpanel" / "treeitem" / "menu"
               / "menubar" / "menuitem" / "radiogroup" / "tablist"
               / "listbox" / "tree" / "treegrid" / "grid" / "article"
               / "feed" / "figure" / "group" / "list" / "listitem"
               / "table" / "row" / "rowgroup" / "cell" / "gridcell"
               / "columnheader" / "rowheader" / "toolbar" / "tooltip"
               / "banner" / "navigation" / "main" / "complementary"
               / "contentinfo" / "region" / "search" / "form" / "alert"
               / "log" / "status"
data-role      = "data-" 1*data-char
data-char      = ALPHA / DIGIT / "-" / "_"

; The `self` token is only resolvable inside a `scope.for_each` body.
; A processor MUST raise a runtime error if `self` is used elsewhere.

; --- scope.for_each grammar ---
;
; The accessible name is optional. The expression may itself be scoped
; with `within`/`inside` to limit iteration to descendants of a parent.

for-each-expr  = "criteria" SP 1*VCHAR
               / target

; A quoted string may hold any Unicode scalar value other than DQUOTE, the
; backslash and the control characters, C0 and DEL (§5.3). The processor
; preserves the code
; points as authored and applies no normalisation.

qstring        = DQUOTE *( escape / passthrough / qchar ) DQUOTE
qchar          = %x20-21 / %x23-5B / %x5D-7E / %x80-D7FF / %xE000-10FFFF
escape         = %x5C DQUOTE / %x5C %x5C       ; \" and \\ denote one character
; A backslash before any other character denotes both characters literally:
; "a\nb" is a, backslash, n, b — not a newline.
passthrough    = %x5C qchar

; --- Per-keyword grammars ---

locate-rest    = target [ SP "level" SP 1*DIGIT ]
focus-rest     = target
enter-rest     = target SP "value" SP qstring
                 *( SP enter-mod )
enter-mod      = "type_slowly" SP boolean
               / "clear_first" SP boolean
; `force` is only valid on `option` targets, and suppresses the post-click
; aria-selected assertion (§5.6.4).
select-rest    = target [ SP "option" SP qstring ] [ SP "force" SP boolean ]
deselect-rest  = target
; `force` and `via keyboard` are mutually exclusive (§5.6.7).
activate-rest  = target *( SP activate-mod )
activate-mod   = "via" SP "keyboard"
               / "new_tab" SP boolean
               / "force" SP boolean

; The attribute clause takes NO predicate: toggle asserts the value changed,
; it does not compare it to anything.
toggle-rest    = target [ SP "attribute" SP qstring ]

verify-rest    = "url" SP qstring
               / "title" SP qstring
               / "download" SP qstring
               / "live_region" [ SP live-qualifier ] SP qstring
               / "alert" SP qstring
               / "field_error" SP ( qstring / role-and-name )
               / "text" SP [ "contains" SP ] qstring
               / "heading" SP qstring [ SP scope-kw SP target ]
                 [ SP "level" SP 1*DIGIT ]
               / iter-summary-rest
               / state-verify SP target
               / "count" SP target SP "is" SP 1*DIGIT
               / target SP "has_value" SP qstring
               / target SP "attribute" SP qstring SP attr-predicate
               / target

state-verify   = "visible" / "hidden" / "enabled" / "disabled"
               / "checked" / "unchecked" / "focus"

; --- Attribute predicates ---

attr-predicate = "is" SP qstring
               / "is_or_absent" SP qstring
               / "present"
               / "absent"
               / "starts_with" SP qstring
               / "matches" SP qstring
               / "references_existing_id"
               / "within_range_of" SP qstring SP qstring

; --- iteration_summary terminal verify ---

live-qualifier = "role" SP qstring / "politeness" SP politeness
politeness     = %s"polite" / %s"assertive"

iter-summary-rest = "iteration_summary." iter-summary-field SP "is" SP 1*DIGIT
iter-summary-field = "failed" / "passed" / "total" / "inapplicable"

anchor-rest        = 1*( ALPHA / DIGIT / "_" / "-" )

wait-rest          = duration
duration           = 1*DIGIT [ "ms" / "s" ]
wait-for-rest      = target
navigate-rest      = qstring / 1*char
screenshot-rest    = DQUOTE screenshot-name DQUOTE / screenshot-name
screenshot-name    = 1*( ALPHA / DIGIT / "." / "_" / "-" )
; A screenshot name labels one capture beneath `screenshot_dir`; it is not a
; path. The quoted spelling carries the same restriction as the bare one --
; the quotes are not part of the name. `.` and `..` match `screenshot-name`
; but MUST also be rejected (§5.6.12), which the grammar cannot state.
; Every key-spec MUST name a key the processor can press. A token that does not
; is rejected at parse time rather than compiled to a press that fails at run
; time; `type` is the verb for literal text.
keyboard-rest      = key-spec *( SP key-spec )
key-spec           = 1*( ALPHA / DIGIT / "+" )
type-rest          = 1*char
scroll-rest        = [ "to" SP ] target
hover-rest         = target
hover-out-rest     = target
note-rest          = qstring / 1*char
audit-rest         = "page" [ SP "level" SP audit-level ]
                   / target [ SP "level" SP audit-level ]
audit-level        = DQUOTE ( "A" / "AA" / "AAA" ) DQUOTE
contrast-rest      = target [ SP "level" SP contrast-level ]
contrast-level     = DQUOTE ( "AA" / "AAA" ) DQUOTE
lang-check-rest    = "page" / target
read-image-rest    = target [ SP read-image-expectation ]
                     [ SP "lang" SP qstring ]
read-image-expectation = "has_text" SP qstring
                       / "matches" SP qstring
sr-says-rest       = sr-says-payload
                     [ SP "after" SP step-descriptor ]
                     [ SP "within" SP sr-says-duration ]
sr-says-payload    = sr-says-expectation
                   / "role" SP qstring SP sr-says-expectation
sr-says-expectation = qstring / "matches" SP qstring
sr-says-duration   = duration
viewport-rest      = viewport-preset / viewport-size
viewport-preset    = "mobile" / "tablet" / "desktop" / "reflow"
viewport-size      = 1*DIGIT "x" 1*DIGIT
step-descriptor    = keyword [ SP target ]

; --- Lexical primitives ---

boolean        = "true" / "false"
SP             = %x20
DQUOTE         = %x22
ALPHA          = %x41-5A / %x61-7A
DIGIT          = %x30-39
char           = ALPHA / DIGIT / SP / "-" / "_" / "/" / "." / ":" / "?" / "&" / "=" / "%"

The grammar above is the post-tokenization shape; YAML parsing handles the outer layer. The reference implementation's parser is hand-written, and there are five places where a hand-written parser is easily more permissive than this grammar. A conforming processor MUST reject all five: an unrecognized modifier, which is an error rather than a warning (§7.5); a boolean other than true or false, which MUST NOT be coerced to false; a "via" argument other than keyboard; a modifier belonging to one keyword after another keyword's target, since the per-keyword productions above (locate-rest, enter-mod, activate-mod, …) apply to every modifier and not to force alone; and input following wait-rest, wait-for-rest, scroll-rest or lang-check-rest, which are closed productions with nothing optional left at the end even though those four verbs never reach a modifier list.