fix: vite
This commit is contained in:
214
node_modules/svelte/src/compiler/phases/1-parse/acorn.js
generated
vendored
Normal file
214
node_modules/svelte/src/compiler/phases/1-parse/acorn.js
generated
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
/** @import { Comment, Program } from 'estree' */
|
||||
/** @import { Node } from 'acorn' */
|
||||
import * as acorn from 'acorn';
|
||||
import { walk } from 'zimmerframe';
|
||||
import { tsPlugin } from 'acorn-typescript';
|
||||
import { locator } from '../../state.js';
|
||||
|
||||
const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {boolean} typescript
|
||||
* @param {boolean} [is_script]
|
||||
*/
|
||||
export function parse(source, typescript, is_script) {
|
||||
const parser = typescript ? ParserWithTS : acorn.Parser;
|
||||
const { onComment, add_comments } = get_comment_handlers(source);
|
||||
// @ts-ignore
|
||||
const parse_statement = parser.prototype.parseStatement;
|
||||
|
||||
// If we're dealing with a <script> then it might contain an export
|
||||
// for something that doesn't exist directly inside but is inside the
|
||||
// component instead, so we need to ensure that Acorn doesn't throw
|
||||
// an error in these cases
|
||||
if (is_script) {
|
||||
// @ts-ignore
|
||||
parser.prototype.parseStatement = function (...args) {
|
||||
const v = parse_statement.call(this, ...args);
|
||||
// @ts-ignore
|
||||
this.undefinedExports = {};
|
||||
return v;
|
||||
};
|
||||
}
|
||||
|
||||
let ast;
|
||||
|
||||
try {
|
||||
ast = parser.parse(source, {
|
||||
onComment,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 13,
|
||||
locations: true
|
||||
});
|
||||
} finally {
|
||||
if (is_script) {
|
||||
// @ts-ignore
|
||||
parser.prototype.parseStatement = parse_statement;
|
||||
}
|
||||
}
|
||||
|
||||
if (typescript) amend(source, ast);
|
||||
add_comments(ast);
|
||||
|
||||
return /** @type {Program} */ (ast);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {boolean} typescript
|
||||
* @param {number} index
|
||||
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
|
||||
*/
|
||||
export function parse_expression_at(source, typescript, index) {
|
||||
const parser = typescript ? ParserWithTS : acorn.Parser;
|
||||
const { onComment, add_comments } = get_comment_handlers(source);
|
||||
|
||||
const ast = parser.parseExpressionAt(source, index, {
|
||||
onComment,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 13,
|
||||
locations: true
|
||||
});
|
||||
|
||||
if (typescript) amend(source, ast);
|
||||
add_comments(ast);
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
|
||||
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
|
||||
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
|
||||
* @param {string} source
|
||||
*/
|
||||
function get_comment_handlers(source) {
|
||||
/**
|
||||
* @typedef {Comment & {
|
||||
* start: number;
|
||||
* end: number;
|
||||
* }} CommentWithLocation
|
||||
*/
|
||||
|
||||
/** @type {CommentWithLocation[]} */
|
||||
const comments = [];
|
||||
|
||||
return {
|
||||
/**
|
||||
* @param {boolean} block
|
||||
* @param {string} value
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
*/
|
||||
onComment: (block, value, start, end) => {
|
||||
if (block && /\n/.test(value)) {
|
||||
let a = start;
|
||||
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
||||
|
||||
let b = a;
|
||||
while (/[ \t]/.test(source[b])) b += 1;
|
||||
|
||||
const indentation = source.slice(a, b);
|
||||
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
|
||||
}
|
||||
|
||||
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
|
||||
},
|
||||
|
||||
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
|
||||
add_comments(ast) {
|
||||
if (comments.length === 0) return;
|
||||
|
||||
walk(ast, null, {
|
||||
_(node, { next, path }) {
|
||||
let comment;
|
||||
|
||||
while (comments[0] && comments[0].start < node.start) {
|
||||
comment = /** @type {CommentWithLocation} */ (comments.shift());
|
||||
(node.leadingComments ||= []).push(comment);
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
if (comments[0]) {
|
||||
const parent = /** @type {any} */ (path.at(-1));
|
||||
|
||||
if (parent === undefined || node.end !== parent.end) {
|
||||
const slice = source.slice(node.end, comments[0].start);
|
||||
const is_last_in_body =
|
||||
((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
|
||||
parent.body.indexOf(node) === parent.body.length - 1) ||
|
||||
(parent?.type === 'ArrayExpression' &&
|
||||
parent.elements.indexOf(node) === parent.elements.length - 1) ||
|
||||
(parent?.type === 'ObjectExpression' &&
|
||||
parent.properties.indexOf(node) === parent.properties.length - 1);
|
||||
|
||||
if (is_last_in_body) {
|
||||
// Special case: There can be multiple trailing comments after the last node in a block,
|
||||
// and they can be separated by newlines
|
||||
let end = node.end;
|
||||
|
||||
while (comments.length) {
|
||||
const comment = comments[0];
|
||||
if (parent && comment.start >= parent.end) break;
|
||||
|
||||
(node.trailingComments ||= []).push(comment);
|
||||
comments.shift();
|
||||
end = comment.end;
|
||||
}
|
||||
} else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
|
||||
node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
|
||||
// Adding them ensures that we can later detect the end of the expression tag correctly.
|
||||
if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
|
||||
(ast.trailingComments ||= []).push(...comments.splice(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy up some stuff left behind by acorn-typescript
|
||||
* @param {string} source
|
||||
* @param {Node} node
|
||||
*/
|
||||
function amend(source, node) {
|
||||
return walk(node, null, {
|
||||
_(node, context) {
|
||||
// @ts-expect-error
|
||||
delete node.loc.start.index;
|
||||
// @ts-expect-error
|
||||
delete node.loc.end.index;
|
||||
|
||||
if (typeof node.loc?.end === 'number') {
|
||||
const loc = locator(node.loc.end);
|
||||
if (loc) {
|
||||
node.loc.end = {
|
||||
line: loc.line,
|
||||
column: loc.column
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
/** @type {any} */ (node).typeAnnotation &&
|
||||
(node.end === undefined || node.end < node.start)
|
||||
) {
|
||||
// i think there might be a bug in acorn-typescript that prevents
|
||||
// `end` from being assigned when there's a type annotation
|
||||
let end = /** @type {any} */ (node).typeAnnotation.start;
|
||||
while (/\s/.test(source[end - 1])) end -= 1;
|
||||
node.end = end;
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
3
node_modules/svelte/src/compiler/phases/1-parse/ambient.d.ts
generated
vendored
Normal file
3
node_modules/svelte/src/compiler/phases/1-parse/ambient.d.ts
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// Silence the acorn typescript errors through this ambient type definition + tsconfig.json path alias
|
||||
// That way we can omit `"skipLibCheck": true` and catch other errors in our d.ts files
|
||||
declare module 'acorn-typescript';
|
||||
312
node_modules/svelte/src/compiler/phases/1-parse/index.js
generated
vendored
Normal file
312
node_modules/svelte/src/compiler/phases/1-parse/index.js
generated
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
// @ts-expect-error acorn type definitions are borked in the release we use
|
||||
import { isIdentifierStart, isIdentifierChar } from 'acorn';
|
||||
import fragment from './state/fragment.js';
|
||||
import { regex_whitespace } from '../patterns.js';
|
||||
import * as e from '../../errors.js';
|
||||
import { create_fragment } from './utils/create.js';
|
||||
import read_options from './read/options.js';
|
||||
import { is_reserved } from '../../../utils.js';
|
||||
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
|
||||
|
||||
const regex_position_indicator = / \(\d+:\d+\)$/;
|
||||
|
||||
const regex_lang_attribute =
|
||||
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
|
||||
|
||||
export class Parser {
|
||||
/**
|
||||
* @readonly
|
||||
* @type {string}
|
||||
*/
|
||||
template;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @type {string}
|
||||
*/
|
||||
template_untrimmed;
|
||||
|
||||
/**
|
||||
* Whether or not we're in loose parsing mode, in which
|
||||
* case we try to continue parsing as much as possible
|
||||
* @type {boolean}
|
||||
*/
|
||||
loose;
|
||||
|
||||
/** */
|
||||
index = 0;
|
||||
|
||||
/** Whether we're parsing in TypeScript mode */
|
||||
ts = false;
|
||||
|
||||
/** @type {AST.TemplateNode[]} */
|
||||
stack = [];
|
||||
|
||||
/** @type {AST.Fragment[]} */
|
||||
fragments = [];
|
||||
|
||||
/** @type {AST.Root} */
|
||||
root;
|
||||
|
||||
/** @type {Record<string, boolean>} */
|
||||
meta_tags = {};
|
||||
|
||||
/** @type {LastAutoClosedTag | undefined} */
|
||||
last_auto_closed_tag;
|
||||
|
||||
/**
|
||||
* @param {string} template
|
||||
* @param {boolean} loose
|
||||
*/
|
||||
constructor(template, loose) {
|
||||
if (typeof template !== 'string') {
|
||||
throw new TypeError('Template must be a string');
|
||||
}
|
||||
|
||||
this.loose = loose;
|
||||
this.template_untrimmed = template;
|
||||
this.template = template.trimEnd();
|
||||
|
||||
let match_lang;
|
||||
|
||||
do match_lang = regex_lang_attribute.exec(template);
|
||||
while (match_lang && match_lang[0][1] !== 's'); // ensure it starts with '<s' to match script tags
|
||||
|
||||
regex_lang_attribute.lastIndex = 0; // reset matched index to pass tests - otherwise declare the regex inside the constructor
|
||||
|
||||
this.ts = match_lang?.[2] === 'ts';
|
||||
|
||||
this.root = {
|
||||
css: null,
|
||||
js: [],
|
||||
// @ts-ignore
|
||||
start: null,
|
||||
// @ts-ignore
|
||||
end: null,
|
||||
type: 'Root',
|
||||
fragment: create_fragment(),
|
||||
options: null,
|
||||
metadata: {
|
||||
ts: this.ts
|
||||
}
|
||||
};
|
||||
|
||||
this.stack.push(this.root);
|
||||
this.fragments.push(this.root.fragment);
|
||||
|
||||
/** @type {ParserState} */
|
||||
let state = fragment;
|
||||
|
||||
while (this.index < this.template.length) {
|
||||
state = state(this) || fragment;
|
||||
}
|
||||
|
||||
if (this.stack.length > 1) {
|
||||
const current = this.current();
|
||||
|
||||
if (this.loose) {
|
||||
current.end = this.template.length;
|
||||
} else if (current.type === 'RegularElement') {
|
||||
current.end = current.start + 1;
|
||||
e.element_unclosed(current, current.name);
|
||||
} else {
|
||||
current.end = current.start + 1;
|
||||
e.block_unclosed(current);
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== fragment) {
|
||||
e.unexpected_eof(this.index);
|
||||
}
|
||||
|
||||
if (this.root.fragment.nodes.length) {
|
||||
let start = /** @type {number} */ (this.root.fragment.nodes[0].start);
|
||||
while (regex_whitespace.test(template[start])) start += 1;
|
||||
|
||||
let end = /** @type {number} */ (
|
||||
this.root.fragment.nodes[this.root.fragment.nodes.length - 1].end
|
||||
);
|
||||
while (regex_whitespace.test(template[end - 1])) end -= 1;
|
||||
|
||||
this.root.start = start;
|
||||
this.root.end = end;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.root.start = this.root.end = null;
|
||||
}
|
||||
|
||||
const options_index = this.root.fragment.nodes.findIndex(
|
||||
/** @param {any} thing */
|
||||
(thing) => thing.type === 'SvelteOptions'
|
||||
);
|
||||
if (options_index !== -1) {
|
||||
const options = /** @type {AST.SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
|
||||
this.root.fragment.nodes.splice(options_index, 1);
|
||||
this.root.options = read_options(options);
|
||||
|
||||
disallow_children(options);
|
||||
|
||||
// We need this for the old AST format
|
||||
Object.defineProperty(this.root.options, '__raw__', {
|
||||
value: options,
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
current() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} err
|
||||
* @returns {never}
|
||||
*/
|
||||
acorn_error(err) {
|
||||
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {boolean} required
|
||||
* @param {boolean} required_in_loose
|
||||
*/
|
||||
eat(str, required = false, required_in_loose = true) {
|
||||
if (this.match(str)) {
|
||||
this.index += str.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (required && (!this.loose || required_in_loose)) {
|
||||
e.expected_token(this.index, str);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {string} str */
|
||||
match(str) {
|
||||
const length = str.length;
|
||||
if (length === 1) {
|
||||
// more performant than slicing
|
||||
return this.template[this.index] === str;
|
||||
}
|
||||
|
||||
return this.template.slice(this.index, this.index + length) === str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a regex at the current index
|
||||
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
|
||||
*/
|
||||
match_regex(pattern) {
|
||||
const match = pattern.exec(this.template.slice(this.index));
|
||||
if (!match || match.index !== 0) return null;
|
||||
|
||||
return match[0];
|
||||
}
|
||||
|
||||
allow_whitespace() {
|
||||
while (this.index < this.template.length && regex_whitespace.test(this.template[this.index])) {
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a regex starting at the current index and return the result if it matches
|
||||
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
|
||||
*/
|
||||
read(pattern) {
|
||||
const result = this.match_regex(pattern);
|
||||
if (result) this.index += result.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @param {any} allow_reserved */
|
||||
read_identifier(allow_reserved = false) {
|
||||
const start = this.index;
|
||||
|
||||
let i = this.index;
|
||||
|
||||
const code = /** @type {number} */ (this.template.codePointAt(i));
|
||||
if (!isIdentifierStart(code, true)) return null;
|
||||
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
|
||||
while (i < this.template.length) {
|
||||
const code = /** @type {number} */ (this.template.codePointAt(i));
|
||||
|
||||
if (!isIdentifierChar(code, true)) break;
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
}
|
||||
|
||||
const identifier = this.template.slice(this.index, (this.index = i));
|
||||
|
||||
if (!allow_reserved && is_reserved(identifier)) {
|
||||
e.unexpected_reserved_word(start, identifier);
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/** @param {RegExp} pattern */
|
||||
read_until(pattern) {
|
||||
if (this.index >= this.template.length) {
|
||||
if (this.loose) return '';
|
||||
e.unexpected_eof(this.template.length);
|
||||
}
|
||||
|
||||
const start = this.index;
|
||||
const match = pattern.exec(this.template.slice(start));
|
||||
|
||||
if (match) {
|
||||
this.index = start + match.index;
|
||||
return this.template.slice(start, this.index);
|
||||
}
|
||||
|
||||
this.index = this.template.length;
|
||||
return this.template.slice(start);
|
||||
}
|
||||
|
||||
require_whitespace() {
|
||||
if (!regex_whitespace.test(this.template[this.index])) {
|
||||
e.expected_whitespace(this.index);
|
||||
}
|
||||
|
||||
this.allow_whitespace();
|
||||
}
|
||||
|
||||
pop() {
|
||||
this.fragments.pop();
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {AST.Fragment['nodes'][number]} T
|
||||
* @param {T} node
|
||||
* @returns {T}
|
||||
*/
|
||||
append(node) {
|
||||
this.fragments.at(-1)?.nodes.push(node);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} template
|
||||
* @param {boolean} [loose]
|
||||
* @returns {AST.Root}
|
||||
*/
|
||||
export function parse(template, loose = false) {
|
||||
const parser = new Parser(template, loose);
|
||||
return parser.root;
|
||||
}
|
||||
|
||||
/** @typedef {(parser: Parser) => ParserState | void} ParserState */
|
||||
|
||||
/** @typedef {Object} LastAutoClosedTag
|
||||
* @property {string} tag
|
||||
* @property {string} reason
|
||||
* @property {number} depth
|
||||
*/
|
||||
187
node_modules/svelte/src/compiler/phases/1-parse/read/context.js
generated
vendored
Normal file
187
node_modules/svelte/src/compiler/phases/1-parse/read/context.js
generated
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
/** @import { Location } from 'locate-character' */
|
||||
/** @import { Pattern } from 'estree' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js';
|
||||
import { parse_expression_at } from '../acorn.js';
|
||||
import { regex_not_newline_characters } from '../../patterns.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import { locator } from '../../../state.js';
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export default function read_pattern(parser) {
|
||||
const start = parser.index;
|
||||
let i = parser.index;
|
||||
|
||||
const name = parser.read_identifier();
|
||||
|
||||
if (name !== null) {
|
||||
const annotation = read_type_annotation(parser);
|
||||
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name,
|
||||
start,
|
||||
loc: {
|
||||
start: /** @type {Location} */ (locator(start)),
|
||||
end: /** @type {Location} */ (locator(parser.index))
|
||||
},
|
||||
end: parser.index,
|
||||
typeAnnotation: annotation
|
||||
};
|
||||
}
|
||||
|
||||
if (!is_bracket_open(parser.template[i])) {
|
||||
e.expected_pattern(i);
|
||||
}
|
||||
|
||||
i = match_bracket(parser, start);
|
||||
parser.index = i;
|
||||
|
||||
const pattern_string = parser.template.slice(start, i);
|
||||
|
||||
try {
|
||||
// the length of the `space_with_newline` has to be start - 1
|
||||
// because we added a `(` in front of the pattern_string,
|
||||
// which shifted the entire string to right by 1
|
||||
// so we offset it by removing 1 character in the `space_with_newline`
|
||||
// to achieve that, we remove the 1st space encountered,
|
||||
// so it will not affect the `column` of the node
|
||||
let space_with_newline = parser.template
|
||||
.slice(0, start)
|
||||
.replace(regex_not_newline_characters, ' ');
|
||||
const first_space = space_with_newline.indexOf(' ');
|
||||
space_with_newline =
|
||||
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
|
||||
|
||||
const expression = /** @type {any} */ (
|
||||
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1)
|
||||
).left;
|
||||
|
||||
expression.typeAnnotation = read_type_annotation(parser);
|
||||
if (expression.typeAnnotation) {
|
||||
expression.end = expression.typeAnnotation.end;
|
||||
}
|
||||
|
||||
return expression;
|
||||
} catch (error) {
|
||||
parser.acorn_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} start
|
||||
*/
|
||||
function match_bracket(parser, start) {
|
||||
const bracket_stack = [];
|
||||
|
||||
let i = start;
|
||||
|
||||
while (i < parser.template.length) {
|
||||
let char = parser.template[i++];
|
||||
|
||||
if (char === "'" || char === '"' || char === '`') {
|
||||
i = match_quote(parser, i, char);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_bracket_open(char)) {
|
||||
bracket_stack.push(char);
|
||||
} else if (is_bracket_close(char)) {
|
||||
const popped = /** @type {string} */ (bracket_stack.pop());
|
||||
const expected = /** @type {string} */ (get_bracket_close(popped));
|
||||
|
||||
if (char !== expected) {
|
||||
e.expected_token(i - 1, expected);
|
||||
}
|
||||
|
||||
if (bracket_stack.length === 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} start
|
||||
* @param {string} quote
|
||||
*/
|
||||
function match_quote(parser, start, quote) {
|
||||
let is_escaped = false;
|
||||
let i = start;
|
||||
|
||||
while (i < parser.template.length) {
|
||||
const char = parser.template[i++];
|
||||
|
||||
if (is_escaped) {
|
||||
is_escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === quote) {
|
||||
return i;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
is_escaped = true;
|
||||
}
|
||||
|
||||
if (quote === '`' && char === '$' && parser.template[i] === '{') {
|
||||
i = match_bracket(parser, i);
|
||||
}
|
||||
}
|
||||
|
||||
e.unterminated_string_constant(start);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {any}
|
||||
*/
|
||||
function read_type_annotation(parser) {
|
||||
const start = parser.index;
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (!parser.eat(':')) {
|
||||
parser.index = start;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// we need to trick Acorn into parsing the type annotation
|
||||
const insert = '_ as ';
|
||||
let a = parser.index - insert.length;
|
||||
const template =
|
||||
parser.template.slice(0, a).replace(/[^\n]/g, ' ') +
|
||||
insert +
|
||||
// If this is a type annotation for a function parameter, Acorn-TS will treat subsequent
|
||||
// parameters as part of a sequence expression instead, and will then error on optional
|
||||
// parameters (`?:`). Therefore replace that sequence with something that will not error.
|
||||
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
|
||||
let expression = parse_expression_at(template, parser.ts, a);
|
||||
|
||||
// `foo: bar = baz` gets mangled — fix it
|
||||
if (expression.type === 'AssignmentExpression') {
|
||||
let b = expression.right.start;
|
||||
while (template[b] !== '=') b -= 1;
|
||||
expression = parse_expression_at(template.slice(0, b), parser.ts, a);
|
||||
}
|
||||
|
||||
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that
|
||||
if (expression.type === 'SequenceExpression') {
|
||||
expression = expression.expressions[0];
|
||||
}
|
||||
|
||||
parser.index = /** @type {number} */ (expression.end);
|
||||
return {
|
||||
type: 'TSTypeAnnotation',
|
||||
start,
|
||||
end: parser.index,
|
||||
typeAnnotation: /** @type {any} */ (expression).typeAnnotation
|
||||
};
|
||||
}
|
||||
81
node_modules/svelte/src/compiler/phases/1-parse/read/expression.js
generated
vendored
Normal file
81
node_modules/svelte/src/compiler/phases/1-parse/read/expression.js
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { parse_expression_at } from '../acorn.js';
|
||||
import { regex_whitespace } from '../../patterns.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import { find_matching_bracket } from '../utils/bracket.js';
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {string} [opening_token]
|
||||
* @returns {Expression | undefined}
|
||||
*/
|
||||
export function get_loose_identifier(parser, opening_token) {
|
||||
// Find the next } and treat it as the end of the expression
|
||||
const end = find_matching_bracket(parser.template, parser.index, opening_token ?? '{');
|
||||
if (end) {
|
||||
const start = parser.index;
|
||||
parser.index = end;
|
||||
// We don't know what the expression is and signal this by returning an empty identifier
|
||||
return {
|
||||
type: 'Identifier',
|
||||
start,
|
||||
end,
|
||||
name: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {string} [opening_token]
|
||||
* @param {boolean} [disallow_loose]
|
||||
* @returns {Expression}
|
||||
*/
|
||||
export default function read_expression(parser, opening_token, disallow_loose) {
|
||||
try {
|
||||
const node = parse_expression_at(parser.template, parser.ts, parser.index);
|
||||
|
||||
let num_parens = 0;
|
||||
|
||||
if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
|
||||
parser.index = node.leadingComments.at(-1).end;
|
||||
}
|
||||
|
||||
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
|
||||
if (parser.template[i] === '(') num_parens += 1;
|
||||
}
|
||||
|
||||
let index = /** @type {number} */ (node.end);
|
||||
if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
|
||||
index = node.trailingComments.at(-1).end;
|
||||
}
|
||||
|
||||
while (num_parens > 0) {
|
||||
const char = parser.template[index];
|
||||
|
||||
if (char === ')') {
|
||||
num_parens -= 1;
|
||||
} else if (!regex_whitespace.test(char)) {
|
||||
e.expected_token(index, ')');
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
parser.index = index;
|
||||
|
||||
return /** @type {Expression} */ (node);
|
||||
} catch (err) {
|
||||
// If we are in an each loop we need the error to be thrown in cases like
|
||||
// `as { y = z }` so we still throw and handle the error there
|
||||
if (parser.loose && !disallow_loose) {
|
||||
const expression = get_loose_identifier(parser, opening_token);
|
||||
if (expression) {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
|
||||
parser.acorn_error(err);
|
||||
}
|
||||
}
|
||||
261
node_modules/svelte/src/compiler/phases/1-parse/read/options.js
generated
vendored
Normal file
261
node_modules/svelte/src/compiler/phases/1-parse/read/options.js
generated
vendored
Normal file
@@ -0,0 +1,261 @@
|
||||
/** @import { ObjectExpression } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteOptionsRaw} node
|
||||
* @returns {AST.Root['options']}
|
||||
*/
|
||||
export default function read_options(node) {
|
||||
/** @type {AST.SvelteOptions} */
|
||||
const component_options = {
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
// @ts-ignore
|
||||
attributes: node.attributes
|
||||
};
|
||||
|
||||
if (!node) {
|
||||
return component_options;
|
||||
}
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type !== 'Attribute') {
|
||||
e.svelte_options_invalid_attribute(attribute);
|
||||
}
|
||||
|
||||
const { name } = attribute;
|
||||
|
||||
switch (name) {
|
||||
case 'runes': {
|
||||
component_options.runes = get_boolean_value(attribute);
|
||||
break;
|
||||
}
|
||||
case 'tag': {
|
||||
e.svelte_options_deprecated_tag(attribute);
|
||||
break; // eslint doesn't know this is unnecessary
|
||||
}
|
||||
case 'customElement': {
|
||||
/** @type {AST.SvelteOptions['customElement']} */
|
||||
const ce = {};
|
||||
const { value: v } = attribute;
|
||||
const value = v === true || Array.isArray(v) ? v : [v];
|
||||
|
||||
if (value === true) {
|
||||
e.svelte_options_invalid_customelement(attribute);
|
||||
} else if (value[0].type === 'Text') {
|
||||
const tag = get_static_value(attribute);
|
||||
validate_tag(attribute, tag);
|
||||
ce.tag = tag;
|
||||
component_options.customElement = ce;
|
||||
break;
|
||||
} else if (value[0].expression.type !== 'ObjectExpression') {
|
||||
// Before Svelte 4 it was necessary to explicitly set customElement to null or else you'd get a warning.
|
||||
// This is no longer necessary, but for backwards compat just skip in this case now.
|
||||
if (value[0].expression.type === 'Literal' && value[0].expression.value === null) {
|
||||
break;
|
||||
}
|
||||
e.svelte_options_invalid_customelement(attribute);
|
||||
}
|
||||
|
||||
/** @type {Array<[string, any]>} */
|
||||
const properties = [];
|
||||
for (const property of value[0].expression.properties) {
|
||||
if (
|
||||
property.type !== 'Property' ||
|
||||
property.computed ||
|
||||
property.key.type !== 'Identifier'
|
||||
) {
|
||||
e.svelte_options_invalid_customelement(attribute);
|
||||
}
|
||||
properties.push([property.key.name, property.value]);
|
||||
}
|
||||
|
||||
const tag = properties.find(([name]) => name === 'tag');
|
||||
if (tag) {
|
||||
const tag_value = tag[1]?.value;
|
||||
validate_tag(tag, tag_value);
|
||||
ce.tag = tag_value;
|
||||
}
|
||||
|
||||
const props = properties.find(([name]) => name === 'props')?.[1];
|
||||
if (props) {
|
||||
if (props.type !== 'ObjectExpression') {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props = {};
|
||||
for (const property of /** @type {ObjectExpression} */ (props).properties) {
|
||||
if (
|
||||
property.type !== 'Property' ||
|
||||
property.computed ||
|
||||
property.key.type !== 'Identifier' ||
|
||||
property.value.type !== 'ObjectExpression'
|
||||
) {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props[property.key.name] = {};
|
||||
for (const prop of property.value.properties) {
|
||||
if (
|
||||
prop.type !== 'Property' ||
|
||||
prop.computed ||
|
||||
prop.key.type !== 'Identifier' ||
|
||||
prop.value.type !== 'Literal'
|
||||
) {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
|
||||
if (prop.key.name === 'type') {
|
||||
if (
|
||||
['String', 'Number', 'Boolean', 'Array', 'Object'].indexOf(
|
||||
/** @type {string} */ (prop.value.value)
|
||||
) === -1
|
||||
) {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props[property.key.name].type = /** @type {any} */ (prop.value.value);
|
||||
} else if (prop.key.name === 'reflect') {
|
||||
if (typeof prop.value.value !== 'boolean') {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props[property.key.name].reflect = prop.value.value;
|
||||
} else if (prop.key.name === 'attribute') {
|
||||
if (typeof prop.value.value !== 'string') {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props[property.key.name].attribute = prop.value.value;
|
||||
} else {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shadow = properties.find(([name]) => name === 'shadow')?.[1];
|
||||
if (shadow) {
|
||||
const shadowdom = shadow?.value;
|
||||
if (shadowdom !== 'open' && shadowdom !== 'none') {
|
||||
e.svelte_options_invalid_customelement_shadow(shadow);
|
||||
}
|
||||
ce.shadow = shadowdom;
|
||||
}
|
||||
|
||||
const extend = properties.find(([name]) => name === 'extend')?.[1];
|
||||
if (extend) {
|
||||
ce.extend = extend;
|
||||
}
|
||||
|
||||
component_options.customElement = ce;
|
||||
break;
|
||||
}
|
||||
case 'namespace': {
|
||||
const value = get_static_value(attribute);
|
||||
|
||||
if (value === NAMESPACE_SVG) {
|
||||
component_options.namespace = 'svg';
|
||||
} else if (value === NAMESPACE_MATHML) {
|
||||
component_options.namespace = 'mathml';
|
||||
} else if (value === 'html' || value === 'mathml' || value === 'svg') {
|
||||
component_options.namespace = value;
|
||||
} else {
|
||||
e.svelte_options_invalid_attribute_value(attribute, `"html", "mathml" or "svg"`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'css': {
|
||||
const value = get_static_value(attribute);
|
||||
|
||||
if (value === 'injected') {
|
||||
component_options.css = value;
|
||||
} else {
|
||||
e.svelte_options_invalid_attribute_value(attribute, `"injected"`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'immutable': {
|
||||
component_options.immutable = get_boolean_value(attribute);
|
||||
break;
|
||||
}
|
||||
case 'preserveWhitespace': {
|
||||
component_options.preserveWhitespace = get_boolean_value(attribute);
|
||||
break;
|
||||
}
|
||||
case 'accessors': {
|
||||
component_options.accessors = get_boolean_value(attribute);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
e.svelte_options_unknown_attribute(attribute, name);
|
||||
}
|
||||
}
|
||||
|
||||
return component_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} attribute
|
||||
*/
|
||||
function get_static_value(attribute) {
|
||||
const { value } = attribute;
|
||||
|
||||
if (value === true) return true;
|
||||
|
||||
const chunk = Array.isArray(value) ? value[0] : value;
|
||||
|
||||
if (!chunk) return true;
|
||||
if (value.length > 1) {
|
||||
return null;
|
||||
}
|
||||
if (chunk.type === 'Text') return chunk.data;
|
||||
if (chunk.expression.type !== 'Literal') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chunk.expression.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} attribute
|
||||
*/
|
||||
function get_boolean_value(attribute) {
|
||||
const value = get_static_value(attribute);
|
||||
if (typeof value !== 'boolean') {
|
||||
e.svelte_options_invalid_attribute_value(attribute, 'true or false');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
|
||||
const tag_name_char =
|
||||
'[a-z0-9_.\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}-]';
|
||||
const regex_valid_tag_name = new RegExp(`^[a-z]${tag_name_char}*-${tag_name_char}*$`, 'u');
|
||||
const reserved_tag_names = [
|
||||
'annotation-xml',
|
||||
'color-profile',
|
||||
'font-face',
|
||||
'font-face-src',
|
||||
'font-face-uri',
|
||||
'font-face-format',
|
||||
'font-face-name',
|
||||
'missing-glyph'
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {any} attribute
|
||||
* @param {string | null} tag
|
||||
* @returns {asserts tag is string}
|
||||
*/
|
||||
function validate_tag(attribute, tag) {
|
||||
if (typeof tag !== 'string') {
|
||||
e.svelte_options_invalid_tagname(attribute);
|
||||
}
|
||||
if (tag) {
|
||||
if (!regex_valid_tag_name.test(tag)) {
|
||||
e.svelte_options_invalid_tagname(attribute);
|
||||
} else if (reserved_tag_names.includes(tag)) {
|
||||
e.svelte_options_reserved_tagname(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
node_modules/svelte/src/compiler/phases/1-parse/read/script.js
generated
vendored
Normal file
90
node_modules/svelte/src/compiler/phases/1-parse/read/script.js
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
/** @import { Program } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import * as acorn from '../acorn.js';
|
||||
import { regex_not_newline_characters } from '../../patterns.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { is_text_attribute } from '../../../utils/ast.js';
|
||||
|
||||
const regex_closing_script_tag = /<\/script\s*>/;
|
||||
const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
|
||||
|
||||
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>} attributes
|
||||
* @returns {AST.Script}
|
||||
*/
|
||||
export function read_script(parser, start, attributes) {
|
||||
const script_start = parser.index;
|
||||
const data = parser.read_until(regex_closing_script_tag);
|
||||
if (parser.index >= parser.template.length) {
|
||||
e.element_unclosed(parser.template.length, 'script');
|
||||
}
|
||||
|
||||
const source =
|
||||
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
|
||||
parser.read(regex_starts_with_closing_script_tag);
|
||||
|
||||
/** @type {Program} */
|
||||
let ast;
|
||||
|
||||
try {
|
||||
ast = acorn.parse(source, parser.ts, true);
|
||||
} catch (err) {
|
||||
parser.acorn_error(err);
|
||||
}
|
||||
|
||||
// TODO is this necessary?
|
||||
ast.start = script_start;
|
||||
|
||||
/** @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
|
||||
};
|
||||
}
|
||||
625
node_modules/svelte/src/compiler/phases/1-parse/read/style.js
generated
vendored
Normal file
625
node_modules/svelte/src/compiler/phases/1-parse/read/style.js
generated
vendored
Normal file
@@ -0,0 +1,625 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
const REGEX_MATCHER = /^[~^$*|]?=/;
|
||||
const REGEX_CLOSING_BRACKET = /[\s\]]/;
|
||||
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof
|
||||
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
|
||||
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
|
||||
const REGEX_NTH_OF =
|
||||
/^(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/;
|
||||
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
|
||||
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
|
||||
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
|
||||
const REGEX_COMMENT_CLOSE = /\*\//;
|
||||
const REGEX_HTML_COMMENT_CLOSE = /-->/;
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} start
|
||||
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
|
||||
* @returns {AST.CSS.StyleSheet}
|
||||
*/
|
||||
export default function read_style(parser, start, attributes) {
|
||||
const content_start = parser.index;
|
||||
const children = read_body(parser, '</style');
|
||||
const content_end = parser.index;
|
||||
|
||||
parser.read(/^<\/style\s*>/);
|
||||
|
||||
return {
|
||||
type: 'StyleSheet',
|
||||
start,
|
||||
end: parser.index,
|
||||
attributes,
|
||||
children,
|
||||
content: {
|
||||
start: content_start,
|
||||
end: content_end,
|
||||
styles: parser.template.slice(content_start, content_end),
|
||||
comment: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {string} close
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function read_body(parser, close) {
|
||||
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
|
||||
const children = [];
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
if (parser.match(close)) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (parser.match('@')) {
|
||||
children.push(read_at_rule(parser));
|
||||
} else {
|
||||
children.push(read_rule(parser));
|
||||
}
|
||||
}
|
||||
|
||||
e.expected_token(parser.template.length, close);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Atrule}
|
||||
*/
|
||||
function read_at_rule(parser) {
|
||||
const start = parser.index;
|
||||
parser.eat('@', true);
|
||||
|
||||
const name = read_identifier(parser);
|
||||
|
||||
const prelude = read_value(parser);
|
||||
|
||||
/** @type {AST.CSS.Block | null} */
|
||||
let block = null;
|
||||
|
||||
if (parser.match('{')) {
|
||||
// e.g. `@media (...) {...}`
|
||||
block = read_block(parser);
|
||||
} else {
|
||||
// e.g. `@import '...'`
|
||||
parser.eat(';', true);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Atrule',
|
||||
start,
|
||||
end: parser.index,
|
||||
name,
|
||||
prelude,
|
||||
block
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Rule}
|
||||
*/
|
||||
function read_rule(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
return {
|
||||
type: 'Rule',
|
||||
prelude: read_selector_list(parser),
|
||||
block: read_block(parser),
|
||||
start,
|
||||
end: parser.index,
|
||||
metadata: {
|
||||
parent_rule: null,
|
||||
has_local_selectors: false,
|
||||
is_global_block: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {boolean} [inside_pseudo_class]
|
||||
* @returns {AST.CSS.SelectorList}
|
||||
*/
|
||||
function read_selector_list(parser, inside_pseudo_class = false) {
|
||||
/** @type {AST.CSS.ComplexSelector[]} */
|
||||
const children = [];
|
||||
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
const start = parser.index;
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
children.push(read_selector(parser, inside_pseudo_class));
|
||||
|
||||
const end = parser.index;
|
||||
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
if (inside_pseudo_class ? parser.match(')') : parser.match('{')) {
|
||||
return {
|
||||
type: 'SelectorList',
|
||||
start,
|
||||
end,
|
||||
children
|
||||
};
|
||||
} else {
|
||||
parser.eat(',', true);
|
||||
allow_comment_or_whitespace(parser);
|
||||
}
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {boolean} [inside_pseudo_class]
|
||||
* @returns {AST.CSS.ComplexSelector}
|
||||
*/
|
||||
function read_selector(parser, inside_pseudo_class = false) {
|
||||
const list_start = parser.index;
|
||||
|
||||
/** @type {AST.CSS.RelativeSelector[]} */
|
||||
const children = [];
|
||||
|
||||
/**
|
||||
* @param {AST.CSS.Combinator | null} combinator
|
||||
* @param {number} start
|
||||
* @returns {AST.CSS.RelativeSelector}
|
||||
*/
|
||||
function create_selector(combinator, start) {
|
||||
return {
|
||||
type: 'RelativeSelector',
|
||||
combinator,
|
||||
selectors: [],
|
||||
start,
|
||||
end: -1,
|
||||
metadata: {
|
||||
is_global: false,
|
||||
is_global_like: false,
|
||||
scoped: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {AST.CSS.RelativeSelector} */
|
||||
let relative_selector = create_selector(null, parser.index);
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
let start = parser.index;
|
||||
|
||||
if (parser.eat('&')) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'NestingSelector',
|
||||
name: '&',
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('*')) {
|
||||
let name = '*';
|
||||
|
||||
if (parser.eat('|')) {
|
||||
// * is the namespace (which we ignore)
|
||||
name = read_identifier(parser);
|
||||
}
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'TypeSelector',
|
||||
name,
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('#')) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'IdSelector',
|
||||
name: read_identifier(parser),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('.')) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'ClassSelector',
|
||||
name: read_identifier(parser),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('::')) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'PseudoElementSelector',
|
||||
name: read_identifier(parser),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
// We read the inner selectors of a pseudo element to ensure it parses correctly,
|
||||
// but we don't do anything with the result.
|
||||
if (parser.eat('(')) {
|
||||
read_selector_list(parser, true);
|
||||
parser.eat(')', true);
|
||||
}
|
||||
} else if (parser.eat(':')) {
|
||||
const name = read_identifier(parser);
|
||||
|
||||
/** @type {null | AST.CSS.SelectorList} */
|
||||
let args = null;
|
||||
|
||||
if (parser.eat('(')) {
|
||||
args = read_selector_list(parser, true);
|
||||
parser.eat(')', true);
|
||||
}
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'PseudoClassSelector',
|
||||
name,
|
||||
args,
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('[')) {
|
||||
parser.allow_whitespace();
|
||||
const name = read_identifier(parser);
|
||||
parser.allow_whitespace();
|
||||
|
||||
/** @type {string | null} */
|
||||
let value = null;
|
||||
|
||||
const matcher = parser.read(REGEX_MATCHER);
|
||||
|
||||
if (matcher) {
|
||||
parser.allow_whitespace();
|
||||
value = read_attribute_value(parser);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const flags = parser.read(REGEX_ATTRIBUTE_FLAGS);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat(']', true);
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'AttributeSelector',
|
||||
start,
|
||||
end: parser.index,
|
||||
name,
|
||||
matcher,
|
||||
value,
|
||||
flags
|
||||
});
|
||||
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
|
||||
// nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'Nth',
|
||||
value: /**@type {string} */ (parser.read(REGEX_NTH_OF)),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.match_regex(REGEX_PERCENTAGE)) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'Percentage',
|
||||
value: /** @type {string} */ (parser.read(REGEX_PERCENTAGE)),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (!parser.match_regex(REGEX_COMBINATOR)) {
|
||||
let name = read_identifier(parser);
|
||||
|
||||
if (parser.eat('|')) {
|
||||
// we ignore the namespace when trying to find matching element classes
|
||||
name = read_identifier(parser);
|
||||
}
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'TypeSelector',
|
||||
name,
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
}
|
||||
|
||||
const index = parser.index;
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
|
||||
// rewind, so we know whether to continue building the selector list
|
||||
parser.index = index;
|
||||
|
||||
relative_selector.end = index;
|
||||
children.push(relative_selector);
|
||||
|
||||
return {
|
||||
type: 'ComplexSelector',
|
||||
start: list_start,
|
||||
end: index,
|
||||
children,
|
||||
metadata: {
|
||||
rule: null,
|
||||
used: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
parser.index = index;
|
||||
const combinator = read_combinator(parser);
|
||||
|
||||
if (combinator) {
|
||||
if (relative_selector.selectors.length > 0) {
|
||||
relative_selector.end = index;
|
||||
children.push(relative_selector);
|
||||
}
|
||||
|
||||
// ...and start a new one
|
||||
relative_selector = create_selector(combinator, combinator.start);
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
|
||||
e.css_selector_invalid(parser.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Combinator | null}
|
||||
*/
|
||||
function read_combinator(parser) {
|
||||
const start = parser.index;
|
||||
parser.allow_whitespace();
|
||||
|
||||
const index = parser.index;
|
||||
const name = parser.read(REGEX_COMBINATOR);
|
||||
|
||||
if (name) {
|
||||
const end = parser.index;
|
||||
parser.allow_whitespace();
|
||||
|
||||
return {
|
||||
type: 'Combinator',
|
||||
name,
|
||||
start: index,
|
||||
end
|
||||
};
|
||||
}
|
||||
|
||||
if (parser.index !== start) {
|
||||
return {
|
||||
type: 'Combinator',
|
||||
name: ' ',
|
||||
start,
|
||||
end: parser.index
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Block}
|
||||
*/
|
||||
function read_block(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
parser.eat('{', true);
|
||||
|
||||
/** @type {Array<AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule>} */
|
||||
const children = [];
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
if (parser.match('}')) {
|
||||
break;
|
||||
} else {
|
||||
children.push(read_block_item(parser));
|
||||
}
|
||||
}
|
||||
|
||||
parser.eat('}', true);
|
||||
|
||||
return {
|
||||
type: 'Block',
|
||||
start,
|
||||
end: parser.index,
|
||||
children
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a declaration, rule or at-rule
|
||||
*
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule}
|
||||
*/
|
||||
function read_block_item(parser) {
|
||||
if (parser.match('@')) {
|
||||
return read_at_rule(parser);
|
||||
}
|
||||
|
||||
// read ahead to understand whether we're dealing with a declaration or a nested rule.
|
||||
// this involves some duplicated work, but avoids a try-catch that would disguise errors
|
||||
const start = parser.index;
|
||||
read_value(parser);
|
||||
const char = parser.template[parser.index];
|
||||
parser.index = start;
|
||||
|
||||
return char === '{' ? read_rule(parser) : read_declaration(parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Declaration}
|
||||
*/
|
||||
function read_declaration(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
const property = parser.read_until(REGEX_WHITESPACE_OR_COLON);
|
||||
parser.allow_whitespace();
|
||||
parser.eat(':');
|
||||
let index = parser.index;
|
||||
parser.allow_whitespace();
|
||||
|
||||
const value = read_value(parser);
|
||||
|
||||
if (!value && !property.startsWith('--')) {
|
||||
e.css_empty_declaration({ start, end: index });
|
||||
}
|
||||
|
||||
const end = parser.index;
|
||||
|
||||
if (!parser.match('}')) {
|
||||
parser.eat(';', true);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Declaration',
|
||||
start,
|
||||
end,
|
||||
property,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {string}
|
||||
*/
|
||||
function read_value(parser) {
|
||||
let value = '';
|
||||
let escaped = false;
|
||||
let in_url = false;
|
||||
|
||||
/** @type {null | '"' | "'"} */
|
||||
let quote_mark = null;
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const char = parser.template[parser.index];
|
||||
|
||||
if (escaped) {
|
||||
value += '\\' + char;
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === quote_mark) {
|
||||
quote_mark = null;
|
||||
} else if (char === ')') {
|
||||
in_url = false;
|
||||
} else if (quote_mark === null && (char === '"' || char === "'")) {
|
||||
quote_mark = char;
|
||||
} else if (char === '(' && value.slice(-3) === 'url') {
|
||||
in_url = true;
|
||||
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
value += char;
|
||||
|
||||
parser.index++;
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a property that may or may not be quoted, e.g.
|
||||
* `foo` or `'foo bar'` or `"foo bar"`
|
||||
* @param {Parser} parser
|
||||
*/
|
||||
function read_attribute_value(parser) {
|
||||
let value = '';
|
||||
let escaped = false;
|
||||
const quote_mark = parser.eat('"') ? '"' : parser.eat("'") ? "'" : null;
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const char = parser.template[parser.index];
|
||||
if (escaped) {
|
||||
value += '\\' + char;
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (quote_mark ? char === quote_mark : REGEX_CLOSING_BRACKET.test(char)) {
|
||||
if (quote_mark) {
|
||||
parser.eat(quote_mark, true);
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
} else {
|
||||
value += char;
|
||||
}
|
||||
|
||||
parser.index++;
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
|
||||
* @param {Parser} parser
|
||||
*/
|
||||
function read_identifier(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
let identifier = '';
|
||||
|
||||
if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
|
||||
e.css_expected_identifier(start);
|
||||
}
|
||||
|
||||
let escaped = false;
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const char = parser.template[parser.index];
|
||||
if (escaped) {
|
||||
identifier += '\\' + char;
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (
|
||||
/** @type {number} */ (char.codePointAt(0)) >= 160 ||
|
||||
REGEX_VALID_IDENTIFIER_CHAR.test(char)
|
||||
) {
|
||||
identifier += char;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
parser.index++;
|
||||
}
|
||||
|
||||
if (identifier === '') {
|
||||
e.css_expected_identifier(start);
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function allow_comment_or_whitespace(parser) {
|
||||
parser.allow_whitespace();
|
||||
while (parser.match('/*') || parser.match('<!--')) {
|
||||
if (parser.eat('/*')) {
|
||||
parser.read_until(REGEX_COMMENT_CLOSE);
|
||||
parser.eat('*/', true);
|
||||
}
|
||||
|
||||
if (parser.eat('<!--')) {
|
||||
parser.read_until(REGEX_HTML_COMMENT_CLOSE);
|
||||
parser.eat('-->', true);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
}
|
||||
147
node_modules/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
generated
vendored
Normal file
147
node_modules/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
generated
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
/** @import { Context, Visitors } from 'zimmerframe' */
|
||||
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
|
||||
import { walk } from 'zimmerframe';
|
||||
import * as b from '../../utils/builders.js';
|
||||
import * as e from '../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {FunctionExpression | FunctionDeclaration} node
|
||||
* @param {Context<any, any>} context
|
||||
*/
|
||||
function remove_this_param(node, context) {
|
||||
if (node.params[0]?.type === 'Identifier' && node.params[0].name === 'this') {
|
||||
node.params.shift();
|
||||
}
|
||||
return context.next();
|
||||
}
|
||||
|
||||
/** @type {Visitors<any, null>} */
|
||||
const visitors = {
|
||||
_(node, context) {
|
||||
const n = context.next() ?? node;
|
||||
|
||||
// TODO there may come a time when we decide to preserve type annotations.
|
||||
// until that day comes, we just delete them so they don't confuse esrap
|
||||
delete n.typeAnnotation;
|
||||
delete n.typeParameters;
|
||||
delete n.returnType;
|
||||
delete n.accessibility;
|
||||
},
|
||||
Decorator(node) {
|
||||
e.typescript_invalid_feature(node, 'decorators (related TSC proposal is not stage 4 yet)');
|
||||
},
|
||||
ImportDeclaration(node) {
|
||||
if (node.importKind === 'type') return b.empty;
|
||||
|
||||
if (node.specifiers?.length > 0) {
|
||||
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.importKind !== 'type');
|
||||
if (specifiers.length === 0) return b.empty;
|
||||
|
||||
return { ...node, specifiers };
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
ExportNamedDeclaration(node, context) {
|
||||
if (node.exportKind === 'type') return b.empty;
|
||||
|
||||
if (node.declaration) {
|
||||
const result = context.next();
|
||||
if (result?.declaration?.type === 'EmptyStatement') {
|
||||
return b.empty;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (node.specifiers) {
|
||||
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.exportKind !== 'type');
|
||||
if (specifiers.length === 0) return b.empty;
|
||||
|
||||
return { ...node, specifiers };
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
ExportDefaultDeclaration(node) {
|
||||
if (node.exportKind === 'type') return b.empty;
|
||||
return node;
|
||||
},
|
||||
ExportAllDeclaration(node) {
|
||||
if (node.exportKind === 'type') return b.empty;
|
||||
return node;
|
||||
},
|
||||
PropertyDefinition(node, { next }) {
|
||||
if (node.accessor) {
|
||||
e.typescript_invalid_feature(
|
||||
node,
|
||||
'accessor fields (related TSC proposal is not stage 4 yet)'
|
||||
);
|
||||
}
|
||||
return next();
|
||||
},
|
||||
TSAsExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
TSSatisfiesExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
TSNonNullExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
TSInterfaceDeclaration() {
|
||||
return b.empty;
|
||||
},
|
||||
TSTypeAliasDeclaration() {
|
||||
return b.empty;
|
||||
},
|
||||
TSEnumDeclaration(node) {
|
||||
e.typescript_invalid_feature(node, 'enums');
|
||||
},
|
||||
TSParameterProperty(node, context) {
|
||||
if ((node.readonly || node.accessibility) && context.path.at(-2)?.kind === 'constructor') {
|
||||
e.typescript_invalid_feature(node, 'accessibility modifiers on constructor parameters');
|
||||
}
|
||||
return context.visit(node.parameter);
|
||||
},
|
||||
TSInstantiationExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
FunctionExpression: remove_this_param,
|
||||
FunctionDeclaration: remove_this_param,
|
||||
TSDeclareFunction() {
|
||||
return b.empty;
|
||||
},
|
||||
ClassDeclaration(node, context) {
|
||||
if (node.declare) {
|
||||
return b.empty;
|
||||
}
|
||||
delete node.implements;
|
||||
return context.next();
|
||||
},
|
||||
VariableDeclaration(node, context) {
|
||||
if (node.declare) {
|
||||
return b.empty;
|
||||
}
|
||||
return context.next();
|
||||
},
|
||||
TSModuleDeclaration(node, context) {
|
||||
if (!node.body) return b.empty;
|
||||
|
||||
// namespaces can contain non-type nodes
|
||||
const cleaned = /** @type {any[]} */ (node.body.body).map((entry) => context.visit(entry));
|
||||
if (cleaned.some((entry) => entry !== b.empty)) {
|
||||
e.typescript_invalid_feature(node, 'namespaces with non-type nodes');
|
||||
}
|
||||
|
||||
return b.empty;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} ast
|
||||
* @returns {T}
|
||||
*/
|
||||
export function remove_typescript_nodes(ast) {
|
||||
return walk(ast, null, visitors);
|
||||
}
|
||||
823
node_modules/svelte/src/compiler/phases/1-parse/state/element.js
generated
vendored
Normal file
823
node_modules/svelte/src/compiler/phases/1-parse/state/element.js
generated
vendored
Normal file
@@ -0,0 +1,823 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { is_void } from '../../../../utils.js';
|
||||
import read_expression from '../read/expression.js';
|
||||
import { read_script } from '../read/script.js';
|
||||
import read_style from '../read/style.js';
|
||||
import { decode_character_references } from '../utils/html.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { create_fragment } from '../utils/create.js';
|
||||
import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
|
||||
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
|
||||
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
|
||||
import { list } from '../../../utils/string.js';
|
||||
import { regex_whitespace } from '../../patterns.js';
|
||||
|
||||
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
|
||||
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
|
||||
const regex_closing_comment = /-->/;
|
||||
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
|
||||
const regex_token_ending_character = /[\s=/>"']/;
|
||||
const regex_starts_with_quote_characters = /^["']/;
|
||||
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
|
||||
const regex_valid_element_name =
|
||||
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
|
||||
export const regex_valid_component_name =
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
|
||||
// (must start with uppercase letter if no dots, can contain dots)
|
||||
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;
|
||||
|
||||
/** @type {Map<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(regex_closing_comment);
|
||||
parser.eat('-->', true);
|
||||
|
||||
parser.append({
|
||||
type: 'Comment',
|
||||
start,
|
||||
end: parser.index,
|
||||
data
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const is_closing_tag = parser.eat('/');
|
||||
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
|
||||
|
||||
if (is_closing_tag) {
|
||||
parser.allow_whitespace();
|
||||
parser.eat('>', true);
|
||||
|
||||
if (is_void(name)) {
|
||||
e.void_element_invalid_content(start);
|
||||
}
|
||||
|
||||
// close any elements that don't have their own closing tags, e.g. <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' && !parser.loose) {
|
||||
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
|
||||
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
|
||||
} else {
|
||||
e.element_invalid_closing_tag(start, name);
|
||||
}
|
||||
}
|
||||
|
||||
parent.end = start;
|
||||
parser.pop();
|
||||
|
||||
parent = parser.current();
|
||||
}
|
||||
|
||||
parent.end = parser.index;
|
||||
parser.pop();
|
||||
|
||||
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
|
||||
parser.last_auto_closed_tag = undefined;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
|
||||
const bounds = { start: start + 1, end: start + 1 + name.length };
|
||||
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
|
||||
}
|
||||
|
||||
if (!regex_valid_element_name.test(name) && !regex_valid_component_name.test(name)) {
|
||||
// <div. -> in the middle of typing -> allow in loose mode
|
||||
if (!parser.loose || !name.endsWith('.')) {
|
||||
const bounds = { start: start + 1, end: start + 1 + name.length };
|
||||
e.tag_invalid_name(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (root_only_meta_tags.has(name)) {
|
||||
if (name in parser.meta_tags) {
|
||||
e.svelte_meta_duplicate(start, name);
|
||||
}
|
||||
|
||||
if (parent.type !== 'Root') {
|
||||
e.svelte_meta_invalid_placement(start, name);
|
||||
}
|
||||
|
||||
parser.meta_tags[name] = true;
|
||||
}
|
||||
|
||||
const type = meta_tags.has(name)
|
||||
? meta_tags.get(name)
|
||||
: regex_valid_component_name.test(name) || (parser.loose && name.endsWith('.'))
|
||||
? 'Component'
|
||||
: name === 'title' && parent_is_head(parser.stack)
|
||||
? 'TitleElement'
|
||||
: // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element
|
||||
name === 'slot' && !parent_is_shadowroot_template(parser.stack)
|
||||
? 'SlotElement'
|
||||
: 'RegularElement';
|
||||
|
||||
/** @type {AST.ElementLike} */
|
||||
const element =
|
||||
type === 'RegularElement'
|
||||
? {
|
||||
type,
|
||||
start,
|
||||
end: -1,
|
||||
name,
|
||||
attributes: [],
|
||||
fragment: create_fragment(true),
|
||||
metadata: {
|
||||
svg: false,
|
||||
mathml: false,
|
||||
scoped: false,
|
||||
has_spread: false,
|
||||
path: []
|
||||
}
|
||||
}
|
||||
: /** @type {AST.ElementLike} */ ({
|
||||
type,
|
||||
start,
|
||||
end: -1,
|
||||
name,
|
||||
attributes: [],
|
||||
fragment: create_fragment(true),
|
||||
metadata: {
|
||||
// unpopulated at first, differs between types
|
||||
}
|
||||
});
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
|
||||
parent.end = start;
|
||||
parser.pop();
|
||||
parser.last_auto_closed_tag = {
|
||||
tag: parent.name,
|
||||
reason: name,
|
||||
depth: parser.stack.length
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const unique_names = [];
|
||||
|
||||
const current = parser.current();
|
||||
const is_top_level_script_or_style =
|
||||
(name === 'script' || name === 'style') && current.type === 'Root';
|
||||
|
||||
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
|
||||
|
||||
let attribute;
|
||||
while ((attribute = read(parser))) {
|
||||
// animate and transition can only be specified once per element so no need
|
||||
// to check here, use can be used multiple times, same for the on directive
|
||||
// finally let already has error handling in case of duplicate variable names
|
||||
if (
|
||||
attribute.type === 'Attribute' ||
|
||||
attribute.type === 'BindDirective' ||
|
||||
attribute.type === 'StyleDirective' ||
|
||||
attribute.type === 'ClassDirective'
|
||||
) {
|
||||
// `bind:attribute` and `attribute` are just the same but `class:attribute`,
|
||||
// `style:attribute` and `attribute` are different and should be allowed together
|
||||
// so we concatenate the type while normalizing the type for BindDirective
|
||||
const type = attribute.type === 'BindDirective' ? 'Attribute' : attribute.type;
|
||||
if (unique_names.includes(type + attribute.name)) {
|
||||
e.attribute_duplicate(attribute);
|
||||
// <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 === 'SvelteComponent') {
|
||||
const index = element.attributes.findIndex(
|
||||
/** @param {any} attr */
|
||||
(attr) => attr.type === 'Attribute' && attr.name === 'this'
|
||||
);
|
||||
if (index === -1) {
|
||||
e.svelte_component_missing_this(start);
|
||||
}
|
||||
|
||||
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
|
||||
if (!is_expression_attribute(definition)) {
|
||||
e.svelte_component_invalid_this(definition.start);
|
||||
}
|
||||
|
||||
element.expression = get_attribute_expression(definition);
|
||||
}
|
||||
|
||||
if (element.type === 'SvelteElement') {
|
||||
const index = element.attributes.findIndex(
|
||||
/** @param {any} attr */
|
||||
(attr) => attr.type === 'Attribute' && attr.name === 'this'
|
||||
);
|
||||
if (index === -1) {
|
||||
e.svelte_element_missing_this(start);
|
||||
}
|
||||
|
||||
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
|
||||
|
||||
if (definition.value === true) {
|
||||
e.svelte_element_missing_this(definition);
|
||||
}
|
||||
|
||||
if (!is_expression_attribute(definition)) {
|
||||
w.svelte_element_invalid_this(definition);
|
||||
|
||||
// note that this is wrong, in the case of e.g. `this="h{n}"` — it will result in `<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);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_top_level_script_or_style) {
|
||||
parser.eat('>', true);
|
||||
|
||||
/** @type {AST.Comment | null} */
|
||||
let prev_comment = null;
|
||||
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
|
||||
const node = current.fragment.nodes[i];
|
||||
|
||||
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.type === 'Comment') {
|
||||
prev_comment = node;
|
||||
break;
|
||||
} else if (node.type !== 'Text' || node.data.trim()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'script') {
|
||||
const content = read_script(parser, start, element.attributes);
|
||||
if (prev_comment) {
|
||||
// We take advantage of the fact that the root will never have leadingComments set,
|
||||
// and set the previous comment to it so that the warning mechanism can later
|
||||
// inspect the root and see if there was a html comment before it silencing specific warnings.
|
||||
content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }];
|
||||
}
|
||||
|
||||
if (content.context === 'module') {
|
||||
if (current.module) e.script_duplicate(start);
|
||||
current.module = content;
|
||||
} else {
|
||||
if (current.instance) e.script_duplicate(start);
|
||||
current.instance = content;
|
||||
}
|
||||
} else {
|
||||
const content = read_style(parser, start, element.attributes);
|
||||
content.content.comment = prev_comment;
|
||||
|
||||
if (current.css) e.style_duplicate(start);
|
||||
current.css = content;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
parser.append(element);
|
||||
|
||||
const self_closing = parser.eat('/') || is_void(name);
|
||||
const closed = parser.eat('>', true, false);
|
||||
|
||||
// Loose parsing mode
|
||||
if (!closed) {
|
||||
// We may have eaten an opening `<` of the next element and treated it as an attribute...
|
||||
const last = element.attributes.at(-1);
|
||||
if (last?.type === 'Attribute' && last.name === '<') {
|
||||
parser.index = last.start;
|
||||
element.attributes.pop();
|
||||
} else {
|
||||
// ... or we may have eaten part of a following block ...
|
||||
const prev_1 = parser.template[parser.index - 1];
|
||||
const prev_2 = parser.template[parser.index - 2];
|
||||
const current = parser.template[parser.index];
|
||||
if (prev_2 === '{' && prev_1 === '/') {
|
||||
parser.index -= 2;
|
||||
} else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) {
|
||||
parser.index -= 1;
|
||||
} else {
|
||||
// ... or we're followed by whitespace, for example near the end of the template,
|
||||
// which we want to take in so that language tools has more room to work with
|
||||
parser.allow_whitespace();
|
||||
if (parser.index === parser.template.length) {
|
||||
while (
|
||||
parser.index < parser.template_untrimmed.length &&
|
||||
regex_whitespace.test(parser.template_untrimmed[parser.index])
|
||||
) {
|
||||
parser.index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self_closing || !closed) {
|
||||
// don't push self-closing elements onto the stack
|
||||
element.end = parser.index;
|
||||
} else if (name === 'textarea') {
|
||||
// special case
|
||||
element.fragment.nodes = read_sequence(
|
||||
parser,
|
||||
() => regex_closing_textarea_tag.test(parser.template.slice(parser.index)),
|
||||
'inside <textarea>'
|
||||
);
|
||||
parser.read(regex_closing_textarea_tag);
|
||||
element.end = parser.index;
|
||||
} else if (name === 'script' || name === 'style') {
|
||||
// special case
|
||||
const start = parser.index;
|
||||
const data = parser.read_until(new RegExp(`</${name}>`));
|
||||
const end = parser.index;
|
||||
|
||||
/** @type {AST.Text} */
|
||||
const node = {
|
||||
start,
|
||||
end,
|
||||
type: 'Text',
|
||||
data,
|
||||
raw: data
|
||||
};
|
||||
|
||||
element.fragment.nodes.push(node);
|
||||
parser.eat(`</${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 name = parser.read_until(regex_token_ending_character);
|
||||
if (!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(name, start, parser.index, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
|
||||
*/
|
||||
function read_attribute(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
if (parser.eat('{')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
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: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
|
||||
return spread;
|
||||
} else {
|
||||
const value_start = parser.index;
|
||||
let name = parser.read_identifier();
|
||||
|
||||
if (name === null) {
|
||||
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
|
||||
name = '';
|
||||
} else {
|
||||
e.attribute_empty_shorthand(start);
|
||||
}
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.ExpressionTag} */
|
||||
const expression = {
|
||||
type: 'ExpressionTag',
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
expression: {
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
type: 'Identifier',
|
||||
name
|
||||
},
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
|
||||
return create_attribute(name, start, parser.index, expression);
|
||||
}
|
||||
}
|
||||
|
||||
const name = parser.read_until(regex_token_ending_character);
|
||||
if (!name) return null;
|
||||
|
||||
let end = parser.index;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const colon_index = name.indexOf(':');
|
||||
const type = colon_index !== -1 && get_directive_type(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] = name.slice(colon_index + 1).split('|');
|
||||
|
||||
if (directive_name === '') {
|
||||
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
|
||||
}
|
||||
|
||||
if (type === 'StyleDirective') {
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
modifiers: /** @type {Array<'important'>} */ (modifiers),
|
||||
value,
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {AST.Directive} */
|
||||
const directive = {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
|
||||
directive.modifiers = modifiers;
|
||||
|
||||
if (directive.type === 'TransitionDirective') {
|
||||
const direction = 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
|
||||
});
|
||||
}
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
return create_attribute(name, start, end, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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'
|
||||
);
|
||||
} catch (/** @type {any} */ error) {
|
||||
if (error.code === 'js_parse_error') {
|
||||
// if the attribute value didn't close + self-closing tag
|
||||
// eg: `<Component test={{a:1} />`
|
||||
// acorn may throw a `Unterminated regular expression` because of `/>`
|
||||
const pos = error.position?.[0];
|
||||
if (pos !== undefined && parser.template.slice(pos - 1, pos + 1) === '/>') {
|
||||
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
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function read_sequence(parser, done, location) {
|
||||
/** @type {AST.Text} */
|
||||
let current_chunk = {
|
||||
start: parser.index,
|
||||
end: -1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
};
|
||||
|
||||
/** @type {Array<AST.Text | AST.ExpressionTag>} */
|
||||
const chunks = [];
|
||||
|
||||
/** @param {number} end */
|
||||
function flush(end) {
|
||||
if (current_chunk.raw) {
|
||||
current_chunk.data = decode_character_references(current_chunk.raw, true);
|
||||
current_chunk.end = end;
|
||||
chunks.push(current_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
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(/[^a-z]/);
|
||||
e.block_invalid_placement(index, name, location);
|
||||
} else if (parser.match('@')) {
|
||||
const index = parser.index - 1;
|
||||
parser.eat('@');
|
||||
const name = parser.read_until(/[^a-z]/);
|
||||
e.tag_invalid_placement(index, name, location);
|
||||
}
|
||||
|
||||
flush(parser.index - 1);
|
||||
|
||||
parser.allow_whitespace();
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.ExpressionTag} */
|
||||
const chunk = {
|
||||
type: 'ExpressionTag',
|
||||
start: index,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
|
||||
chunks.push(chunk);
|
||||
|
||||
current_chunk = {
|
||||
start: parser.index,
|
||||
end: -1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
};
|
||||
} else {
|
||||
current_chunk.raw += parser.template[parser.index++];
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.loose) {
|
||||
return chunks;
|
||||
} else {
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
}
|
||||
17
node_modules/svelte/src/compiler/phases/1-parse/state/fragment.js
generated
vendored
Normal file
17
node_modules/svelte/src/compiler/phases/1-parse/state/fragment.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/** @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;
|
||||
}
|
||||
715
node_modules/svelte/src/compiler/phases/1-parse/state/tag.js
generated
vendored
Normal file
715
node_modules/svelte/src/compiler/phases/1-parse/state/tag.js
generated
vendored
Normal file
@@ -0,0 +1,715 @@
|
||||
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { walk } from 'zimmerframe';
|
||||
import * as e from '../../../errors.js';
|
||||
import { create_expression_metadata } from '../../nodes.js';
|
||||
import { parse_expression_at } from '../acorn.js';
|
||||
import read_pattern from '../read/context.js';
|
||||
import read_expression, { get_loose_identifier } from '../read/expression.js';
|
||||
import { create_fragment } from '../utils/create.js';
|
||||
|
||||
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
|
||||
|
||||
/** @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 expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'ExpressionTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @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
|
||||
});
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.consequent);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('each')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const template = parser.template;
|
||||
let end = parser.template.length;
|
||||
|
||||
/** @type {Expression | undefined} */
|
||||
let expression;
|
||||
|
||||
// we have to do this loop because `{#each x as { y = z }}` fails to parse —
|
||||
// the `as { y = z }` is treated as an Expression but it's actually a Pattern.
|
||||
// the 'fix' is to backtrack and hide everything from the `as` onwards, until
|
||||
// we get a valid expression
|
||||
while (!expression) {
|
||||
try {
|
||||
expression = read_expression(parser, undefined, true);
|
||||
} catch (err) {
|
||||
end = /** @type {any} */ (err).position[0] - 2;
|
||||
|
||||
while (end > start && parser.template.slice(end, end + 2) !== 'as') {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
if (end <= start) {
|
||||
if (parser.loose) {
|
||||
expression = get_loose_identifier(parser);
|
||||
if (expression) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// @ts-expect-error parser.template is meant to be readonly, this is a special case
|
||||
parser.template = template.slice(0, end);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
parser.template = template;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
// {#each} blocks must declare a context – {#each list as item}
|
||||
if (!parser.match('as')) {
|
||||
// this could be a TypeScript assertion that was erroneously eaten.
|
||||
|
||||
if (expression.type === 'SequenceExpression') {
|
||||
expression = expression.expressions[0];
|
||||
}
|
||||
|
||||
let assertion = null;
|
||||
let end = expression.end;
|
||||
|
||||
expression = walk(expression, null, {
|
||||
// @ts-expect-error
|
||||
TSAsExpression(node, context) {
|
||||
if (node.end === /** @type {Expression} */ (expression).end) {
|
||||
assertion = node;
|
||||
end = node.expression.end;
|
||||
return node.expression;
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
});
|
||||
|
||||
expression.end = end;
|
||||
|
||||
if (assertion) {
|
||||
// we can't reset `parser.index` to `expression.expression.end` because
|
||||
// it will ignore any parentheses — we need to jump through this hoop
|
||||
let end = /** @type {any} */ (/** @type {any} */ (assertion).typeAnnotation).start - 2;
|
||||
while (parser.template.slice(end, end + 2) !== 'as') end -= 1;
|
||||
|
||||
parser.index = end;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Pattern | null} */
|
||||
let context = null;
|
||||
let index;
|
||||
let key;
|
||||
|
||||
if (parser.eat('as')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
context = read_pattern(parser);
|
||||
} else {
|
||||
// {#each Array.from({ length: 10 }), i} is read as a sequence expression,
|
||||
// which is set back above - we now gotta reset the index as a consequence
|
||||
// to properly read the , i part
|
||||
parser.index = /** @type {number} */ (expression.end);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat(',')) {
|
||||
parser.allow_whitespace();
|
||||
index = parser.read_identifier();
|
||||
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();
|
||||
}
|
||||
|
||||
const matches = parser.eat('}', true, false);
|
||||
|
||||
if (!matches) {
|
||||
// Parser may have read the `as` as part of the expression (e.g. in `{#each foo. as x}`)
|
||||
if (parser.template.slice(parser.index - 4, parser.index) === ' as ') {
|
||||
const prev_index = parser.index;
|
||||
context = read_pattern(parser);
|
||||
parser.eat('}', true);
|
||||
expression = {
|
||||
type: 'Identifier',
|
||||
name: '',
|
||||
start: expression.start,
|
||||
end: prev_index - 4
|
||||
};
|
||||
} else {
|
||||
parser.eat('}', true); // rerun to produce the parser error
|
||||
}
|
||||
}
|
||||
|
||||
/** @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);
|
||||
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
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const matches = parser.eat('}', true, false);
|
||||
|
||||
// Parser may have read the `then/catch` as part of the expression (e.g. in `{#await foo. then x}`)
|
||||
if (!matches) {
|
||||
if (parser.template.slice(parser.index - 6, parser.index) === ' then ') {
|
||||
const prev_index = parser.index;
|
||||
block.value = read_pattern(parser);
|
||||
parser.eat('}', true);
|
||||
block.expression = {
|
||||
type: 'Identifier',
|
||||
name: '',
|
||||
start: expression.start,
|
||||
end: prev_index - 6
|
||||
};
|
||||
block.then = block.pending;
|
||||
block.pending = null;
|
||||
} else if (parser.template.slice(parser.index - 7, parser.index) === ' catch ') {
|
||||
const prev_index = parser.index;
|
||||
block.error = read_pattern(parser);
|
||||
parser.eat('}', true);
|
||||
block.expression = {
|
||||
type: 'Identifier',
|
||||
name: '',
|
||||
start: expression.start,
|
||||
end: prev_index - 7
|
||||
};
|
||||
block.catch = block.pending;
|
||||
block.pending = null;
|
||||
} else {
|
||||
parser.eat('}', true); // rerun to produce the parser error
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
});
|
||||
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.fragment);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('snippet')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const name_start = parser.index;
|
||||
let name = parser.read_identifier();
|
||||
const name_end = parser.index;
|
||||
|
||||
if (name === null) {
|
||||
if (parser.loose) {
|
||||
name = '';
|
||||
} else {
|
||||
e.expected_identifier(parser.index);
|
||||
}
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const params_start = parser.index;
|
||||
|
||||
const matched = parser.eat('(', true, false);
|
||||
|
||||
if (matched) {
|
||||
let parentheses = 1;
|
||||
|
||||
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) {
|
||||
if (parser.match('(')) parentheses++;
|
||||
if (parser.match(')')) parentheses--;
|
||||
parser.index += 1;
|
||||
}
|
||||
|
||||
parser.eat(')', true);
|
||||
}
|
||||
|
||||
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
|
||||
const params = parser.template.slice(params_start, parser.index);
|
||||
|
||||
let function_expression = matched
|
||||
? /** @type {ArrowFunctionExpression} */ (
|
||||
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
|
||||
)
|
||||
: { params: [] };
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.SnippetBlock} */
|
||||
const block = parser.append({
|
||||
type: 'SnippetBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression: {
|
||||
type: 'Identifier',
|
||||
start: name_start,
|
||||
end: name_end,
|
||||
name
|
||||
},
|
||||
parameters: function_expression.params,
|
||||
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
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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 expression_start = parser.index;
|
||||
const init = read_expression(parser);
|
||||
if (
|
||||
init.type === 'SequenceExpression' &&
|
||||
!parser.template.substring(expression_start, init.start).includes('(')
|
||||
) {
|
||||
// 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: init.end }],
|
||||
start: start + 2, // start at const, not at @const
|
||||
end: parser.index - 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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: {
|
||||
dynamic: false,
|
||||
arguments: [],
|
||||
path: [],
|
||||
snippets: new Set()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
23
node_modules/svelte/src/compiler/phases/1-parse/state/text.js
generated
vendored
Normal file
23
node_modules/svelte/src/compiler/phases/1-parse/state/text.js
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/** @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;
|
||||
|
||||
let data = '';
|
||||
|
||||
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
|
||||
data += parser.template[parser.index++];
|
||||
}
|
||||
|
||||
/** @type {AST.Text} */
|
||||
parser.append({
|
||||
type: 'Text',
|
||||
start,
|
||||
end: parser.index,
|
||||
raw: data,
|
||||
data: decode_character_references(data, false)
|
||||
});
|
||||
}
|
||||
164
node_modules/svelte/src/compiler/phases/1-parse/utils/bracket.js
generated
vendored
Normal file
164
node_modules/svelte/src/compiler/phases/1-parse/utils/bracket.js
generated
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
const SQUARE_BRACKET_OPEN = '[';
|
||||
const SQUARE_BRACKET_CLOSE = ']';
|
||||
const CURLY_BRACKET_OPEN = '{';
|
||||
const CURLY_BRACKET_CLOSE = '}';
|
||||
const PARENTHESES_OPEN = '(';
|
||||
const PARENTHESES_CLOSE = ')';
|
||||
|
||||
/** @param {string} char */
|
||||
export function is_bracket_open(char) {
|
||||
return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN;
|
||||
}
|
||||
|
||||
/** @param {string} char */
|
||||
export function is_bracket_close(char) {
|
||||
return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE;
|
||||
}
|
||||
|
||||
/** @param {string} open */
|
||||
export function get_bracket_close(open) {
|
||||
if (open === SQUARE_BRACKET_OPEN) {
|
||||
return SQUARE_BRACKET_CLOSE;
|
||||
}
|
||||
|
||||
if (open === CURLY_BRACKET_OPEN) {
|
||||
return CURLY_BRACKET_CLOSE;
|
||||
}
|
||||
|
||||
if (open === PARENTHESES_OPEN) {
|
||||
return PARENTHESES_CLOSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} num
|
||||
* @returns {number} Infinity if {@link num} is negative, else {@link num}.
|
||||
*/
|
||||
function infinity_if_negative(num) {
|
||||
if (num < 0) {
|
||||
return Infinity;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} string The string to search.
|
||||
* @param {number} search_start_index The index to start searching at.
|
||||
* @param {"'" | '"' | '`'} string_start_char The character that started this string.
|
||||
* @returns {number} The index of the end of this string expression, or `Infinity` if not found.
|
||||
*/
|
||||
function find_string_end(string, search_start_index, string_start_char) {
|
||||
let string_to_search;
|
||||
if (string_start_char === '`') {
|
||||
string_to_search = string;
|
||||
} else {
|
||||
// we could slice at the search start index, but this way the index remains valid
|
||||
string_to_search = string.slice(
|
||||
0,
|
||||
infinity_if_negative(string.indexOf('\n', search_start_index))
|
||||
);
|
||||
}
|
||||
|
||||
return find_unescaped_char(string_to_search, search_start_index, string_start_char);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} string The string to search.
|
||||
* @param {number} search_start_index The index to start searching at.
|
||||
* @returns {number} The index of the end of this regex expression, or `Infinity` if not found.
|
||||
*/
|
||||
function find_regex_end(string, search_start_index) {
|
||||
return find_unescaped_char(string, search_start_index, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} string The string to search.
|
||||
* @param {number} search_start_index The index to begin the search at.
|
||||
* @param {string} char The character to search for.
|
||||
* @returns {number} The index of the first unescaped instance of {@link char}, or `Infinity` if not found.
|
||||
*/
|
||||
function find_unescaped_char(string, search_start_index, char) {
|
||||
let i = search_start_index;
|
||||
while (true) {
|
||||
const found_index = string.indexOf(char, i);
|
||||
if (found_index === -1) {
|
||||
return Infinity;
|
||||
}
|
||||
if (count_leading_backslashes(string, found_index - 1) % 2 === 0) {
|
||||
return found_index;
|
||||
}
|
||||
i = found_index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count consecutive leading backslashes before {@link search_start_index}.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* count_leading_backslashes('\\\\\\foo', 2); // 3 (the backslashes have to be escaped in the string literal, there are three in reality)
|
||||
* ```
|
||||
*
|
||||
* @param {string} string The string to search.
|
||||
* @param {number} search_start_index The index to begin the search at.
|
||||
*/
|
||||
function count_leading_backslashes(string, search_start_index) {
|
||||
let i = search_start_index;
|
||||
let count = 0;
|
||||
while (string[i] === '\\') {
|
||||
count++;
|
||||
i--;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the corresponding closing bracket, ignoring brackets found inside comments, strings, or regex expressions.
|
||||
* @param {string} template The string to search.
|
||||
* @param {number} index The index to begin the search at.
|
||||
* @param {string} open The opening bracket (ex: `'{'` will search for `'}'`).
|
||||
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
|
||||
*/
|
||||
export function find_matching_bracket(template, index, open) {
|
||||
const close = get_bracket_close(open);
|
||||
let brackets = 1;
|
||||
let i = index;
|
||||
while (brackets > 0 && i < template.length) {
|
||||
const char = template[i];
|
||||
switch (char) {
|
||||
case "'":
|
||||
case '"':
|
||||
case '`':
|
||||
i = find_string_end(template, i + 1, char) + 1;
|
||||
continue;
|
||||
case '/': {
|
||||
const next_char = template[i + 1];
|
||||
if (!next_char) continue;
|
||||
if (next_char === '/') {
|
||||
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
|
||||
continue;
|
||||
}
|
||||
if (next_char === '*') {
|
||||
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
|
||||
continue;
|
||||
}
|
||||
i = find_regex_end(template, i + 1) + '/'.length;
|
||||
continue;
|
||||
}
|
||||
default: {
|
||||
const char = template[i];
|
||||
if (char === open) {
|
||||
brackets++;
|
||||
} else if (char === close) {
|
||||
brackets--;
|
||||
}
|
||||
if (brackets === 0) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
16
node_modules/svelte/src/compiler/phases/1-parse/utils/create.js
generated
vendored
Normal file
16
node_modules/svelte/src/compiler/phases/1-parse/utils/create.js
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
|
||||
/**
|
||||
* @param {any} transparent
|
||||
* @returns {AST.Fragment}
|
||||
*/
|
||||
export function create_fragment(transparent = false) {
|
||||
return {
|
||||
type: 'Fragment',
|
||||
nodes: [],
|
||||
metadata: {
|
||||
transparent,
|
||||
dynamic: false
|
||||
}
|
||||
};
|
||||
}
|
||||
2234
node_modules/svelte/src/compiler/phases/1-parse/utils/entities.js
generated
vendored
Normal file
2234
node_modules/svelte/src/compiler/phases/1-parse/utils/entities.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
280
node_modules/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
generated
vendored
Normal file
280
node_modules/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
generated
vendored
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string[]} names
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export default function fuzzymatch(name, names) {
|
||||
if (names.length === 0) return null;
|
||||
|
||||
const set = new FuzzySet(names);
|
||||
const matches = set.get(name);
|
||||
|
||||
return matches && matches[0][0] > 0.7 ? matches[0][1] : null;
|
||||
}
|
||||
|
||||
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
|
||||
// BSD Licensed
|
||||
|
||||
const GRAM_SIZE_LOWER = 2;
|
||||
const GRAM_SIZE_UPPER = 3;
|
||||
|
||||
// return an edit distance from 0 to 1
|
||||
|
||||
/**
|
||||
* @param {string} str1
|
||||
* @param {string} str2
|
||||
*/
|
||||
function _distance(str1, str2) {
|
||||
if (str1 === null && str2 === null) {
|
||||
throw 'Trying to compare two null values';
|
||||
}
|
||||
if (str1 === null || str2 === null) return 0;
|
||||
str1 = String(str1);
|
||||
str2 = String(str2);
|
||||
|
||||
const distance = levenshtein(str1, str2);
|
||||
return 1 - distance / Math.max(str1.length, str2.length);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
/**
|
||||
* @param {string} str1
|
||||
* @param {string} str2
|
||||
*/
|
||||
function levenshtein(str1, str2) {
|
||||
/** @type {number[]} */
|
||||
const current = [];
|
||||
let prev = 0;
|
||||
let value = 0;
|
||||
|
||||
for (let i = 0; i <= str2.length; i++) {
|
||||
for (let j = 0; j <= str1.length; j++) {
|
||||
if (i && j) {
|
||||
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
|
||||
value = prev;
|
||||
} else {
|
||||
value = Math.min(current[j], current[j - 1], prev) + 1;
|
||||
}
|
||||
} else {
|
||||
value = i + j;
|
||||
}
|
||||
|
||||
prev = current[j];
|
||||
current[j] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return /** @type {number} */ (current.pop());
|
||||
}
|
||||
|
||||
const non_word_regex = /[^\w, ]+/;
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {any} gram_size
|
||||
*/
|
||||
function iterate_grams(value, gram_size = 2) {
|
||||
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
|
||||
const len_diff = gram_size - simplified.length;
|
||||
const results = [];
|
||||
|
||||
if (len_diff > 0) {
|
||||
for (let i = 0; i < len_diff; ++i) {
|
||||
value += '-';
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
|
||||
results.push(simplified.slice(i, i + gram_size));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {any} gram_size
|
||||
*/
|
||||
function gram_counter(value, gram_size = 2) {
|
||||
// return an object where key=gram, value=number of occurrences
|
||||
|
||||
/** @type {Record<string, number>} */
|
||||
const result = {};
|
||||
const grams = iterate_grams(value, gram_size);
|
||||
let i = 0;
|
||||
|
||||
for (i; i < grams.length; ++i) {
|
||||
if (grams[i] in result) {
|
||||
result[grams[i]] += 1;
|
||||
} else {
|
||||
result[grams[i]] = 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MatchTuple} a
|
||||
* @param {MatchTuple} b
|
||||
*/
|
||||
function sort_descending(a, b) {
|
||||
return b[0] - a[0];
|
||||
}
|
||||
|
||||
class FuzzySet {
|
||||
/** @type {Record<string, string>} */
|
||||
exact_set = {};
|
||||
|
||||
/** @type {Record<string, [number, number][]>} */
|
||||
match_dict = {};
|
||||
|
||||
/** @type {Record<string, number[]>} */
|
||||
items = {};
|
||||
|
||||
/** @param {string[]} arr */
|
||||
constructor(arr) {
|
||||
// initialisation
|
||||
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this.items[i] = [];
|
||||
}
|
||||
|
||||
// add all the items to the set
|
||||
for (let i = 0; i < arr.length; ++i) {
|
||||
this.add(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} value */
|
||||
add(value) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
if (normalized_value in this.exact_set) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i = GRAM_SIZE_LOWER;
|
||||
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this._add(value, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {number} gram_size
|
||||
*/
|
||||
_add(value, gram_size) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const items = this.items[gram_size] || [];
|
||||
const index = items.length;
|
||||
|
||||
items.push(0);
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
this.match_dict[gram].push([index, gram_count]);
|
||||
} else {
|
||||
this.match_dict[gram] = [[index, gram_count]];
|
||||
}
|
||||
}
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
// @ts-ignore no idea what this code is doing
|
||||
items[index] = [vector_normal, normalized_value];
|
||||
this.items[gram_size] = items;
|
||||
this.exact_set[normalized_value] = value;
|
||||
}
|
||||
|
||||
/** @param {string} value */
|
||||
get(value) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const result = this.exact_set[normalized_value];
|
||||
|
||||
if (result) {
|
||||
return /** @type {MatchTuple[]} */ ([[1, result]]);
|
||||
}
|
||||
|
||||
// start with high gram size and if there are no results, go to lower gram sizes
|
||||
for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) {
|
||||
const results = this.__get(value, gram_size);
|
||||
if (results.length > 0) return results;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {number} gram_size
|
||||
* @returns {MatchTuple[]}
|
||||
*/
|
||||
__get(value, gram_size) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
|
||||
/** @type {Record<string, number>} */
|
||||
const matches = {};
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
const items = this.items[gram_size];
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
let i;
|
||||
let index;
|
||||
let other_gram_count;
|
||||
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
for (i = 0; i < this.match_dict[gram].length; ++i) {
|
||||
index = this.match_dict[gram][i][0];
|
||||
other_gram_count = this.match_dict[gram][i][1];
|
||||
if (index in matches) {
|
||||
matches[index] += gram_count * other_gram_count;
|
||||
} else {
|
||||
matches[index] = gram_count * other_gram_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
|
||||
/** @type {MatchTuple[]} */
|
||||
let results = [];
|
||||
let match_score;
|
||||
|
||||
// build a results list of [score, str]
|
||||
for (const match_index in matches) {
|
||||
match_score = matches[match_index];
|
||||
// @ts-ignore no idea what this code is doing
|
||||
results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]);
|
||||
}
|
||||
|
||||
results.sort(sort_descending);
|
||||
|
||||
/** @type {MatchTuple[]} */
|
||||
let new_results = [];
|
||||
const end_index = Math.min(50, results.length);
|
||||
// truncate somewhat arbitrarily to 50
|
||||
for (let i = 0; i < end_index; ++i) {
|
||||
// @ts-ignore no idea what this code is doing
|
||||
new_results.push([_distance(results[i][1], normalized_value), results[i][1]]);
|
||||
}
|
||||
results = new_results;
|
||||
results.sort(sort_descending);
|
||||
|
||||
new_results = [];
|
||||
for (let i = 0; i < results.length; ++i) {
|
||||
if (results[i][0] === results[0][0]) {
|
||||
// @ts-ignore no idea what this code is doing
|
||||
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
|
||||
}
|
||||
}
|
||||
|
||||
return new_results;
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {[score: number, match: string]} MatchTuple */
|
||||
120
node_modules/svelte/src/compiler/phases/1-parse/utils/html.js
generated
vendored
Normal file
120
node_modules/svelte/src/compiler/phases/1-parse/utils/html.js
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
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 = '#(?:x[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) {
|
||||
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') {
|
||||
code = parseInt(entity.substring(2), 16);
|
||||
} else {
|
||||
code = parseInt(entity.substring(1), 10);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return match;
|
||||
}
|
||||
|
||||
return String.fromCodePoint(validate_code(code));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/** @param {number} code */
|
||||
function validate_code(code) {
|
||||
// line feed becomes generic whitespace
|
||||
if (code === 10) {
|
||||
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;
|
||||
}
|
||||
|
||||
return NUL;
|
||||
}
|
||||
Reference in New Issue
Block a user