svelte
Version:
Cybernetically enhanced web apps
313 lines (250 loc) • 7.51 kB
JavaScript
/** @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
*/