import { is_html, is_svg, is_void } from '../../../shared/utils/names.js';
import Node from './shared/Node.js';
import { walk } from 'estree-walker';
import Attribute from './Attribute.js';
import Binding from './Binding.js';
import EventHandler from './EventHandler.js';
import Transition from './Transition.js';
import Animation from './Animation.js';
import Action from './Action.js';
import Class from './Class.js';
import StyleDirective from './StyleDirective.js';
import Text from './Text.js';
import { namespaces } from '../../utils/namespaces.js';
import map_children from './shared/map_children.js';
import {
is_name_contenteditable,
get_contenteditable_attr,
has_contenteditable_attr
} from '../utils/contenteditable.js';
import {
regex_dimensions,
regex_starts_with_newline,
regex_non_whitespace_character,
regex_box_size
} from '../../utils/patterns.js';
import fuzzymatch from '../../utils/fuzzymatch.js';
import list from '../../utils/list.js';
import hash from '../utils/hash.js';
import Let from './Let.js';
import Expression from './shared/Expression.js';
import { string_literal } from '../utils/stringify.js';
import compiler_warnings from '../compiler_warnings.js';
import compiler_errors from '../compiler_errors.js';
import { roles, aria } from 'aria-query';
import {
is_interactive_element,
is_non_interactive_element,
is_non_interactive_roles,
is_presentation_role,
is_interactive_roles,
is_hidden_from_screen_reader,
is_semantic_role_element,
is_abstract_role,
is_static_element,
has_disabled_attribute,
is_valid_autocomplete
} from '../utils/a11y.js';
const aria_attributes =
'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(
' '
);
const aria_attribute_set = new Set(aria_attributes);
const aria_roles = roles.keys();
const aria_role_set = new Set(aria_roles);
const a11y_required_attributes = {
a: ['href'],
area: ['alt', 'aria-label', 'aria-labelledby'],
// html-has-lang
html: ['lang'],
// iframe-has-title
iframe: ['title'],
img: ['alt'],
object: ['title', 'aria-label', 'aria-labelledby']
};
const a11y_distracting_elements = new Set(['blink', 'marquee']);
const a11y_required_content = new Set([
// anchor-has-content
'a',
// heading-has-content
'h1',
'h2',
'h3',
'h4',
'h5',
'h6'
]);
const a11y_labelable = new Set([
'button',
'input',
'keygen',
'meter',
'output',
'progress',
'select',
'textarea'
]);
const a11y_interactive_handlers = new Set([
// Keyboard events
'keypress',
'keydown',
'keyup',
// Click events
'click',
'contextmenu',
'dblclick',
'drag',
'dragend',
'dragenter',
'dragexit',
'dragleave',
'dragover',
'dragstart',
'drop',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup'
]);
const a11y_recommended_interactive_handlers = new Set([
'click',
'mousedown',
'mouseup',
'keypress',
'keydown',
'keyup'
]);
const a11y_nested_implicit_semantics = new Map([
['header', 'banner'],
['footer', 'contentinfo']
]);
const a11y_implicit_semantics = new Map([
['a', 'link'],
['area', 'link'],
['article', 'article'],
['aside', 'complementary'],
['body', 'document'],
['button', 'button'],
['datalist', 'listbox'],
['dd', 'definition'],
['dfn', 'term'],
['dialog', 'dialog'],
['details', 'group'],
['dt', 'term'],
['fieldset', 'group'],
['figure', 'figure'],
['form', 'form'],
['h1', 'heading'],
['h2', 'heading'],
['h3', 'heading'],
['h4', 'heading'],
['h5', 'heading'],
['h6', 'heading'],
['hr', 'separator'],
['img', 'img'],
['li', 'listitem'],
['link', 'link'],
['main', 'main'],
['menu', 'list'],
['meter', 'progressbar'],
['nav', 'navigation'],
['ol', 'list'],
['option', 'option'],
['optgroup', 'group'],
['output', 'status'],
['progress', 'progressbar'],
['section', 'region'],
['summary', 'button'],
['table', 'table'],
['tbody', 'rowgroup'],
['textarea', 'textbox'],
['tfoot', 'rowgroup'],
['thead', 'rowgroup'],
['tr', 'row'],
['ul', 'list']
]);
const menuitem_type_to_implicit_role = new Map([
['command', 'menuitem'],
['checkbox', 'menuitemcheckbox'],
['radio', 'menuitemradio']
]);
const input_type_to_implicit_role = new Map([
['button', 'button'],
['image', 'button'],
['reset', 'button'],
['submit', 'button'],
['checkbox', 'checkbox'],
['radio', 'radio'],
['range', 'slider'],
['number', 'spinbutton'],
['email', 'textbox'],
['search', 'searchbox'],
['tel', 'textbox'],
['text', 'textbox'],
['url', 'textbox']
]);
/**
* Exceptions to the rule which follows common A11y conventions
* TODO make this configurable by the user
*/
const a11y_non_interactive_element_to_interactive_role_exceptions = {
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],
fieldset: ['radiogroup', 'presentation']
};
const combobox_if_list = new Set(['email', 'search', 'tel', 'text', 'url']);
/** @param {Map} attribute_map */
function input_implicit_role(attribute_map) {
const type_attribute = attribute_map.get('type');
if (!type_attribute || !type_attribute.is_static) return;
const type = /** @type {string} */ (type_attribute.get_static_value());
const list_attribute_exists = attribute_map.has('list');
if (list_attribute_exists && combobox_if_list.has(type)) {
return 'combobox';
}
return input_type_to_implicit_role.get(type);
}
/** @param {Map} attribute_map */
function menuitem_implicit_role(attribute_map) {
const type_attribute = attribute_map.get('type');
if (!type_attribute || !type_attribute.is_static) return;
const type = /** @type {string} */ (type_attribute.get_static_value());
return menuitem_type_to_implicit_role.get(type);
}
/**
* @param {string} name
* @param {Map} attribute_map
* @returns {string}
*/
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);
}
}
const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
const valid_modifiers = new Set([
'preventDefault',
'stopPropagation',
'stopImmediatePropagation',
'capture',
'once',
'passive',
'nonpassive',
'self',
'trusted'
]);
const passive_events = new Set(['wheel', 'touchstart', 'touchmove', 'touchend', 'touchcancel']);
const react_attributes = new Map([
['className', 'class'],
['htmlFor', 'for']
]);
const attributes_to_compact_whitespace = ['class', 'style'];
/**
* @param {import('./interfaces.js').INode} parent
* @param {string[]} elements
*/
function is_parent(parent, elements) {
let check = false;
while (parent) {
const parent_name = /** @type {Element} */ (parent).name;
if (elements.includes(parent_name)) {
check = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
return check;
}
/**
* @param {Element} parent
* @param {Element} element
* @param {string} explicit_namespace
*/
function get_namespace(parent, element, explicit_namespace) {
const parent_element = parent.find_nearest(/^Element/);
if (!parent_element) {
return explicit_namespace || (is_svg(element.name) ? namespaces.svg : null);
}
if (parent_element.namespace !== namespaces.foreign) {
if (is_svg(element.name.toLowerCase())) return namespaces.svg;
if (parent_element.name.toLowerCase() === 'foreignobject') return null;
}
return parent_element.namespace;
}
/**
* @param {import('aria-query').ARIAPropertyDefinition} schema
* @param {string | boolean} value
* @returns {boolean}
*/
function is_valid_aria_attribute_value(schema, value) {
switch (schema.type) {
case 'boolean':
return typeof value === 'boolean';
case 'string':
case 'id':
return typeof value === 'string';
case 'tristate':
return typeof value === 'boolean' || value === 'mixed';
case 'integer':
case 'number':
return typeof value !== 'boolean' && isNaN(Number(value)) === false;
case 'token': // single token
return (
(schema.values || []).indexOf(typeof value === 'string' ? value.toLowerCase() : value) > -1
);
case 'idlist': // if list of ids, split each
return (
typeof value === 'string' &&
value.split(regex_any_repeated_whitespaces).every((id) => typeof id === 'string')
);
case 'tokenlist': // if list of tokens, split each
return (
typeof value === 'string' &&
value
.split(regex_any_repeated_whitespaces)
.every((token) => (schema.values || []).indexOf(token.toLowerCase()) > -1)
);
default:
return false;
}
}
const regex_any_repeated_whitespaces = /[\s]+/g;
const regex_heading_tags = /^h[1-6]$/;
const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%?!|()[\]{}^*+~;]/;
/** @extends Node<'Element'> */
export default class Element extends Node {
/** @type {string} */
name;
/** @type {import('./shared/TemplateScope.js').default} */
scope;
/** @type {import('./Action.js').default[]} */
actions = [];
/** @type {import('./Binding.js').default[]} */
bindings = [];
/** @type {import('./Class.js').default[]} */
classes = [];
/** @type {import('./StyleDirective.js').default[]} */
styles = [];
/** @type {import('./EventHandler.js').default[]} */
handlers = [];
/** @type {import('./Let.js').default[]} */
lets = [];
/** @type {import('./Transition.js').default} */
intro = null;
/** @type {import('./Transition.js').default} */
outro = null;
/** @type {import('./Animation.js').default} */
animation = null;
/** @type {import('./interfaces.js').INode[]} */
children;
/** @type {string} */
namespace;
/** @type {boolean} */
needs_manual_style_scoping;
/** @type {import('./shared/Expression.js').default} */
tag_expr;
/** @type {boolean} */
contains_a11y_label;
get is_dynamic_element() {
return this.name === 'svelte:element';
}
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {any} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
if (info.name === 'svelte:element') {
if (typeof info.tag !== 'string') {
this.tag_expr = new Expression(component, this, scope, info.tag);
} else {
this.tag_expr = new Expression(
component,
this,
scope,
/** @type {import('estree').Literal} */ (string_literal(info.tag))
);
this.name = info.tag;
}
} else {
this.tag_expr = new Expression(
component,
this,
scope,
/** @type {import('estree').Literal} */ (string_literal(this.name))
);
}
this.namespace = get_namespace(/** @type {Element} */ (parent), this, component.namespace);
if (this.namespace !== namespaces.foreign) {
if (this.name === 'pre' || this.name === 'textarea') {
const first = info.children[0];
if (first && first.type === 'Text') {
// The leading newline character needs to be stripped because of a quirk,
// it is ignored by browsers if the tag and its contents are set through
// innerHTML (NOT if set through the innerHTML of the tag or dynamically).
// Therefore strip it here but add it back in the appropriate
// places if there's another newline afterwards.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
first.data = first.data.replace(regex_starts_with_newline, '');
}
}
if (this.name === 'textarea') {
if (info.children.length > 0) {
const value_attribute = get_value_attribute(info.attributes);
if (value_attribute) {
component.error(value_attribute, compiler_errors.textarea_duplicate_value);
return;
}
// this is an egregious hack, but it's the easiest way to get