svelte
Version:
Cybernetically enhanced web apps
197 lines (171 loc) • 6.09 kB
JavaScript
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_mathml, is_svg, is_void } from '../../../../utils.js';
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_attribute, is_custom_element_node } from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.RegularElement} node
* @param {Context} context
*/
export function RegularElement(node, context) {
validate_element(node, context);
check_element(node, context);
node.metadata.path = [...context.path];
context.state.analysis.elements.push(node);
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
if (node.name === 'textarea' && node.fragment.nodes.length > 0) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute' && attribute.name === 'value') {
e.textarea_invalid_content(node);
}
}
if (node.fragment.nodes.length > 1 || node.fragment.nodes[0].type !== 'Text') {
const first = node.fragment.nodes[0];
if (first.type === 'Text') {
// The leading newline character needs to be stripped because of a qirk:
// It is ignored by browsers if the tag and its contents are set through
// innerHTML, but we're now setting it through the value property at which
// point it is _not_ ignored, so we need to strip it ourselves.
// 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, '');
first.raw = first.raw.replace(regex_starts_with_newline, '');
}
node.attributes.push(
create_attribute(
'value',
/** @type {AST.Text} */ (node.fragment.nodes.at(0)).start,
/** @type {AST.Text} */ (node.fragment.nodes.at(-1)).end,
// @ts-ignore
node.fragment.nodes
)
);
node.fragment.nodes = [];
}
}
// Special case: single expression tag child of option element -> add "fake" attribute
// to ensure that value types are the same (else for example numbers would be strings)
if (
node.name === 'option' &&
node.fragment.nodes?.length === 1 &&
node.fragment.nodes[0].type === 'ExpressionTag' &&
!node.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'value'
)
) {
const child = node.fragment.nodes[0];
node.attributes.push(create_attribute('value', child.start, child.end, [child]));
}
const binding = context.state.scope.get(node.name);
if (
binding !== null &&
binding.declaration_kind === 'import' &&
binding.references.length === 0
) {
w.component_name_lowercase(node, node.name);
}
node.metadata.has_spread = node.attributes.some(
(attribute) => attribute.type === 'SpreadAttribute'
);
const is_svg_element = () => {
if (is_svg(node.name)) {
return true;
}
if (node.name === 'a' || node.name === 'title') {
let i = context.path.length;
while (i--) {
const ancestor = context.path[i];
if (ancestor.type === 'RegularElement') {
return ancestor.metadata.svg;
}
}
}
return false;
};
node.metadata.svg = is_svg_element();
node.metadata.mathml = is_mathml(node.name);
if (is_custom_element_node(node) && node.attributes.length > 0) {
// we're setting all attributes on custom elements through properties
mark_subtree_dynamic(context.path);
}
if (context.state.parent_element) {
let past_parent = false;
let only_warn = false;
const ancestors = [context.state.parent_element];
for (let i = context.path.length - 1; i >= 0; i--) {
const ancestor = context.path[i];
if (
ancestor.type === 'IfBlock' ||
ancestor.type === 'EachBlock' ||
ancestor.type === 'AwaitBlock' ||
ancestor.type === 'KeyBlock'
) {
// We're creating a separate template string inside blocks, which means client-side this would work
only_warn = true;
}
if (!past_parent) {
if (ancestor.type === 'RegularElement' && ancestor.name === context.state.parent_element) {
const message = is_tag_valid_with_parent(node.name, context.state.parent_element);
if (message) {
if (only_warn) {
w.node_invalid_placement_ssr(node, message);
} else {
e.node_invalid_placement(node, message);
}
}
past_parent = true;
}
} else if (ancestor.type === 'RegularElement') {
ancestors.push(ancestor.name);
const message = is_tag_valid_with_ancestor(node.name, ancestors);
if (message) {
if (only_warn) {
w.node_invalid_placement_ssr(node, message);
} else {
e.node_invalid_placement(node, message);
}
}
} else if (
ancestor.type === 'Component' ||
ancestor.type === 'SvelteComponent' ||
ancestor.type === 'SvelteElement' ||
ancestor.type === 'SvelteSelf' ||
ancestor.type === 'SnippetBlock'
) {
break;
}
}
}
// Strip off any namespace from the beginning of the node name.
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
if (
context.state.analysis.source[node.end - 2] === '/' &&
!is_void(node_name) &&
!is_svg(node_name) &&
!is_mathml(node_name)
) {
w.element_invalid_self_closing_tag(node, node.name);
}
context.next({ ...context.state, parent_element: node.name });
// Special case: <a> tags are valid in both the SVG and HTML namespace.
// If there's no parent, look downwards to see if it's the parent of a SVG or HTML element.
if (node.name === 'a' && !context.state.parent_element) {
for (const child of node.fragment.nodes) {
if (child.type === 'RegularElement') {
if (child.metadata.svg && child.name !== 'svg') {
node.metadata.svg = true;
break;
}
}
}
}
}