Convert src/compiler/parse/index.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent 9e28d692e4
commit 202d9c4280

@ -1,5 +1,5 @@
// All parser errors should be listed and accessed from here // All parser errors should be listed and accessed from here
import list from '../utils/list'; import list from '../utils/list.js';
/** /**
* @internal * @internal
*/ */

@ -1,113 +1,135 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment'; import fragment from './state/fragment.js';
import { regex_whitespace } from '../utils/patterns'; import { regex_whitespace } from '../utils/patterns';
import { reserved } from '../utils/names'; import { reserved } from '../utils/names';
import full_char_code_at from '../utils/full_char_code_at'; import full_char_code_at from '../utils/full_char_code_at';
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces';
import error from '../utils/error'; import error from '../utils/error';
import parser_errors from './errors'; import parser_errors from './errors';
const regex_position_indicator = / \(\d+:\d+\)$/;
/** */
export class Parser {
type ParserState = (parser: Parser) => ParserState | void; /**
* @readonly
* @type {string}
*/
template = undefined;
interface LastAutoClosedTag { /**
tag: string; * @readonly
reason: string; * @type {string}
depth: number; */
} filename = undefined;
const regex_position_indicator = / \(\d+:\d+\)$/; /**
* @readonly
* @type {boolean}
*/
customElement = undefined;
export class Parser { /**
readonly template: string; * @readonly
readonly filename?: string; * @type {'injected' | 'external' | 'none' | boolean}
readonly customElement: boolean; */
readonly css_mode: 'injected' | 'external' | 'none' | boolean; css_mode = undefined;
/**
* @default 0 */
index = 0; index = 0;
stack: TemplateNode[] = [];
html: Fragment; /**
css: Style[] = []; * @default []
js: Script[] = []; * @type {TemplateNode[]}
*/
stack = [];
/**
* @type {Fragment} */
html = undefined;
/**
* @default []
* @type {Style[]}
*/
css = [];
/**
* @default []
* @type {Script[]}
*/
js = [];
/**
* @default {} */
meta_tags = {}; meta_tags = {};
last_auto_closed_tag?: LastAutoClosedTag;
constructor(template: string, options: ParserOptions) { /**
* @type {LastAutoClosedTag} */
last_auto_closed_tag = undefined;
constructor(template, options) {
if (typeof template !== 'string') { if (typeof template !== 'string') {
throw new TypeError('Template must be a string'); throw new TypeError('Template must be a string');
} }
this.template = template.trimRight(); this.template = template.trimRight();
this.filename = options.filename; this.filename = options.filename;
this.customElement = options.customElement; this.customElement = options.customElement;
this.css_mode = options.css; this.css_mode = options.css;
this.html = { this.html = {
start: null, start: null,
end: null, end: null,
type: 'Fragment', type: 'Fragment',
children: [] children: []
}; };
this.stack.push(this.html); this.stack.push(this.html);
let state = fragment;
let state: ParserState = fragment;
while (this.index < this.template.length) { while (this.index < this.template.length) {
state = state(this) || fragment; state = state(this) || fragment;
} }
if (this.stack.length > 1) { if (this.stack.length > 1) {
const current = this.current(); const current = this.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block'; const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
const slug = current.type === 'Element' ? 'element' : 'block'; const slug = current.type === 'Element' ? 'element' : 'block';
this.error({
this.error(
{
code: `unclosed-${slug}`, code: `unclosed-${slug}`,
message: `${type} was left open` message: `${type} was left open`
}, }, current.start);
current.start
);
} }
if (state !== fragment) { if (state !== fragment) {
this.error({ this.error({
code: 'unexpected-eof', code: 'unexpected-eof',
message: 'Unexpected end of input' message: 'Unexpected end of input'
}); });
} }
if (this.html.children.length) { if (this.html.children.length) {
let start = this.html.children[0].start; let start = this.html.children[0].start;
while (regex_whitespace.test(template[start])) start += 1; while (regex_whitespace.test(template[start]))
start += 1;
let end = this.html.children[this.html.children.length - 1].end; let end = this.html.children[this.html.children.length - 1].end;
while (regex_whitespace.test(template[end - 1])) end -= 1; while (regex_whitespace.test(template[end - 1]))
end -= 1;
this.html.start = start; this.html.start = start;
this.html.end = end; this.html.end = end;
} else { }
else {
this.html.start = this.html.end = null; this.html.start = this.html.end = null;
} }
} }
current() { current() {
return this.stack[this.stack.length - 1]; return this.stack[this.stack.length - 1];
} }
acorn_error(err: any) { /**
this.error( * @param {any} err */
{ acorn_error(err) {
this.error({
code: 'parse-error', code: 'parse-error',
message: err.message.replace(regex_position_indicator, '') message: err.message.replace(regex_position_indicator, '')
}, }, err.pos);
err.pos
);
} }
error({ code, message }: { code: string; message: string }, index = this.index) { /**
* @param {{ code: string; message: string }} */
error({ code, message }, index = this.index) {
error(message, { error(message, {
name: 'ParseError', name: 'ParseError',
code, code,
@ -117,109 +139,98 @@ export class Parser {
}); });
} }
eat(str: string, required?: boolean, error?: { code: string; message: string }) { /**
* @param {string} str
* @param {boolean} required
* @param {{ code: string; message: string }} error
*/
eat(str, required, error) {
if (this.match(str)) { if (this.match(str)) {
this.index += str.length; this.index += str.length;
return true; return true;
} }
if (required) { if (required) {
this.error( this.error(error ||
error ||
(this.index === this.template.length (this.index === this.template.length
? parser_errors.unexpected_eof_token(str) ? parser_errors.unexpected_eof_token(str)
: parser_errors.unexpected_token(str)) : parser_errors.unexpected_token(str)));
);
} }
return false; return false;
} }
match(str: string) { /**
* @param {string} str */
match(str) {
return this.template.slice(this.index, this.index + str.length) === str; return this.template.slice(this.index, this.index + str.length) === str;
} }
/** /**
* Match a regex at the current index * Match a regex at the current index
* @param pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance * @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: RegExp) { match_regex(pattern) {
const match = pattern.exec(this.template.slice(this.index)); const match = pattern.exec(this.template.slice(this.index));
if (!match || match.index !== 0) return null; if (!match || match.index !== 0)
return null;
return match[0]; return match[0];
} }
allow_whitespace() { allow_whitespace() {
while (this.index < this.template.length && regex_whitespace.test(this.template[this.index])) { while (this.index < this.template.length && regex_whitespace.test(this.template[this.index])) {
this.index++; this.index++;
} }
} }
/** /**
* Search for a regex starting at the current index and return the result if it matches * Search for a regex starting at the current index and return the result if it matches
* @param pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance * @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: RegExp) { read(pattern) {
const result = this.match_regex(pattern); const result = this.match_regex(pattern);
if (result) this.index += result.length; if (result)
this.index += result.length;
return result; return result;
} }
read_identifier(allow_reserved = false) { read_identifier(allow_reserved = false) {
const start = this.index; const start = this.index;
let i = this.index; let i = this.index;
const code = full_char_code_at(this.template, i); const code = full_char_code_at(this.template, i);
if (!isIdentifierStart(code, true)) return null; if (!isIdentifierStart(code, true))
return null;
i += code <= 0xffff ? 1 : 2; i += code <= 0xffff ? 1 : 2;
while (i < this.template.length) { while (i < this.template.length) {
const code = full_char_code_at(this.template, i); const code = full_char_code_at(this.template, i);
if (!isIdentifierChar(code, true))
if (!isIdentifierChar(code, true)) break; break;
i += code <= 0xffff ? 1 : 2; i += code <= 0xffff ? 1 : 2;
} }
const identifier = this.template.slice(this.index, (this.index = i)); const identifier = this.template.slice(this.index, (this.index = i));
if (!allow_reserved && reserved.has(identifier)) { if (!allow_reserved && reserved.has(identifier)) {
this.error( this.error({
{
code: 'unexpected-reserved-word', code: 'unexpected-reserved-word',
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here` message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
}, }, start);
start
);
} }
return identifier; return identifier;
} }
read_until(pattern: RegExp, error_message?: Parameters<Parser['error']>[0]) { /**
* @param {RegExp} pattern
* @param {Parameters<Parser['error']>[0]} error_message
*/
read_until(pattern, error_message) {
if (this.index >= this.template.length) { if (this.index >= this.template.length) {
this.error( this.error(error_message || {
error_message || {
code: 'unexpected-eof', code: 'unexpected-eof',
message: 'Unexpected end of input' message: 'Unexpected end of input'
});
} }
);
}
const start = this.index; const start = this.index;
const match = pattern.exec(this.template.slice(start)); const match = pattern.exec(this.template.slice(start));
if (match) { if (match) {
this.index = start + match.index; this.index = start + match.index;
return this.template.slice(start, this.index); return this.template.slice(start, this.index);
} }
this.index = this.template.length; this.index = this.template.length;
return this.template.slice(start); return this.template.slice(start);
} }
require_whitespace() { require_whitespace() {
if (!regex_whitespace.test(this.template[this.index])) { if (!regex_whitespace.test(this.template[this.index])) {
this.error({ this.error({
@ -227,31 +238,30 @@ export class Parser {
message: 'Expected whitespace' message: 'Expected whitespace'
}); });
} }
this.allow_whitespace(); this.allow_whitespace();
} }
} }
export default function parse(template: string, options: ParserOptions = {}): Ast { /**
* @param {string} template
* @param {ParserOptions} options
* @returns {Ast}
*/
export default function parse(template, options = {}) {
const parser = new Parser(template, options); const parser = new Parser(template, options);
// TODO we may want to allow multiple <style> tags — // TODO we may want to allow multiple <style> tags —
// one scoped, one global. for now, only allow one // one scoped, one global. for now, only allow one
if (parser.css.length > 1) { if (parser.css.length > 1) {
parser.error(parser_errors.duplicate_style, parser.css[1].start); parser.error(parser_errors.duplicate_style, parser.css[1].start);
} }
const instance_scripts = parser.js.filter((script) => script.context === 'default'); const instance_scripts = parser.js.filter((script) => script.context === 'default');
const module_scripts = parser.js.filter((script) => script.context === 'module'); const module_scripts = parser.js.filter((script) => script.context === 'module');
if (instance_scripts.length > 1) { if (instance_scripts.length > 1) {
parser.error(parser_errors.invalid_script_instance, instance_scripts[1].start); parser.error(parser_errors.invalid_script_instance, instance_scripts[1].start);
} }
if (module_scripts.length > 1) { if (module_scripts.length > 1) {
parser.error(parser_errors.invalid_script_module, module_scripts[1].start); parser.error(parser_errors.invalid_script_module, module_scripts[1].start);
} }
return { return {
html: parser.html, html: parser.html,
css: parser.css[0], css: parser.css[0],
@ -259,3 +269,12 @@ export default function parse(template: string, options: ParserOptions = {}): As
module: module_scripts[0] module: module_scripts[0]
}; };
} }
/** @typedef {(parser: Parser) => ParserState | void} ParserState */
/** @typedef {Object} LastAutoClosedTag
* @property {string} tag
* @property {string} reason
* @property {number} depth
*/
Loading…
Cancel
Save