put the exported function up top

pull/16345/head
Rich Harris 2 months ago
parent e68d49f28a
commit 21ed6c1dea

@ -49,286 +49,6 @@ import fuzzymatch from '../../../../1-parse/utils/fuzzymatch.js';
import { is_content_editable_binding } from '../../../../../../utils.js'; import { is_content_editable_binding } from '../../../../../../utils.js';
import * as w from '../../../../../warnings.js'; import * as w from '../../../../../warnings.js';
/**
* @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 {AST.RegularElement | AST.SvelteElement} node * @param {AST.RegularElement | AST.SvelteElement} node
* @param {Context} context * @param {Context} context
@ -682,149 +402,429 @@ export function check_element(node, context) {
w.a11y_invalid_attribute(href, href_value, href.name); w.a11y_invalid_attribute(href, href_value, href.name);
} }
} }
} else if (!has_spread) { } else if (!has_spread) {
const id_attribute = get_static_value(attribute_map.get('id')); const id_attribute = get_static_value(attribute_map.get('id'));
const name_attribute = get_static_value(attribute_map.get('name')); const name_attribute = get_static_value(attribute_map.get('name'));
const aria_disabled_attribute = get_static_value(attribute_map.get('aria-disabled')); const aria_disabled_attribute = get_static_value(attribute_map.get('aria-disabled'));
if (!id_attribute && !name_attribute && aria_disabled_attribute !== 'true') { if (!id_attribute && !name_attribute && aria_disabled_attribute !== 'true') {
warn_missing_attribute(node, ['href']); 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;
} }
} }
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 ?? '...'
);
} }
return false;
} }
break;
/**
* @param {null | true | string} autocomplete
*/
function is_valid_autocomplete(autocomplete) {
if (autocomplete === true) {
return false;
} else if (!autocomplete) {
return true; // dynamic value
} }
case 'img': { const tokens = autocomplete.trim().toLowerCase().split(regex_whitespaces);
const alt_attribute = get_static_text_value(attribute_map.get('alt')); if (typeof tokens[0] === 'string' && tokens[0].startsWith('section-')) {
const aria_hidden = get_static_value(attribute_map.get('aria-hidden')); tokens.shift();
if (alt_attribute && !aria_hidden && !has_spread) {
if (regex_redundant_img_alt.test(alt_attribute)) {
w.a11y_img_redundant_alt(node);
} }
if (address_type_tokens.includes(tokens[0])) {
tokens.shift();
} }
break; if (autofill_field_name_tokens.includes(tokens[0])) {
tokens.shift();
} else {
if (contact_type_tokens.includes(tokens[0])) {
tokens.shift();
} }
case 'label': { if (autofill_contact_field_name_tokens.includes(tokens[0])) {
/** @param {AST.TemplateNode} node */ tokens.shift();
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 { } else {
next(); return false;
} }
} }
if (tokens[0] === 'webauthn') {
tokens.shift();
} }
); return tokens.length === 0;
return has;
};
if (!has_spread && !attribute_map.has('for') && !has_input_child(node)) {
w.a11y_label_has_associated_control(node);
} }
break;
/** @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';
} }
case 'video': { return input_type_to_implicit_role.get(type);
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} */ ( /** @param {Map<string, AST.Attribute>} attribute_map */
node.fragment.nodes.find((i) => i.type === 'RegularElement' && i.name === 'track') function menuitem_implicit_role(attribute_map) {
); const type_attribute = attribute_map.get('type');
if (track) { if (!type_attribute) return;
has_caption = track.attributes.some( const type = get_static_text_value(type_attribute);
(a) => if (!type) return;
a.type === 'SpreadAttribute' || return menuitem_type_to_implicit_role.get(type);
(a.type === 'Attribute' && a.name === 'kind' && get_static_value(a) === 'captions')
);
} }
if (!has_caption) {
w.a11y_media_has_caption(node); /**
* @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);
} }
break;
} }
case 'figcaption': {
if (!is_parent(context.path, ['figure'])) { /**
w.a11y_figcaption_parent(node); * @param {ARIARoleDefinitionKey} role
*/
function is_non_interactive_roles(role) {
return non_interactive_roles.includes(role);
} }
break;
/**
* @param {ARIARoleDefinitionKey} role
*/
function is_interactive_roles(role) {
return interactive_roles.includes(role);
} }
case 'figure': {
const children = node.fragment.nodes.filter((node) => { /**
if (node.type === 'Comment') return false; * @param {ARIARoleDefinitionKey} role
if (node.type === 'Text') return regex_not_whitespace.test(node.data); */
return true; function is_abstract_role(role) {
}); return abstract_roles.includes(role);
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;
/**
* @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;
} }
if (!has_spread && node.name !== 'a') { /**
const required_attributes = a11y_required_attributes[node.name]; * @param {AST.RegularElement | AST.SvelteElement} element
if (required_attributes) { */
const has_attribute = required_attributes.some((name) => attribute_map.has(name)); function has_content(element) {
if (!has_attribute) { for (const node of element.fragment.nodes) {
warn_missing_attribute(node, required_attributes); 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 (a11y_distracting_elements.includes(node.name)) { if (!has_content(node)) {
// no-distracting-elements continue;
w.a11y_distracting_elements(node, node.name); }
} }
// Check content // assume everything else has content — this will result in false positives
if ( // (e.g. an empty `{#if ...}{/if}`) but that's probably fine
!has_spread && return true;
!is_labelled &&
!has_contenteditable_binding &&
a11y_required_content.includes(node.name) &&
!has_content(node)
) {
w.a11y_missing_content(node, node.name);
} }
} }

Loading…
Cancel
Save