/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { Parser } from '../index.js' */ import { is_void } from '../../../../utils.js'; import read_expression from '../read/expression.js'; import { read_script } from '../read/script.js'; import read_style from '../read/style.js'; import { decode_character_references } from '../utils/html.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { create_fragment } from '../utils/create.js'; import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js'; import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js'; import { closing_tag_omitted } from '../../../../html-tree-validation.js'; import { list } from '../../../utils/string.js'; import { regex_whitespace } from '../../patterns.js'; const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/; const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i; const regex_closing_comment = /-->/; const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/; const regex_token_ending_character = /[\s=/>"']/; const regex_starts_with_quote_characters = /^["']/; const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/; const regex_valid_element_name = /^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/; export const regex_valid_component_name = // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs // (must start with uppercase letter if no dots, can contain dots) /^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u; /** @type {Map} */ const root_only_meta_tags = new Map([ ['svelte:head', 'SvelteHead'], ['svelte:options', 'SvelteOptions'], ['svelte:window', 'SvelteWindow'], ['svelte:document', 'SvelteDocument'], ['svelte:body', 'SvelteBody'] ]); /** @type {Map} */ const meta_tags = new Map([ ...root_only_meta_tags, ['svelte:element', 'SvelteElement'], ['svelte:component', 'SvelteComponent'], ['svelte:self', 'SvelteSelf'], ['svelte:fragment', 'SvelteFragment'], ['svelte:boundary', 'SvelteBoundary'] ]); /** @param {Parser} parser */ export default function element(parser) { const start = parser.index++; let parent = parser.current(); if (parser.eat('!--')) { const data = parser.read_until(regex_closing_comment); parser.eat('-->', true); parser.append({ type: 'Comment', start, end: parser.index, data }); return; } const is_closing_tag = parser.eat('/'); const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag); if (is_closing_tag) { parser.allow_whitespace(); parser.eat('>', true); if (is_void(name)) { e.void_element_invalid_content(start); } // close any elements that don't have their own closing tags, e.g.

while (/** @type {AST.RegularElement} */ (parent).name !== name) { if (parser.loose) { // If the previous element did interpret the next opening tag as an attribute, backtrack if (is_element_node(parent)) { const last = parent.attributes.at(-1); if (last?.type === 'Attribute' && last.name === `<${name}`) { parser.index = last.start; parent.attributes.pop(); break; } } } if (parent.type !== 'RegularElement' && !parser.loose) { if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) { e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason); } else { e.element_invalid_closing_tag(start, name); } } parent.end = start; parser.pop(); parent = parser.current(); } parent.end = parser.index; parser.pop(); if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) { parser.last_auto_closed_tag = undefined; } return; } if (name.startsWith('svelte:') && !meta_tags.has(name)) { const bounds = { start: start + 1, end: start + 1 + name.length }; e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys()))); } if (!regex_valid_element_name.test(name) && !regex_valid_component_name.test(name)) { // in the middle of typing -> allow in loose mode if (!parser.loose || !name.endsWith('.')) { const bounds = { start: start + 1, end: start + 1 + name.length }; e.tag_invalid_name(bounds); } } if (root_only_meta_tags.has(name)) { if (name in parser.meta_tags) { e.svelte_meta_duplicate(start, name); } if (parent.type !== 'Root') { e.svelte_meta_invalid_placement(start, name); } parser.meta_tags[name] = true; } const type = meta_tags.has(name) ? meta_tags.get(name) : regex_valid_component_name.test(name) || (parser.loose && name.endsWith('.')) ? 'Component' : name === 'title' && parent_is_head(parser.stack) ? 'TitleElement' : // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element name === 'slot' && !parent_is_shadowroot_template(parser.stack) ? 'SlotElement' : 'RegularElement'; /** @type {AST.ElementLike} */ const element = type === 'RegularElement' ? { type, start, end: -1, name, attributes: [], fragment: create_fragment(true), metadata: { svg: false, mathml: false, scoped: false, has_spread: false, path: [] } } : /** @type {AST.ElementLike} */ ({ type, start, end: -1, name, attributes: [], fragment: create_fragment(true), metadata: { // unpopulated at first, differs between types } }); parser.allow_whitespace(); if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) { parent.end = start; parser.pop(); parser.last_auto_closed_tag = { tag: parent.name, reason: name, depth: parser.stack.length }; } /** @type {string[]} */ const unique_names = []; const current = parser.current(); const is_top_level_script_or_style = (name === 'script' || name === 'style') && current.type === 'Root'; const read = is_top_level_script_or_style ? read_static_attribute : read_attribute; let attribute; while ((attribute = read(parser))) { // animate and transition can only be specified once per element so no need // to check here, use can be used multiple times, same for the on directive // finally let already has error handling in case of duplicate variable names if ( attribute.type === 'Attribute' || attribute.type === 'BindDirective' || attribute.type === 'StyleDirective' || attribute.type === 'ClassDirective' ) { // `bind:attribute` and `attribute` are just the same but `class:attribute`, // `style:attribute` and `attribute` are different and should be allowed together // so we concatenate the type while normalizing the type for BindDirective const type = attribute.type === 'BindDirective' ? 'Attribute' : attribute.type; if (unique_names.includes(type + attribute.name)) { e.attribute_duplicate(attribute); // is allowed } else if (attribute.name !== 'this') { unique_names.push(type + attribute.name); } } element.attributes.push(attribute); parser.allow_whitespace(); } if (element.type === 'SvelteComponent') { const index = element.attributes.findIndex( /** @param {any} attr */ (attr) => attr.type === 'Attribute' && attr.name === 'this' ); if (index === -1) { e.svelte_component_missing_this(start); } const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]); if (!is_expression_attribute(definition)) { e.svelte_component_invalid_this(definition.start); } element.expression = get_attribute_expression(definition); } if (element.type === 'SvelteElement') { const index = element.attributes.findIndex( /** @param {any} attr */ (attr) => attr.type === 'Attribute' && attr.name === 'this' ); if (index === -1) { e.svelte_element_missing_this(start); } const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]); if (definition.value === true) { e.svelte_element_missing_this(definition); } if (!is_expression_attribute(definition)) { w.svelte_element_invalid_this(definition); // note that this is wrong, in the case of e.g. `this="h{n}"` — it will result in ``. // it would be much better to just error here, but we are preserving the existing buggy // Svelte 4 behaviour out of an overabundance of caution regarding breaking changes. // TODO in 6.0, error const chunk = /** @type {Array} */ (definition.value)[0]; element.tag = chunk.type === 'Text' ? { type: 'Literal', value: chunk.data, raw: `'${chunk.raw}'`, start: chunk.start, end: chunk.end } : chunk.expression; } else { element.tag = get_attribute_expression(definition); } } if (is_top_level_script_or_style) { parser.eat('>', true); /** @type {AST.Comment | null} */ let prev_comment = null; for (let i = current.fragment.nodes.length - 1; i >= 0; i--) { const node = current.fragment.nodes[i]; if (i === current.fragment.nodes.length - 1 && node.end !== start) { break; } if (node.type === 'Comment') { prev_comment = node; break; } else if (node.type !== 'Text' || node.data.trim()) { break; } } if (name === 'script') { const content = read_script(parser, start, element.attributes); if (prev_comment) { // We take advantage of the fact that the root will never have leadingComments set, // and set the previous comment to it so that the warning mechanism can later // inspect the root and see if there was a html comment before it silencing specific warnings. content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }]; } if (content.context === 'module') { if (current.module) e.script_duplicate(start); current.module = content; } else { if (current.instance) e.script_duplicate(start); current.instance = content; } } else { const content = read_style(parser, start, element.attributes); content.content.comment = prev_comment; if (current.css) e.style_duplicate(start); current.css = content; } return; } parser.append(element); const self_closing = parser.eat('/') || is_void(name); const closed = parser.eat('>', true, false); // Loose parsing mode if (!closed) { // We may have eaten an opening `<` of the next element and treated it as an attribute... const last = element.attributes.at(-1); if (last?.type === 'Attribute' && last.name === '<') { parser.index = last.start; element.attributes.pop(); } else { // ... or we may have eaten part of a following block ... const prev_1 = parser.template[parser.index - 1]; const prev_2 = parser.template[parser.index - 2]; const current = parser.template[parser.index]; if (prev_2 === '{' && prev_1 === '/') { parser.index -= 2; } else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) { parser.index -= 1; } else { // ... or we're followed by whitespace, for example near the end of the template, // which we want to take in so that language tools has more room to work with parser.allow_whitespace(); if (parser.index === parser.template.length) { while ( parser.index < parser.template_untrimmed.length && regex_whitespace.test(parser.template_untrimmed[parser.index]) ) { parser.index++; } } } } } if (self_closing || !closed) { // don't push self-closing elements onto the stack element.end = parser.index; } else if (name === 'textarea') { // special case element.fragment.nodes = read_sequence( parser, () => regex_closing_textarea_tag.test(parser.template.slice(parser.index)), 'inside