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