mirror of https://github.com/sveltejs/svelte
the template is read by the parser: one call per component, the compiler's shape put on the tree after it, the scope tables cut per expression for the analysis, the parser's errors mapped to ours; phase 1's own reader goes
parent
57c7de58a5
commit
a7f4c5c5bb
@ -0,0 +1,80 @@
|
||||
/** The template language, as the parser reads it: the delimiters, the elements and their fields, the directives and the blocks. */
|
||||
export const grammar = `
|
||||
host svelte
|
||||
|
||||
document Root css=style js=list options=null comments=comments module?=script:module { instance?=script { fragment=fragment } }
|
||||
delimiters { }
|
||||
attributes expressions shorthand
|
||||
sigils open=# branch=: close=/ tag=@
|
||||
autoclose
|
||||
trim
|
||||
void area base br col command embed hr img input keygen link meta param source track wbr
|
||||
fragment Fragment nodes scope
|
||||
elements name=name attributes=attributes children=fragment
|
||||
text Text data=data raw=raw
|
||||
comment Comment data=data
|
||||
|
||||
element svelte:element SvelteElement this=tag:text
|
||||
element svelte:component SvelteComponent this=expression
|
||||
element svelte:self SvelteSelf
|
||||
element svelte:window SvelteWindow root once
|
||||
element svelte:document SvelteDocument root once
|
||||
element svelte:body SvelteBody root once
|
||||
element svelte:head SvelteHead root once
|
||||
element svelte:options SvelteOptions root once
|
||||
element svelte:fragment SvelteFragment
|
||||
element svelte:boundary SvelteBoundary
|
||||
element title TitleElement inside svelte:head
|
||||
element slot SlotElement outside shadowrootmode
|
||||
element textarea RegularElement rcdata
|
||||
element script RegularElement raw
|
||||
element style RegularElement raw
|
||||
element component-name Component
|
||||
element * RegularElement
|
||||
|
||||
script script module=context:module module=module typescript=lang:ts
|
||||
style style
|
||||
|
||||
directives arg=: modifier=| field:arg=name field:modifiers=modifiers
|
||||
directive bind BindDirective expression?name unique:attribute
|
||||
directive on OnDirective expression?
|
||||
directive use UseDirective expression?
|
||||
directive class ClassDirective expression?name unique
|
||||
directive style StyleDirective value unique
|
||||
directive transition TransitionDirective expression? intro outro
|
||||
directive in TransitionDirective expression? intro !outro
|
||||
directive out TransitionDirective expression? !intro outro
|
||||
directive animate AnimateDirective expression?
|
||||
directive let LetDirective pattern?name declares
|
||||
|
||||
spread SpreadAttribute expression
|
||||
|
||||
block if IfBlock chain=elseif
|
||||
open test=expression -> consequent
|
||||
branch else if test=expression -> alternate chain consequent
|
||||
branch else -> alternate
|
||||
|
||||
block each EachBlock
|
||||
open expression=expression [ as context=pattern ] [ , index?=identifier ] [ ( key?=expression ) ] -> body declares context index
|
||||
branch else -> fallback?
|
||||
|
||||
block await AwaitBlock
|
||||
open expression=expression [ then [ value=pattern ] -> then declares value | catch [ error=pattern ] -> catch declares error ] -> pending
|
||||
branch then [ value=pattern ] -> then declares value
|
||||
branch catch [ error=pattern ] -> catch declares error
|
||||
|
||||
block key KeyBlock
|
||||
open expression=expression -> fragment
|
||||
|
||||
block snippet SnippetBlock
|
||||
open expression=identifier [ typeParams?=typeParameters ] parameters=params -> body declares expression:outside parameters
|
||||
|
||||
tag html HtmlTag expression=expression
|
||||
tag debug DebugTag identifiers=identifiers
|
||||
tag const ConstTag declaration=const
|
||||
tag render RenderTag expression=expression
|
||||
tag attach AttachTag expression=expression attribute
|
||||
|
||||
declaration DeclarationTag declaration=statement
|
||||
expression ExpressionTag expression=expression
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,83 +0,0 @@
|
||||
/** @import { Program } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from './index.js' */
|
||||
import { parse_script } from './js.js';
|
||||
import * as e from '../../errors.js';
|
||||
import * as w from '../../warnings.js';
|
||||
import { is_text_attribute } from '../../utils/ast.js';
|
||||
import { locator } from '../../state.js';
|
||||
|
||||
const regex_closing_script_tag = /<\/script\s*>/;
|
||||
const regex_starts_with_closing_script_tag = /<\/script\s*>/y;
|
||||
|
||||
const RESERVED_ATTRIBUTES = ['server', 'client', 'worker', 'test', 'default'];
|
||||
const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} start
|
||||
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
|
||||
* @returns {AST.Script}
|
||||
*/
|
||||
export function read_script(parser, start, attributes) {
|
||||
const script_start = parser.index;
|
||||
parser.read_until_regex(regex_closing_script_tag);
|
||||
if (parser.index >= parser.template.length) {
|
||||
e.element_unclosed(parser.template.length, 'script');
|
||||
}
|
||||
|
||||
const ast = parse_script(parser, script_start, parser.index);
|
||||
parser.read(regex_starts_with_closing_script_tag);
|
||||
|
||||
if (ast.loc) {
|
||||
// the legacy AST places the program at the tag, not at its contents
|
||||
({ line: ast.loc.start.line, column: ast.loc.start.column } = locator(start));
|
||||
({ line: ast.loc.end.line, column: ast.loc.end.column } = locator(parser.index));
|
||||
}
|
||||
|
||||
/** @type {'default' | 'module'} */
|
||||
let context = 'default';
|
||||
|
||||
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
|
||||
if (RESERVED_ATTRIBUTES.includes(attribute.name)) {
|
||||
e.script_reserved_attribute(attribute, attribute.name);
|
||||
}
|
||||
|
||||
if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) {
|
||||
w.script_unknown_attribute(attribute);
|
||||
}
|
||||
|
||||
if (attribute.name === 'module') {
|
||||
if (attribute.value !== true) {
|
||||
// Deliberately a generic code to future-proof for potential other attributes
|
||||
e.script_invalid_attribute_value(attribute, attribute.name);
|
||||
}
|
||||
|
||||
context = 'module';
|
||||
}
|
||||
|
||||
if (attribute.name === 'context') {
|
||||
if (attribute.value === true || !is_text_attribute(attribute)) {
|
||||
e.script_invalid_context(attribute);
|
||||
}
|
||||
|
||||
const value = attribute.value[0].data;
|
||||
|
||||
if (value !== 'module') {
|
||||
e.script_invalid_context(attribute);
|
||||
}
|
||||
|
||||
context = 'module';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Script',
|
||||
start,
|
||||
end: parser.index,
|
||||
context,
|
||||
content: ast,
|
||||
// @ts-ignore
|
||||
attributes
|
||||
};
|
||||
}
|
||||
@ -1,988 +0,0 @@
|
||||
/** @import { Expression, Identifier, SourceLocation } from 'estree' */
|
||||
/** @import { Location } from 'locate-character' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { is_void, REGEX_VALID_TAG_NAME } from '../../../../utils.js';
|
||||
import { read_expression } from '../js.js';
|
||||
import { read_script } from '../script.js';
|
||||
import read_style from '../css.js';
|
||||
import { decode_character_references } from '../utils/html.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import {
|
||||
create_attribute,
|
||||
create_fragment,
|
||||
ExpressionMetadata,
|
||||
is_element_node
|
||||
} from '../../nodes.js';
|
||||
import {
|
||||
get_attribute_expression,
|
||||
is_expression_attribute,
|
||||
value_names
|
||||
} from '../../../utils/ast.js';
|
||||
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
|
||||
import { list } from '../../../utils/string.js';
|
||||
import { locator } from '../../../state.js';
|
||||
import { is_whitespace } from '../utils/whitespace.js';
|
||||
|
||||
const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y;
|
||||
const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy;
|
||||
const regex_starts_with_quote_characters = /["']/y;
|
||||
const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y;
|
||||
const regex_doctype_name = /^![a-zA-Z]+$/;
|
||||
const regex_namespaced_name = /^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/;
|
||||
|
||||
/** @param {string} name */
|
||||
function is_valid_element_name(name) {
|
||||
// DOCTYPE (e.g. !DOCTYPE)
|
||||
if (regex_doctype_name.test(name)) return true;
|
||||
// svelte:* meta tags (e.g. svelte:element, svelte:head)
|
||||
if (regex_namespaced_name.test(name)) return true;
|
||||
// standard HTML/SVG/MathML elements and custom elements
|
||||
return REGEX_VALID_TAG_NAME.test(name);
|
||||
}
|
||||
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<string, AST.ElementLike['type']>} */
|
||||
const root_only_meta_tags = new Map([
|
||||
['svelte:head', 'SvelteHead'],
|
||||
['svelte:options', 'SvelteOptions'],
|
||||
['svelte:window', 'SvelteWindow'],
|
||||
['svelte:document', 'SvelteDocument'],
|
||||
['svelte:body', 'SvelteBody']
|
||||
]);
|
||||
|
||||
/** @type {Map<string, AST.ElementLike['type']>} */
|
||||
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('-->');
|
||||
parser.eat('-->', true);
|
||||
|
||||
parser.append({
|
||||
type: 'Comment',
|
||||
start,
|
||||
end: parser.index,
|
||||
data
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('/')) {
|
||||
const name = read_tag_name(parser);
|
||||
|
||||
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. <div><p></div>
|
||||
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') {
|
||||
if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) {
|
||||
const end = parent.fragment.nodes[0]?.start ?? start;
|
||||
w.element_implicitly_closed(
|
||||
{ start: parent.start, end },
|
||||
`</${name}>`,
|
||||
`</${parent.name}>`
|
||||
);
|
||||
}
|
||||
} else if (!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;
|
||||
}
|
||||
|
||||
const tag = read_tag(parser);
|
||||
|
||||
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
|
||||
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
|
||||
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
|
||||
}
|
||||
|
||||
if (!is_valid_element_name(tag.name) && !regex_valid_component_name.test(tag.name)) {
|
||||
// <div. -> in the middle of typing -> allow in loose mode
|
||||
if (!parser.loose || !tag.name.endsWith('.')) {
|
||||
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
|
||||
e.tag_invalid_name(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (root_only_meta_tags.has(tag.name)) {
|
||||
if (tag.name in parser.meta_tags) {
|
||||
e.svelte_meta_duplicate(start, tag.name);
|
||||
}
|
||||
|
||||
if (parent.type !== 'Root') {
|
||||
e.svelte_meta_invalid_placement(start, tag.name);
|
||||
}
|
||||
|
||||
parser.meta_tags[tag.name] = true;
|
||||
}
|
||||
|
||||
const type = meta_tags.has(tag.name)
|
||||
? meta_tags.get(tag.name)
|
||||
: regex_valid_component_name.test(tag.name) || (parser.loose && tag.name.endsWith('.'))
|
||||
? 'Component'
|
||||
: tag.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
|
||||
tag.name === 'slot' && !parent_is_shadowroot_template(parser.stack)
|
||||
? 'SlotElement'
|
||||
: 'RegularElement';
|
||||
|
||||
/** @type {AST.ElementLike} */
|
||||
const element =
|
||||
type === 'RegularElement'
|
||||
? {
|
||||
type,
|
||||
start,
|
||||
end: -1,
|
||||
name: tag.name,
|
||||
name_loc: tag.loc,
|
||||
attributes: [],
|
||||
fragment: create_fragment(true),
|
||||
metadata: {
|
||||
svg: false,
|
||||
mathml: false,
|
||||
scoped: false,
|
||||
has_spread: false,
|
||||
path: [],
|
||||
synthetic_value_node: null
|
||||
}
|
||||
}
|
||||
: /** @type {AST.ElementLike} */ ({
|
||||
type,
|
||||
start,
|
||||
end: -1,
|
||||
name: tag.name,
|
||||
name_loc: tag.loc,
|
||||
attributes: [],
|
||||
fragment: create_fragment(true),
|
||||
metadata: {
|
||||
// unpopulated at first, differs between types
|
||||
}
|
||||
});
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, tag.name)) {
|
||||
const end = parent.fragment.nodes[0]?.start ?? start;
|
||||
w.element_implicitly_closed({ start: parent.start, end }, `<${tag.name}>`, `</${parent.name}>`);
|
||||
parent.end = start;
|
||||
parser.pop();
|
||||
parser.last_auto_closed_tag = {
|
||||
tag: parent.name,
|
||||
reason: tag.name,
|
||||
depth: parser.stack.length
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const unique_names = [];
|
||||
|
||||
const current = parser.current();
|
||||
const is_top_level_script_or_style =
|
||||
(tag.name === 'script' || tag.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);
|
||||
// <svelte:element bind:this this=..> is allowed
|
||||
} else if (attribute.name !== 'this') {
|
||||
unique_names.push(type + attribute.name);
|
||||
}
|
||||
}
|
||||
|
||||
element.attributes.push(attribute);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (element.type === 'Component') {
|
||||
element.metadata.expression = new ExpressionMetadata();
|
||||
}
|
||||
|
||||
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);
|
||||
element.metadata.expression = new ExpressionMetadata();
|
||||
}
|
||||
|
||||
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 `<h>`.
|
||||
// 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<AST.ExpressionTag | AST.Text>} */ (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);
|
||||
}
|
||||
|
||||
element.metadata.expression = new ExpressionMetadata();
|
||||
}
|
||||
|
||||
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 (tag.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(tag.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 (self_closing || !closed) {
|
||||
// don't push self-closing elements onto the stack
|
||||
element.end = parser.index;
|
||||
} else if (tag.name === 'textarea') {
|
||||
// special case
|
||||
element.fragment.nodes = read_sequence(
|
||||
parser,
|
||||
() => {
|
||||
regex_closing_textarea_tag.lastIndex = parser.index;
|
||||
return regex_closing_textarea_tag.test(parser.template);
|
||||
},
|
||||
'inside <textarea>'
|
||||
);
|
||||
parser.read(regex_closing_textarea_tag);
|
||||
element.end = parser.index;
|
||||
} else if (tag.name === 'script' || tag.name === 'style') {
|
||||
// special case
|
||||
const start = parser.index;
|
||||
const close_tag = `</${tag.name}>`;
|
||||
const close_index = parser.template.indexOf(close_tag, parser.index);
|
||||
const data = parser.template.slice(
|
||||
parser.index,
|
||||
close_index === -1 ? parser.template.length : close_index
|
||||
);
|
||||
parser.index = close_index === -1 ? parser.template.length : close_index;
|
||||
const end = parser.index;
|
||||
|
||||
/** @type {AST.Text} */
|
||||
const node = {
|
||||
start,
|
||||
end,
|
||||
type: 'Text',
|
||||
data,
|
||||
raw: data
|
||||
};
|
||||
|
||||
element.fragment.nodes.push(node);
|
||||
parser.eat(`</${tag.name}>`, true);
|
||||
element.end = parser.index;
|
||||
} else {
|
||||
parser.stack.push(element);
|
||||
parser.fragments.push(element.fragment);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AST.TemplateNode[]} stack */
|
||||
function parent_is_head(stack) {
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
const { type } = stack[i];
|
||||
if (type === 'SvelteHead') return true;
|
||||
if (type === 'RegularElement' || type === 'Component') return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {AST.TemplateNode[]} stack */
|
||||
function parent_is_shadowroot_template(stack) {
|
||||
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
if (
|
||||
stack[i].type === 'RegularElement' &&
|
||||
/** @type {AST.RegularElement} */ (stack[i]).attributes.some(
|
||||
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.Attribute | null}
|
||||
*/
|
||||
function read_static_attribute(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
const tag = read_tag(parser, true);
|
||||
if (!tag.name) return null;
|
||||
|
||||
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
|
||||
let value = true;
|
||||
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
let raw = parser.match_regex(regex_attribute_value);
|
||||
if (!raw) {
|
||||
e.expected_attribute_value(parser.index);
|
||||
}
|
||||
|
||||
parser.index += raw.length;
|
||||
|
||||
const quoted = raw[0] === '"' || raw[0] === "'";
|
||||
if (quoted) {
|
||||
raw = raw.slice(1, -1);
|
||||
}
|
||||
|
||||
value = [
|
||||
{
|
||||
start: parser.index - raw.length - (quoted ? 1 : 0),
|
||||
end: quoted ? parser.index - 1 : parser.index,
|
||||
type: 'Text',
|
||||
raw: raw,
|
||||
data: decode_character_references(raw, true)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (parser.match_regex(regex_starts_with_quote_characters)) {
|
||||
e.expected_token(parser.index, '=');
|
||||
}
|
||||
|
||||
return create_attribute(tag.name, tag.loc, start, parser.index, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
|
||||
*/
|
||||
function read_attribute(parser) {
|
||||
/** @type {AST.JSComment | null} */
|
||||
// eslint-disable-next-line no-useless-assignment -- it is, in fact, eslint that is useless
|
||||
let comment = null;
|
||||
|
||||
while ((comment = read_comment(parser))) {
|
||||
parser.root.comments.push(comment);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
const start = parser.index;
|
||||
|
||||
if (parser.eat('{')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat('@attach')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.AttachTag} */
|
||||
const attachment = {
|
||||
type: 'AttachTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
if (parser.eat('...')) {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.SpreadAttribute} */
|
||||
const spread = {
|
||||
type: 'SpreadAttribute',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
|
||||
return spread;
|
||||
} else {
|
||||
const id = parser.read_identifier();
|
||||
|
||||
if (id.name === '') {
|
||||
if (
|
||||
parser.loose &&
|
||||
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
|
||||
) {
|
||||
// We're likely in an unclosed opening tag and did read part of a block.
|
||||
// Return null to not crash the parser so it can continue with closing the tag.
|
||||
return null;
|
||||
} else if (parser.loose && parser.match('}')) {
|
||||
// Likely in the middle of typing, just created the shorthand
|
||||
} else {
|
||||
e.attribute_empty_shorthand(start);
|
||||
}
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.ExpressionTag} */
|
||||
const expression = {
|
||||
type: 'ExpressionTag',
|
||||
start: id.start,
|
||||
end: id.end,
|
||||
expression: id,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
|
||||
return create_attribute(id.name, id.loc, start, parser.index, expression);
|
||||
}
|
||||
}
|
||||
|
||||
const tag = read_tag(parser, true);
|
||||
|
||||
if (!tag.name) return null;
|
||||
|
||||
let end = parser.index;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const colon_index = tag.name.indexOf(':');
|
||||
const type = colon_index !== -1 && get_directive_type(tag.name.slice(0, colon_index));
|
||||
|
||||
/** @type {true | AST.ExpressionTag | Array<AST.Text | AST.ExpressionTag>} */
|
||||
let value = true;
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') {
|
||||
const char_start = parser.index;
|
||||
parser.index++; // consume '/'
|
||||
value = [
|
||||
{
|
||||
start: char_start,
|
||||
end: char_start + 1,
|
||||
type: 'Text',
|
||||
raw: '/',
|
||||
data: '/'
|
||||
}
|
||||
];
|
||||
end = parser.index;
|
||||
} else {
|
||||
value = read_attribute_value(parser);
|
||||
end = parser.index;
|
||||
}
|
||||
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
|
||||
e.expected_token(parser.index, '=');
|
||||
}
|
||||
|
||||
if (type) {
|
||||
const [directive_name, ...modifiers] = tag.name.slice(colon_index + 1).split('|');
|
||||
|
||||
if (directive_name === '') {
|
||||
e.directive_missing_name({ start, end: start + colon_index + 1 }, tag.name);
|
||||
}
|
||||
|
||||
if (type === 'StyleDirective') {
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
name_loc: tag.loc,
|
||||
modifiers: /** @type {Array<'important'>} */ (modifiers),
|
||||
value,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value;
|
||||
|
||||
/** @type {Expression | null} */
|
||||
let expression = null;
|
||||
|
||||
if (first_value) {
|
||||
const attribute_contains_text =
|
||||
/** @type {any[]} */ (value).length > 1 || first_value.type === 'Text';
|
||||
if (attribute_contains_text) {
|
||||
e.directive_invalid_value(/** @type {number} */ (first_value.start));
|
||||
} else {
|
||||
// TODO throw a parser error in a future version here if this `[ExpressionTag]` instead of `ExpressionTag`,
|
||||
// which means stringified value, which isn't allowed for some directives?
|
||||
expression = first_value.expression;
|
||||
}
|
||||
}
|
||||
|
||||
const directive = /** @type {AST.Directive} */ ({
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
name_loc: tag.loc,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
|
||||
directive.modifiers = modifiers;
|
||||
|
||||
if (directive.type === 'TransitionDirective') {
|
||||
const direction = tag.name.slice(0, colon_index);
|
||||
directive.intro = direction === 'in' || direction === 'transition';
|
||||
directive.outro = direction === 'out' || direction === 'transition';
|
||||
}
|
||||
|
||||
// Directive name is expression, e.g. <p class:isRed />
|
||||
if (
|
||||
(directive.type === 'BindDirective' || directive.type === 'ClassDirective') &&
|
||||
!directive.expression
|
||||
) {
|
||||
directive.expression = /** @type {any} */ ({
|
||||
start: start + colon_index + 1,
|
||||
end,
|
||||
type: 'Identifier',
|
||||
name: directive.name
|
||||
});
|
||||
value_names.add(/** @type {Identifier} */ (directive.expression));
|
||||
}
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
return create_attribute(tag.name, tag.loc, start, end, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.JSComment | null}
|
||||
*/
|
||||
function read_comment(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
if (parser.eat('//')) {
|
||||
const value = parser.read_until('\n');
|
||||
const end = parser.index;
|
||||
|
||||
return {
|
||||
type: 'Line',
|
||||
start,
|
||||
end,
|
||||
value,
|
||||
loc: {
|
||||
start: locator(start),
|
||||
end: locator(end)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (parser.eat('/*')) {
|
||||
const value = parser.read_until('*/');
|
||||
|
||||
parser.eat('*/');
|
||||
const end = parser.index;
|
||||
|
||||
return {
|
||||
type: 'Block',
|
||||
start,
|
||||
end,
|
||||
value,
|
||||
loc: {
|
||||
start: locator(start),
|
||||
end: locator(end)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {any}
|
||||
*/
|
||||
function get_directive_type(name) {
|
||||
if (name === 'use') return 'UseDirective';
|
||||
if (name === 'animate') return 'AnimateDirective';
|
||||
if (name === 'bind') return 'BindDirective';
|
||||
if (name === 'class') return 'ClassDirective';
|
||||
if (name === 'style') return 'StyleDirective';
|
||||
if (name === 'on') return 'OnDirective';
|
||||
if (name === 'let') return 'LetDirective';
|
||||
if (name === 'in' || name === 'out' || name === 'transition') return 'TransitionDirective';
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @return {AST.ExpressionTag | Array<AST.ExpressionTag | AST.Text>}
|
||||
*/
|
||||
function read_attribute_value(parser) {
|
||||
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
|
||||
if (quote_mark && parser.eat(quote_mark)) {
|
||||
return [
|
||||
{
|
||||
start: parser.index - 1,
|
||||
end: parser.index - 1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/** @type {Array<AST.ExpressionTag | AST.Text>} */
|
||||
let value;
|
||||
try {
|
||||
value = read_sequence(
|
||||
parser,
|
||||
() => {
|
||||
// handle common case of quote marks existing outside of regex for performance reasons
|
||||
if (quote_mark) return parser.match(quote_mark);
|
||||
return !!parser.match_regex(regex_invalid_unquoted_attribute_value);
|
||||
},
|
||||
'in attribute value',
|
||||
// `<Component test={ />` is an unclosed value, not a regex
|
||||
['/>']
|
||||
);
|
||||
} catch (/** @type {any} */ error) {
|
||||
const pos = error.position?.[0];
|
||||
if (
|
||||
error.code === 'js_parse_error' &&
|
||||
pos !== undefined &&
|
||||
parser.template.startsWith('/>', pos)
|
||||
) {
|
||||
parser.index = pos;
|
||||
e.expected_token(pos, quote_mark || '}');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (value.length === 0 && !quote_mark) {
|
||||
e.expected_attribute_value(parser.index);
|
||||
}
|
||||
|
||||
if (quote_mark) parser.index += 1;
|
||||
|
||||
if (quote_mark || value.length > 1 || value[0].type === 'Text') {
|
||||
return value;
|
||||
} else {
|
||||
return value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {() => boolean} done
|
||||
* @param {string} location
|
||||
* @param {string[]} [stop_at] the template's tokens that end an expression, see `read_expression`
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function read_sequence(parser, done, location, stop_at) {
|
||||
/** @type {Array<AST.Text | AST.ExpressionTag>} */
|
||||
const chunks = [];
|
||||
let chunk_start = parser.index;
|
||||
|
||||
/** @param {number} end */
|
||||
function flush(end) {
|
||||
if (end > chunk_start) {
|
||||
const raw = parser.template.slice(chunk_start, end);
|
||||
chunks.push({
|
||||
start: chunk_start,
|
||||
end,
|
||||
type: 'Text',
|
||||
raw,
|
||||
data: decode_character_references(raw, true)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const index = parser.index;
|
||||
|
||||
if (done()) {
|
||||
flush(parser.index);
|
||||
return chunks;
|
||||
} else if (parser.eat('{')) {
|
||||
if (parser.match('#')) {
|
||||
const index = parser.index - 1;
|
||||
parser.eat('#');
|
||||
// const name = parser.read_until_regex(/[^a-z]/);
|
||||
const name = read_lowercase_name(parser);
|
||||
e.block_invalid_placement(index, name, location);
|
||||
} else if (parser.match('@')) {
|
||||
const index = parser.index - 1;
|
||||
parser.eat('@');
|
||||
// const name = parser.read_until_regex(/[^a-z]/);
|
||||
const name = read_lowercase_name(parser);
|
||||
e.tag_invalid_placement(index, name, location);
|
||||
}
|
||||
|
||||
flush(parser.index - 1);
|
||||
|
||||
parser.allow_whitespace();
|
||||
const expression = read_expression(parser, stop_at);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.ExpressionTag} */
|
||||
const chunk = {
|
||||
type: 'ExpressionTag',
|
||||
start: index,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
|
||||
chunks.push(chunk);
|
||||
chunk_start = parser.index;
|
||||
} else {
|
||||
parser.index++;
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.loose) {
|
||||
return chunks;
|
||||
} else {
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {boolean} [attribute]
|
||||
*/
|
||||
function read_tag_name(parser, attribute = false) {
|
||||
const start = parser.index;
|
||||
if (start >= parser.template.length && !parser.loose) e.unexpected_eof(parser.template.length);
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const cc = parser.template.charCodeAt(parser.index);
|
||||
if (
|
||||
is_whitespace(cc) ||
|
||||
cc === 47 || // /
|
||||
cc === 62 || // >
|
||||
(attribute && (cc === 34 || cc === 39 || cc === 61)) // " ' =
|
||||
) {
|
||||
break;
|
||||
}
|
||||
parser.index += 1;
|
||||
}
|
||||
|
||||
return parser.template.slice(start, parser.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {boolean} [attribute]
|
||||
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
|
||||
*/
|
||||
function read_tag(parser, attribute = false) {
|
||||
const start = parser.index;
|
||||
const name = read_tag_name(parser, attribute);
|
||||
const end = parser.index;
|
||||
|
||||
/** @type {Identifier & { start: number, end: number, loc: SourceLocation }} */
|
||||
const identifier = {
|
||||
type: 'Identifier',
|
||||
name,
|
||||
start,
|
||||
end,
|
||||
loc: {
|
||||
start: locator(start),
|
||||
end: locator(end)
|
||||
}
|
||||
};
|
||||
value_names.add(identifier);
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function read_lowercase_name(parser) {
|
||||
const start = parser.index;
|
||||
while (parser.index < parser.template.length) {
|
||||
const cc = parser.template.charCodeAt(parser.index);
|
||||
// a-z
|
||||
if (cc < 97 || cc > 122) break;
|
||||
parser.index += 1;
|
||||
}
|
||||
return parser.template.slice(start, parser.index);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import element from './element.js';
|
||||
import tag from './tag.js';
|
||||
import text from './text.js';
|
||||
|
||||
/** @param {Parser} parser */
|
||||
export default function fragment(parser) {
|
||||
if (parser.match('<')) {
|
||||
return element;
|
||||
}
|
||||
|
||||
if (parser.match('{')) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
@ -1,664 +0,0 @@
|
||||
/** @import { Expression, Identifier, Pattern, VariableDeclaration } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { create_fragment, ExpressionMetadata } from '../../nodes.js';
|
||||
import { read_expression, read_params, read_pattern, read_statement, read_type_parameters } from '../js.js';
|
||||
|
||||
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
|
||||
const regex_supported_declaration = /(?:let|const)\b/y;
|
||||
const regex_unsupported_declaration = /(?:var|interface|enum)\b/y;
|
||||
// `type` is a contextual keyword; this is just a shape hint, confirmed by parsing.
|
||||
const regex_maybe_type_declaration = /type\b/y;
|
||||
|
||||
|
||||
/** @param {Parser} parser */
|
||||
export default function tag(parser) {
|
||||
const start = parser.index;
|
||||
parser.index += 1;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat('#')) return open(parser);
|
||||
if (parser.eat(':')) return next(parser);
|
||||
if (parser.eat('@')) return special(parser);
|
||||
if (parser.match('/')) {
|
||||
if (!parser.match('/*') && !parser.match('//')) {
|
||||
parser.eat('/');
|
||||
return close(parser);
|
||||
}
|
||||
}
|
||||
|
||||
const declaration = read_declaration(parser);
|
||||
if (declaration) {
|
||||
parser.append({
|
||||
type: 'DeclarationTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
declaration: /** @type {VariableDeclaration} */ (declaration),
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'ExpressionTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {null | import('estree').VariableDeclaration}
|
||||
*/
|
||||
function read_declaration(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
const unsupported = parser.match_regex(regex_unsupported_declaration);
|
||||
if (unsupported) {
|
||||
e.declaration_tag_invalid_type({ start, end: start + unsupported.length });
|
||||
}
|
||||
|
||||
if (
|
||||
!parser.match_regex(regex_supported_declaration) &&
|
||||
// `type` is special, since it is not a reserved keyword and can be used
|
||||
// as part of a valid expression. We gotta parse first and then see what it is.
|
||||
!parser.match_regex(regex_maybe_type_declaration)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initial_comment_count = parser.root.comments.length;
|
||||
|
||||
const declaration = read_statement(parser);
|
||||
|
||||
if (declaration.type !== 'VariableDeclaration') {
|
||||
if (declaration.type === 'ExpressionStatement') {
|
||||
parser.index = start;
|
||||
parser.root.comments.length = initial_comment_count; // Else they show up duplicated
|
||||
return null;
|
||||
} else {
|
||||
// This is a TSTypeAliasDeclaration
|
||||
e.declaration_tag_invalid_type({
|
||||
start: declaration.start ?? start,
|
||||
end: declaration.end ?? parser.index
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO support using
|
||||
if (declaration.kind !== 'let' && declaration.kind !== 'const') {
|
||||
e.declaration_tag_invalid_type(declaration);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
return declaration;
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function open(parser) {
|
||||
let start = parser.index - 2;
|
||||
while (parser.template[start] !== '{') start -= 1;
|
||||
|
||||
if (parser.eat('if')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
/** @type {AST.IfBlock} */
|
||||
const block = parser.append({
|
||||
type: 'IfBlock',
|
||||
elseif: false,
|
||||
start,
|
||||
end: -1,
|
||||
test: read_expression(parser),
|
||||
consequent: create_fragment(),
|
||||
alternate: null,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.consequent);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('each')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
// the list ends at the `as` naming the item or the `,` before the index, so an assertion in it needs parens
|
||||
const expression = read_expression(parser, ['as', ',']);
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
/** @type {Pattern | null} */
|
||||
let context = null;
|
||||
let index;
|
||||
let key;
|
||||
|
||||
if (parser.eat('as')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
context = read_pattern(parser);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat(',')) {
|
||||
parser.allow_whitespace();
|
||||
index = parser.read_identifier().name;
|
||||
if (!index) {
|
||||
e.expected_identifier(parser.index);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (parser.eat('(')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
key = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat(')', true);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
parser.eat('}', true, false);
|
||||
|
||||
/** @type {AST.EachBlock} */
|
||||
const block = parser.append({
|
||||
type: 'EachBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression,
|
||||
body: create_fragment(),
|
||||
context,
|
||||
index,
|
||||
key,
|
||||
metadata: /** @type {any} */ (null) // filled in later
|
||||
});
|
||||
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.body);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('await')) {
|
||||
parser.require_whitespace();
|
||||
const expression = read_expression(parser, ['then', 'catch']);
|
||||
parser.allow_whitespace();
|
||||
|
||||
/** @type {AST.AwaitBlock} */
|
||||
const block = parser.append({
|
||||
type: 'AwaitBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression,
|
||||
value: null,
|
||||
error: null,
|
||||
pending: null,
|
||||
then: null,
|
||||
catch: null,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
if (parser.eat('then')) {
|
||||
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
|
||||
parser.allow_whitespace();
|
||||
} else {
|
||||
parser.require_whitespace();
|
||||
block.value = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
block.then = create_fragment();
|
||||
parser.fragments.push(block.then);
|
||||
} else if (parser.eat('catch')) {
|
||||
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
|
||||
parser.allow_whitespace();
|
||||
} else {
|
||||
parser.require_whitespace();
|
||||
block.error = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
block.catch = create_fragment();
|
||||
parser.fragments.push(block.catch);
|
||||
} else {
|
||||
block.pending = create_fragment();
|
||||
parser.fragments.push(block.pending);
|
||||
}
|
||||
|
||||
parser.eat('}', true, false);
|
||||
|
||||
parser.stack.push(block);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('key')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.KeyBlock} */
|
||||
const block = parser.append({
|
||||
type: 'KeyBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression,
|
||||
fragment: create_fragment(),
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.fragment);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('snippet')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const id = parser.read_identifier();
|
||||
|
||||
if (id.name === '' && !parser.loose) {
|
||||
e.expected_identifier(parser.index);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
// snippets could have a generic signature, e.g. `#snippet foo<T>(...)`
|
||||
/** @type {string | undefined} */
|
||||
let type_params;
|
||||
|
||||
if (parser.ts && parser.match('<')) {
|
||||
const start = parser.index;
|
||||
read_type_parameters(parser);
|
||||
type_params = parser.template.slice(start + 1, parser.index - 1);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
/** @type {import('estree').Pattern[]} */
|
||||
let parameters = [];
|
||||
|
||||
if (parser.match('(')) {
|
||||
parameters = read_params(parser);
|
||||
} else if (!parser.loose) {
|
||||
e.expected_token(parser.index, '(');
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.SnippetBlock} */
|
||||
const block = parser.append({
|
||||
type: 'SnippetBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression: id,
|
||||
typeParams: type_params,
|
||||
parameters,
|
||||
body: create_fragment(),
|
||||
metadata: {
|
||||
can_hoist: false,
|
||||
sites: new Set()
|
||||
}
|
||||
});
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.body);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
e.expected_block_type(parser.index);
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function next(parser) {
|
||||
const start = parser.index - 1;
|
||||
|
||||
const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad
|
||||
|
||||
if (block.type === 'IfBlock') {
|
||||
if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
|
||||
if (parser.eat('if')) e.block_invalid_elseif(start);
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
parser.fragments.pop();
|
||||
|
||||
block.alternate = create_fragment();
|
||||
parser.fragments.push(block.alternate);
|
||||
|
||||
// :else if
|
||||
if (parser.eat('if')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
let elseif_start = start - 1;
|
||||
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
|
||||
|
||||
/** @type {AST.IfBlock} */
|
||||
const child = parser.append({
|
||||
start: elseif_start,
|
||||
end: -1,
|
||||
type: 'IfBlock',
|
||||
elseif: true,
|
||||
test: expression,
|
||||
consequent: create_fragment(),
|
||||
alternate: null,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
parser.stack.push(child);
|
||||
parser.fragments.pop();
|
||||
parser.fragments.push(child.consequent);
|
||||
} else {
|
||||
// :else
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.type === 'EachBlock') {
|
||||
if (!parser.eat('else')) e.expected_token(start, '{:else}');
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
block.fallback = create_fragment();
|
||||
|
||||
parser.fragments.pop();
|
||||
parser.fragments.push(block.fallback);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.type === 'AwaitBlock') {
|
||||
if (parser.eat('then')) {
|
||||
if (block.then) {
|
||||
e.block_duplicate_clause(start, '{:then}');
|
||||
}
|
||||
|
||||
if (!parser.eat('}')) {
|
||||
parser.require_whitespace();
|
||||
block.value = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
block.then = create_fragment();
|
||||
parser.fragments.pop();
|
||||
parser.fragments.push(block.then);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('catch')) {
|
||||
if (block.catch) {
|
||||
e.block_duplicate_clause(start, '{:catch}');
|
||||
}
|
||||
|
||||
if (!parser.eat('}')) {
|
||||
parser.require_whitespace();
|
||||
block.error = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
block.catch = create_fragment();
|
||||
parser.fragments.pop();
|
||||
parser.fragments.push(block.catch);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
e.expected_token(start, '{:then ...} or {:catch ...}');
|
||||
}
|
||||
|
||||
e.block_invalid_continuation_placement(start);
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function close(parser) {
|
||||
const start = parser.index - 1;
|
||||
|
||||
let block = parser.current();
|
||||
/** Only relevant/reached for loose parsing mode */
|
||||
let matched;
|
||||
|
||||
switch (block.type) {
|
||||
case 'IfBlock':
|
||||
matched = parser.eat('if', true, false);
|
||||
|
||||
if (!matched) {
|
||||
block.end = start - 1;
|
||||
parser.pop();
|
||||
close(parser);
|
||||
return;
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
while (block.elseif) {
|
||||
block.end = parser.index;
|
||||
parser.stack.pop();
|
||||
block = /** @type {AST.IfBlock} */ (parser.current());
|
||||
}
|
||||
|
||||
block.end = parser.index;
|
||||
parser.pop();
|
||||
return;
|
||||
|
||||
case 'EachBlock':
|
||||
matched = parser.eat('each', true, false);
|
||||
break;
|
||||
case 'KeyBlock':
|
||||
matched = parser.eat('key', true, false);
|
||||
break;
|
||||
case 'AwaitBlock':
|
||||
matched = parser.eat('await', true, false);
|
||||
break;
|
||||
case 'SnippetBlock':
|
||||
matched = parser.eat('snippet', true, false);
|
||||
break;
|
||||
|
||||
case 'RegularElement':
|
||||
if (parser.loose) {
|
||||
matched = false;
|
||||
} else {
|
||||
// TODO handle implicitly closed elements
|
||||
e.block_unexpected_close(start);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
e.block_unexpected_close(start);
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
block.end = start - 1;
|
||||
parser.pop();
|
||||
close(parser);
|
||||
return;
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
block.end = parser.index;
|
||||
parser.pop();
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function special(parser) {
|
||||
let start = parser.index;
|
||||
while (parser.template[start] !== '{') start -= 1;
|
||||
|
||||
if (parser.eat('html')) {
|
||||
// {@html content} tag
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'HtmlTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('debug')) {
|
||||
/** @type {Identifier[]} */
|
||||
let identifiers;
|
||||
|
||||
// Implies {@debug} which indicates "debug all"
|
||||
if (parser.read(regex_whitespace_with_closing_curly_brace)) {
|
||||
identifiers = [];
|
||||
} else {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
identifiers =
|
||||
expression.type === 'SequenceExpression'
|
||||
? /** @type {Identifier[]} */ (expression.expressions)
|
||||
: [/** @type {Identifier} */ (expression)];
|
||||
|
||||
identifiers.forEach(
|
||||
/** @param {any} node */ (node) => {
|
||||
if (node.type !== 'Identifier') {
|
||||
e.debug_tag_invalid_arguments(/** @type {number} */ (node.start));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
parser.append({
|
||||
type: 'DebugTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
identifiers
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('const')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const id = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
|
||||
parser.eat('=', true);
|
||||
parser.allow_whitespace();
|
||||
|
||||
const init = read_expression(parser);
|
||||
// parser is past wrapping parens, but `init.end` is not — use the parser position
|
||||
const declarator_end = parser.index;
|
||||
if (init.type === 'SequenceExpression' && !init.parenthesized) {
|
||||
// const a = (b, c) is allowed but a = b, c = d is not;
|
||||
e.const_tag_invalid_expression(init);
|
||||
}
|
||||
parser.allow_whitespace();
|
||||
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'ConstTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
declaration: {
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{ type: 'VariableDeclarator', id, init, start: id.start, end: declarator_end }
|
||||
],
|
||||
start: start + 2, // start at const, not at @const
|
||||
end: parser.index - 1
|
||||
},
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('render')) {
|
||||
// {@render foo(...)}
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
if (
|
||||
expression.type !== 'CallExpression' &&
|
||||
(expression.type !== 'ChainExpression' || expression.expression.type !== 'CallExpression')
|
||||
) {
|
||||
e.render_tag_invalid_expression(expression);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'RenderTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression: /** @type {AST.RenderTag['expression']} */ (expression),
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata(),
|
||||
dynamic: false,
|
||||
arguments: [],
|
||||
path: [],
|
||||
snippets: new Set()
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
e.expected_tag(parser.index);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { decode_character_references } from '../utils/html.js';
|
||||
|
||||
/** @param {Parser} parser */
|
||||
export default function text(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
|
||||
parser.index++;
|
||||
}
|
||||
|
||||
const data = parser.template.slice(start, parser.index);
|
||||
|
||||
/** @type {AST.Text} */
|
||||
parser.append({
|
||||
type: 'Text',
|
||||
start,
|
||||
end: parser.index,
|
||||
raw: data,
|
||||
data: decode_character_references(data, false)
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,134 +0,0 @@
|
||||
import entities from './entities.js';
|
||||
|
||||
const windows_1252 = [
|
||||
8364, 129, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 141, 381, 143, 144, 8216,
|
||||
8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 157, 382, 376
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {string} entity_name
|
||||
* @param {boolean} is_attribute_value
|
||||
*/
|
||||
function reg_exp_entity(entity_name, is_attribute_value) {
|
||||
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
|
||||
// doesn't decode the html entity which not ends with ; and next character is =, number or alphabet in attribute value.
|
||||
if (is_attribute_value && !entity_name.endsWith(';')) {
|
||||
return `${entity_name}\\b(?!=)`;
|
||||
}
|
||||
return entity_name;
|
||||
}
|
||||
|
||||
/** @param {boolean} is_attribute_value */
|
||||
function get_entity_pattern(is_attribute_value) {
|
||||
const reg_exp_num = '#(?:[xX][a-fA-F\\d]+|\\d+)(?:;)?';
|
||||
const reg_exp_entities = Object.keys(entities).map(
|
||||
/** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
|
||||
);
|
||||
|
||||
const entity_pattern = new RegExp(`&(${reg_exp_num}|${reg_exp_entities.join('|')})`, 'g');
|
||||
|
||||
return entity_pattern;
|
||||
}
|
||||
|
||||
const entity_pattern_content = get_entity_pattern(false);
|
||||
const entity_pattern_attr_value = get_entity_pattern(true);
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @param {boolean} is_attribute_value
|
||||
*/
|
||||
export function decode_character_references(html, is_attribute_value) {
|
||||
if (html.indexOf('&') === -1) return html; // fast path
|
||||
|
||||
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
|
||||
return html.replace(
|
||||
entity_pattern,
|
||||
/**
|
||||
* @param {any} match
|
||||
* @param {keyof typeof entities} entity
|
||||
*/ (match, entity) => {
|
||||
let code;
|
||||
|
||||
// Handle named entities
|
||||
if (entity[0] !== '#') {
|
||||
code = entities[entity];
|
||||
} else if (entity[1] === 'x' || entity[1] === 'X') {
|
||||
code = parseInt(entity.substring(2), 16);
|
||||
} else {
|
||||
code = parseInt(entity.substring(1), 10);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return match;
|
||||
}
|
||||
|
||||
return String.fromCodePoint(validate_code(code, is_attribute_value));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const NUL = 0;
|
||||
|
||||
// some code points are verboten. If we were inserting HTML, the browser would replace the illegal
|
||||
// code points with alternatives in some cases - since we're bypassing that mechanism, we need
|
||||
// to replace them ourselves
|
||||
//
|
||||
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
|
||||
// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
|
||||
// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
|
||||
|
||||
/**
|
||||
* @param {number} code
|
||||
* @param {boolean} is_attribute_value
|
||||
*/
|
||||
function validate_code(code, is_attribute_value) {
|
||||
// line feed becomes generic whitespace, since it is collapsed along with the
|
||||
// surrounding whitespace anyway. In an attribute value it is significant, so it
|
||||
// is left alone there
|
||||
if (code === 10 && !is_attribute_value) {
|
||||
return 32;
|
||||
}
|
||||
|
||||
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
|
||||
if (code < 128) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
|
||||
// to correct the mistake or we'll end up with missing € signs and so on
|
||||
if (code <= 159) {
|
||||
return windows_1252[code - 128];
|
||||
}
|
||||
|
||||
// basic multilingual plane
|
||||
if (code < 55296) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// UTF-16 surrogate halves
|
||||
if (code <= 57343) {
|
||||
return NUL;
|
||||
}
|
||||
|
||||
// rest of the basic multilingual plane
|
||||
if (code <= 65535) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary multilingual plane 0x10000 - 0x1ffff
|
||||
if (code >= 65536 && code <= 131071) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary ideographic plane 0x20000 - 0x2ffff
|
||||
if (code >= 131072 && code <= 196607) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef
|
||||
if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) {
|
||||
return code;
|
||||
}
|
||||
|
||||
return NUL;
|
||||
}
|
||||
Loading…
Reference in new issue