UNPKG

svelte

Version:

Cybernetically enhanced web apps

963 lines (895 loc) 27.8 kB
/** @import { AST } from '#compiler' */ /** @import { Context } from '../../../types.js' */ /** @import { ARIARoleDefinitionKey, ARIARoleRelationConcept, ARIAProperty, ARIAPropertyDefinition, ARIARoleDefinition } from 'aria-query' */ import { a11y_distracting_elements, a11y_implicit_semantics, a11y_interactive_handlers, a11y_labelable, a11y_nested_implicit_semantics, a11y_non_interactive_element_to_interactive_role_exceptions, a11y_recommended_interactive_handlers, a11y_required_attributes, a11y_required_content, abstract_roles, address_type_tokens, aria_attributes, aria_roles, autofill_contact_field_name_tokens, autofill_field_name_tokens, combobox_if_list, contact_type_tokens, ElementInteractivity, input_type_to_implicit_role, interactive_element_ax_object_schemas, interactive_element_role_schemas, interactive_roles, invisible_elements, menuitem_type_to_implicit_role, non_interactive_element_ax_object_schemas, non_interactive_element_role_schemas, non_interactive_roles, presentation_roles } from './constants.js'; import { roles as roles_map, aria } from 'aria-query'; // @ts-expect-error package doesn't provide typings import { AXObjectRoles, elementAXObjects } from 'axobject-query'; import { regex_heading_tags, regex_js_prefix, regex_not_whitespace, regex_redundant_img_alt, regex_starts_with_vowel, regex_whitespaces } from '../../../../patterns.js'; import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { list } from '../../../../../utils/string.js'; import { walk } from 'zimmerframe'; import fuzzymatch from '../../../../1-parse/utils/fuzzymatch.js'; import { is_content_editable_binding } from '../../../../../../utils.js'; import * as w from '../../../../../warnings.js'; /** * @param {AST.RegularElement | AST.SvelteElement} node * @param {Context} context */ export function check_element(node, context) { /** @type {Map<string, AST.Attribute>} */ const attribute_map = new Map(); /** @type {Set<string>} */ const handlers = new Set(); /** @type {AST.Attribute[]} */ const attributes = []; const is_dynamic_element = node.type === 'SvelteElement'; let has_spread = false; let has_contenteditable_attr = false; let has_contenteditable_binding = false; for (const attribute of node.attributes) { switch (attribute.type) { case 'Attribute': { if (is_event_attribute(attribute)) { handlers.add(attribute.name.slice(2)); } else { attributes.push(attribute); attribute_map.set(attribute.name, attribute); if (attribute.name === 'contenteditable') { has_contenteditable_attr = true; } } break; } case 'SpreadAttribute': { has_spread = true; break; } case 'BindDirective': { if (is_content_editable_binding(attribute.name)) { has_contenteditable_binding = true; } break; } case 'OnDirective': { handlers.add(attribute.name); break; } } } for (const attribute of node.attributes) { if (attribute.type !== 'Attribute') continue; const name = attribute.name.toLowerCase(); // aria-props if (name.startsWith('aria-')) { if (invisible_elements.includes(node.name)) { // aria-unsupported-elements w.a11y_aria_attributes(attribute, node.name); } const type = name.slice(5); if (!aria_attributes.includes(type)) { const match = fuzzymatch(type, aria_attributes); w.a11y_unknown_aria_attribute(attribute, type, match); } if (name === 'aria-hidden' && regex_heading_tags.test(node.name)) { w.a11y_hidden(attribute, node.name); } // aria-proptypes let value = get_static_value(attribute); const schema = aria.get(/** @type {ARIAProperty} */ (name)); if (schema !== undefined) { validate_aria_attribute_value(attribute, /** @type {ARIAProperty} */ (name), schema, value); } // aria-activedescendant-has-tabindex if ( name === 'aria-activedescendant' && !is_dynamic_element && !is_interactive_element(node.name, attribute_map) && !attribute_map.has('tabindex') && !has_spread ) { w.a11y_aria_activedescendant_has_tabindex(attribute); } } switch (name) { // aria-role case 'role': { if (invisible_elements.includes(node.name)) { // aria-unsupported-elements w.a11y_misplaced_role(attribute, node.name); } const value = get_static_value(attribute); if (typeof value !== 'string') { break; } for (const c_r of value.split(regex_whitespaces)) { const current_role = /** @type {ARIARoleDefinitionKey} current_role */ (c_r); if (current_role && is_abstract_role(current_role)) { w.a11y_no_abstract_role(attribute, current_role); } else if (current_role && !aria_roles.includes(current_role)) { const match = fuzzymatch(current_role, aria_roles); w.a11y_unknown_role(attribute, current_role, match); } // no-redundant-roles if ( current_role === get_implicit_role(node.name, attribute_map) && // <ul role="list"> is ok because CSS list-style:none removes the semantics and this is a way to bring them back !['ul', 'ol', 'li'].includes(node.name) && // <a role="link" /> is ok because without href the a tag doesn't have a role of link !(node.name === 'a' && !attribute_map.has('href')) ) { w.a11y_no_redundant_roles(attribute, current_role); } // Footers and headers are special cases, and should not have redundant roles unless they are the children of sections or articles. const is_parent_section_or_article = is_parent(context.path, ['section', 'article']); if (!is_parent_section_or_article) { const has_nested_redundant_role = current_role === a11y_nested_implicit_semantics.get(node.name); if (has_nested_redundant_role) { w.a11y_no_redundant_roles(attribute, current_role); } } // role-has-required-aria-props if ( !is_dynamic_element && !is_semantic_role_element(current_role, node.name, attribute_map) ) { const role = roles_map.get(current_role); if (role) { const required_role_props = Object.keys(role.requiredProps); const has_missing_props = !has_spread && required_role_props.some((prop) => !attributes.find((a) => a.name === prop)); if (has_missing_props) { w.a11y_role_has_required_aria_props( attribute, current_role, list( required_role_props.map((v) => `"${v}"`), 'and' ) ); } } } // interactive-supports-focus if ( !has_spread && !has_disabled_attribute(attribute_map) && !is_hidden_from_screen_reader(node.name, attribute_map) && !is_presentation_role(current_role) && is_interactive_roles(current_role) && is_static_element(node.name, attribute_map) && !attribute_map.get('tabindex') ) { const has_interactive_handlers = [...handlers].some((handler) => a11y_interactive_handlers.includes(handler) ); if (has_interactive_handlers) { w.a11y_interactive_supports_focus(node, current_role); } } // no-interactive-element-to-noninteractive-role if ( !has_spread && is_interactive_element(node.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role)) ) { w.a11y_no_interactive_element_to_noninteractive_role(node, node.name, current_role); } // no-noninteractive-element-to-interactive-role if ( !has_spread && is_non_interactive_element(node.name, attribute_map) && is_interactive_roles(current_role) && !a11y_non_interactive_element_to_interactive_role_exceptions[node.name]?.includes( current_role ) ) { w.a11y_no_noninteractive_element_to_interactive_role(node, node.name, current_role); } } break; } // no-access-key case 'accesskey': { w.a11y_accesskey(attribute); break; } // no-autofocus case 'autofocus': { if (node.name !== 'dialog' && !is_parent(context.path, ['dialog'])) { w.a11y_autofocus(attribute); } break; } // scope case 'scope': { if (!is_dynamic_element && node.name !== 'th') { w.a11y_misplaced_scope(attribute); } break; } // tabindex-no-positive case 'tabindex': { const value = get_static_value(attribute); // @ts-ignore todo is tabindex=true correct case? if (!isNaN(value) && +value > 0) { w.a11y_positive_tabindex(attribute); } break; } } } const role = attribute_map.get('role'); const role_static_value = /** @type {ARIARoleDefinitionKey} */ (get_static_text_value(role)); // click-events-have-key-events if (handlers.has('click')) { const is_non_presentation_role = role_static_value !== null && !is_presentation_role(role_static_value); if ( !is_dynamic_element && !is_hidden_from_screen_reader(node.name, attribute_map) && (!role || is_non_presentation_role) && !is_interactive_element(node.name, attribute_map) && !has_spread ) { const has_key_event = handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress'); if (!has_key_event) { w.a11y_click_events_have_key_events(node); } } } const role_value = /** @type {ARIARoleDefinitionKey} */ ( role ? role_static_value : get_implicit_role(node.name, attribute_map) ); // no-noninteractive-tabindex if ( !is_dynamic_element && !is_interactive_element(node.name, attribute_map) && !is_interactive_roles(role_static_value) ) { const tab_index = attribute_map.get('tabindex'); const tab_index_value = get_static_text_value(tab_index); if (tab_index && (tab_index_value === null || Number(tab_index_value) >= 0)) { w.a11y_no_noninteractive_tabindex(node); } } // role-supports-aria-props if (typeof role_value === 'string' && roles_map.has(role_value)) { const { props } = /** @type {ARIARoleDefinition} */ (roles_map.get(role_value)); const invalid_aria_props = aria.keys().filter((attribute) => !(attribute in props)); const is_implicit = role_value && role === undefined; for (const attr of attributes) { if (invalid_aria_props.includes(/** @type {ARIAProperty} */ (attr.name))) { if (is_implicit) { w.a11y_role_supports_aria_props_implicit(attr, attr.name, role_value, node.name); } else { w.a11y_role_supports_aria_props(attr, attr.name, role_value); } } } } // no-noninteractive-element-interactions if ( !has_spread && !has_contenteditable_attr && !is_hidden_from_screen_reader(node.name, attribute_map) && !is_presentation_role(role_static_value) && ((!is_interactive_element(node.name, attribute_map) && is_non_interactive_roles(role_static_value)) || (is_non_interactive_element(node.name, attribute_map) && !role)) ) { const has_interactive_handlers = [...handlers].some((handler) => a11y_recommended_interactive_handlers.includes(handler) ); if (has_interactive_handlers) { w.a11y_no_noninteractive_element_interactions(node, node.name); } } // no-static-element-interactions if ( !has_spread && (!role || role_static_value !== null) && !is_hidden_from_screen_reader(node.name, attribute_map) && !is_presentation_role(role_static_value) && !is_interactive_element(node.name, attribute_map) && !is_interactive_roles(role_static_value) && !is_non_interactive_element(node.name, attribute_map) && !is_non_interactive_roles(role_static_value) && !is_abstract_role(role_static_value) ) { const interactive_handlers = [...handlers].filter((handler) => a11y_interactive_handlers.includes(handler) ); if (interactive_handlers.length > 0) { w.a11y_no_static_element_interactions(node, node.name, list(interactive_handlers)); } } if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) { w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus'); } if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) { w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur'); } // element-specific checks const is_labelled = attribute_map.has('aria-label') || attribute_map.has('aria-labelledby'); switch (node.name) { case 'a': case 'button': { const is_hidden = get_static_value(attribute_map.get('aria-hidden')) === 'true' || get_static_value(attribute_map.get('inert')) !== null; if (!has_spread && !is_hidden && !is_labelled && !has_content(node)) { w.a11y_consider_explicit_label(node); } if (node.name === 'button') { break; } const href = attribute_map.get('href') || attribute_map.get('xlink:href'); if (href) { const href_value = get_static_text_value(href); if (href_value !== null) { if (href_value === '' || href_value === '#' || regex_js_prefix.test(href_value)) { w.a11y_invalid_attribute(href, href_value, href.name); } } } else if (!has_spread) { const id_attribute = get_static_value(attribute_map.get('id')); const name_attribute = get_static_value(attribute_map.get('name')); const aria_disabled_attribute = get_static_value(attribute_map.get('aria-disabled')); if (!id_attribute && !name_attribute && aria_disabled_attribute !== 'true') { warn_missing_attribute(node, ['href']); } } break; } case 'input': { const type = attribute_map.get('type'); const type_value = get_static_text_value(type); if (type_value === 'image' && !has_spread) { const required_attributes = ['alt', 'aria-label', 'aria-labelledby']; const has_attribute = required_attributes.some((name) => attribute_map.has(name)); if (!has_attribute) { warn_missing_attribute(node, required_attributes, 'input type="image"'); } } // autocomplete-valid const autocomplete = attribute_map.get('autocomplete'); if (type && autocomplete) { const autocomplete_value = get_static_value(autocomplete); if (!is_valid_autocomplete(autocomplete_value)) { w.a11y_autocomplete_valid( autocomplete, /** @type {string} */ (autocomplete_value), type_value ?? '...' ); } } break; } case 'img': { const alt_attribute = get_static_text_value(attribute_map.get('alt')); const aria_hidden = get_static_value(attribute_map.get('aria-hidden')); if (alt_attribute && !aria_hidden && !has_spread) { if (regex_redundant_img_alt.test(alt_attribute)) { w.a11y_img_redundant_alt(node); } } break; } case 'label': { /** @param {AST.TemplateNode} node */ const has_input_child = (node) => { let has = false; walk( node, {}, { _(node, { next }) { if ( node.type === 'SvelteElement' || node.type === 'SlotElement' || node.type === 'Component' || node.type === 'RenderTag' || (node.type === 'RegularElement' && (a11y_labelable.includes(node.name) || node.name === 'slot')) ) { has = true; } else { next(); } } } ); return has; }; if (!has_spread && !attribute_map.has('for') && !has_input_child(node)) { w.a11y_label_has_associated_control(node); } break; } case 'video': { const aria_hidden_attribute = attribute_map.get('aria-hidden'); const aria_hidden_exist = aria_hidden_attribute && get_static_value(aria_hidden_attribute); if (attribute_map.has('muted') || aria_hidden_exist === 'true' || has_spread) { return; } let has_caption = false; const track = /** @type {AST.RegularElement | undefined} */ ( node.fragment.nodes.find((i) => i.type === 'RegularElement' && i.name === 'track') ); if (track) { has_caption = track.attributes.some( (a) => a.type === 'SpreadAttribute' || (a.type === 'Attribute' && a.name === 'kind' && get_static_value(a) === 'captions') ); } if (!has_caption) { w.a11y_media_has_caption(node); } break; } case 'figcaption': { if (!is_parent(context.path, ['figure'])) { w.a11y_figcaption_parent(node); } break; } case 'figure': { const children = node.fragment.nodes.filter((node) => { if (node.type === 'Comment') return false; if (node.type === 'Text') return regex_not_whitespace.test(node.data); return true; }); const index = children.findIndex( (child) => child.type === 'RegularElement' && child.name === 'figcaption' ); if (index !== -1 && index !== 0 && index !== children.length - 1) { w.a11y_figcaption_index(children[index]); } break; } } if (!has_spread && node.name !== 'a') { const required_attributes = a11y_required_attributes[node.name]; if (required_attributes) { const has_attribute = required_attributes.some((name) => attribute_map.has(name)); if (!has_attribute) { warn_missing_attribute(node, required_attributes); } } } if (a11y_distracting_elements.includes(node.name)) { // no-distracting-elements w.a11y_distracting_elements(node, node.name); } // Check content if ( !has_spread && !is_labelled && !has_contenteditable_binding && a11y_required_content.includes(node.name) && !has_content(node) ) { w.a11y_missing_content(node, node.name); } } /** * @param {ARIARoleDefinitionKey} role */ function is_presentation_role(role) { return presentation_roles.includes(role); } /** * @param {string} tag_name * @param {Map<string, AST.Attribute>} attribute_map */ function is_hidden_from_screen_reader(tag_name, attribute_map) { if (tag_name === 'input') { const type = get_static_value(attribute_map.get('type')); if (type === 'hidden') { return true; } } const aria_hidden = attribute_map.get('aria-hidden'); if (!aria_hidden) return false; const aria_hidden_value = get_static_value(aria_hidden); if (aria_hidden_value === null) return true; return aria_hidden_value === true || aria_hidden_value === 'true'; } /** * @param {Map<string, AST.Attribute>} attribute_map */ function has_disabled_attribute(attribute_map) { const disabled_attr_value = get_static_value(attribute_map.get('disabled')); if (disabled_attr_value) { return true; } const aria_disabled_attr = attribute_map.get('aria-disabled'); if (aria_disabled_attr) { const aria_disabled_attr_value = get_static_value(aria_disabled_attr); if (aria_disabled_attr_value === 'true') { return true; } } return false; } /** * @param {string} tag_name * @param {Map<string, AST.Attribute>} attribute_map * @returns {ElementInteractivity[keyof ElementInteractivity]} */ function element_interactivity(tag_name, attribute_map) { if ( interactive_element_role_schemas.some((schema) => match_schema(schema, tag_name, attribute_map)) ) { return ElementInteractivity.Interactive; } if ( tag_name !== 'header' && non_interactive_element_role_schemas.some((schema) => match_schema(schema, tag_name, attribute_map) ) ) { return ElementInteractivity.NonInteractive; } if ( interactive_element_ax_object_schemas.some((schema) => match_schema(schema, tag_name, attribute_map) ) ) { return ElementInteractivity.Interactive; } if ( non_interactive_element_ax_object_schemas.some((schema) => match_schema(schema, tag_name, attribute_map) ) ) { return ElementInteractivity.NonInteractive; } return ElementInteractivity.Static; } /** * @param {string} tag_name * @param {Map<string, AST.Attribute>} attribute_map * @returns {boolean} */ function is_interactive_element(tag_name, attribute_map) { return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Interactive; } /** * @param {string} tag_name * @param {Map<string, AST.Attribute>} attribute_map * @returns {boolean} */ function is_non_interactive_element(tag_name, attribute_map) { return element_interactivity(tag_name, attribute_map) === ElementInteractivity.NonInteractive; } /** * @param {string} tag_name * @param {Map<string, AST.Attribute>} attribute_map * @returns {boolean} */ function is_static_element(tag_name, attribute_map) { return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Static; } /** * @param {ARIARoleDefinitionKey} role * @param {string} tag_name * @param {Map<string, AST.Attribute>} attribute_map */ function is_semantic_role_element(role, tag_name, attribute_map) { for (const [schema, ax_object] of elementAXObjects.entries()) { if ( schema.name === tag_name && (!schema.attributes || schema.attributes.every( /** @param {any} attr */ (attr) => attribute_map.has(attr.name) && get_static_value(attribute_map.get(attr.name)) === attr.value )) ) { for (const name of ax_object) { const roles = AXObjectRoles.get(name); if (roles) { for (const { name } of roles) { if (name === role) { return true; } } } } } } return false; } /** * @param {null | true | string} autocomplete */ function is_valid_autocomplete(autocomplete) { if (autocomplete === true) { return false; } else if (!autocomplete) { return true; // dynamic value } const tokens = autocomplete.trim().toLowerCase().split(regex_whitespaces); if (typeof tokens[0] === 'string' && tokens[0].startsWith('section-')) { tokens.shift(); } if (address_type_tokens.includes(tokens[0])) { tokens.shift(); } if (autofill_field_name_tokens.includes(tokens[0])) { tokens.shift(); } else { if (contact_type_tokens.includes(tokens[0])) { tokens.shift(); } if (autofill_contact_field_name_tokens.includes(tokens[0])) { tokens.shift(); } else { return false; } } if (tokens[0] === 'webauthn') { tokens.shift(); } return tokens.length === 0; } /** @param {Map<string, AST.Attribute>} attribute_map */ function input_implicit_role(attribute_map) { const type_attribute = attribute_map.get('type'); if (!type_attribute) return; const type = get_static_text_value(type_attribute); if (!type) return; const list_attribute_exists = attribute_map.has('list'); if (list_attribute_exists && combobox_if_list.includes(type)) { return 'combobox'; } return input_type_to_implicit_role.get(type); } /** @param {Map<string, AST.Attribute>} attribute_map */ function menuitem_implicit_role(attribute_map) { const type_attribute = attribute_map.get('type'); if (!type_attribute) return; const type = get_static_text_value(type_attribute); if (!type) return; return menuitem_type_to_implicit_role.get(type); } /** * @param {string} name * @param {Map<string, AST.Attribute>} attribute_map */ function get_implicit_role(name, attribute_map) { if (name === 'menuitem') { return menuitem_implicit_role(attribute_map); } else if (name === 'input') { return input_implicit_role(attribute_map); } else { return a11y_implicit_semantics.get(name); } } /** * @param {ARIARoleDefinitionKey} role */ function is_non_interactive_roles(role) { return non_interactive_roles.includes(role); } /** * @param {ARIARoleDefinitionKey} role */ function is_interactive_roles(role) { return interactive_roles.includes(role); } /** * @param {ARIARoleDefinitionKey} role */ function is_abstract_role(role) { return abstract_roles.includes(role); } /** * @param {AST.Attribute | undefined} attribute */ function get_static_text_value(attribute) { const value = get_static_value(attribute); if (value === true) return null; return value; } /** * @param {AST.Attribute | undefined} attribute */ function get_static_value(attribute) { if (!attribute) return null; if (attribute.value === true) return true; if (is_text_attribute(attribute)) return attribute.value[0].data; return null; } /** * @param {AST.RegularElement | AST.SvelteElement} element */ function has_content(element) { for (const node of element.fragment.nodes) { if (node.type === 'Text') { if (node.data.trim() === '') { continue; } } if (node.type === 'RegularElement' || node.type === 'SvelteElement') { if ( node.name === 'img' && node.attributes.some((node) => node.type === 'Attribute' && node.name === 'alt') ) { return true; } if (!has_content(node)) { continue; } } // assume everything else has content — this will result in false positives // (e.g. an empty `{#if ...}{/if}`) but that's probably fine return true; } } /** * @param {ARIARoleRelationConcept} schema * @param {string} tag_name * @param {Map<string, AST.Attribute>} attribute_map */ function match_schema(schema, tag_name, attribute_map) { if (schema.name !== tag_name) return false; if (!schema.attributes) return true; return schema.attributes.every((schema_attribute) => { const attribute = attribute_map.get(schema_attribute.name); if (!attribute) return false; if (schema_attribute.value && schema_attribute.value !== get_static_text_value(attribute)) { return false; } return true; }); } /** * @param {AST.SvelteNode[]} path * @param {string[]} elements */ function is_parent(path, elements) { let i = path.length; while (i--) { const parent = path[i]; if (parent.type === 'SvelteElement') return true; // unknown, play it safe, so we don't warn if (parent.type === 'RegularElement') { return elements.includes(parent.name); } } return false; } /** * @param {AST.Attribute} attribute * @param {ARIAProperty} name * @param {ARIAPropertyDefinition} schema * @param {string | true | null} value */ function validate_aria_attribute_value(attribute, name, schema, value) { const type = schema.type; if (value === null) return; if (value === true) value = ''; switch (type) { case 'id': case 'string': { if (value === '') { w.a11y_incorrect_aria_attribute_type(attribute, name, 'non-empty string'); } break; } case 'number': { if (value === '' || isNaN(+value)) { w.a11y_incorrect_aria_attribute_type(attribute, name, 'number'); } break; } case 'boolean': { if (value !== 'true' && value !== 'false') { w.a11y_incorrect_aria_attribute_type_boolean(attribute, name); } break; } case 'idlist': { if (value === '') { w.a11y_incorrect_aria_attribute_type_idlist(attribute, name); } break; } case 'integer': { if (value === '' || !Number.isInteger(+value)) { w.a11y_incorrect_aria_attribute_type_integer(attribute, name); } break; } case 'token': { const values = (schema.values ?? []).map((value) => value.toString()); if (!values.includes(value.toLowerCase())) { w.a11y_incorrect_aria_attribute_type_token( attribute, name, list(values.map((v) => `"${v}"`)) ); } break; } case 'tokenlist': { const values = (schema.values ?? []).map((value) => value.toString()); if ( value .toLowerCase() .split(regex_whitespaces) .some((value) => !values.includes(value)) ) { w.a11y_incorrect_aria_attribute_type_tokenlist( attribute, name, list(values.map((v) => `"${v}"`)) ); } break; } case 'tristate': { if (value !== 'true' && value !== 'false' && value !== 'mixed') { w.a11y_incorrect_aria_attribute_type_tristate(attribute, name); } break; } } } /** * @param {AST.RegularElement |AST.SvelteElement} node * @param {string[]} attributes * @param {string} name */ function warn_missing_attribute(node, attributes, name = node.name) { const article = regex_starts_with_vowel.test(attributes[0]) || attributes[0] === 'href' ? 'an' : 'a'; const sequence = attributes.length > 1 ? attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` : attributes[0]; w.a11y_missing_attribute(node, name, article, sequence); }