Lint and format src/compiler/parse/index.js

pull/8569/head
Simon Holthausen 3 years ago
parent f49a6b6f39
commit 4b3cacf3b9

@ -8,238 +8,243 @@ import parser_errors from './errors';
const regex_position_indicator = / \(\d+:\d+\)$/; const regex_position_indicator = / \(\d+:\d+\)$/;
/** */ /** */
export class Parser { export class Parser {
/**
* @readonly
* @type {string}
*/
template = undefined;
/** /**
* @readonly * @readonly
* @type {string} * @type {string}
*/ */
template = undefined; filename = undefined;
/** /**
* @readonly * @readonly
* @type {string} * @type {boolean}
*/ */
filename = undefined; customElement = undefined;
/** /**
* @readonly * @readonly
* @type {boolean} * @type {'injected' | 'external' | 'none' | boolean}
*/ */
customElement = undefined; css_mode = undefined;
/** /**
* @readonly * @default 0 */
* @type {'injected' | 'external' | 'none' | boolean} index = 0;
*/
css_mode = undefined;
/** /**
* @default 0 */ * @default []
index = 0; * @type {TemplateNode[]}
*/
stack = [];
/** /**
* @default [] * @type {Fragment} */
* @type {TemplateNode[]} html = undefined;
*/
stack = [];
/** /**
* @type {Fragment} */ * @default []
html = undefined; * @type {Style[]}
*/
css = [];
/** /**
* @default [] * @default []
* @type {Style[]} * @type {Script[]}
*/ */
css = []; js = [];
/** /**
* @default [] * @default {} */
* @type {Script[]} meta_tags = {};
*/
js = [];
/** /**
* @default {} */ * @type {LastAutoClosedTag} */
meta_tags = {}; last_auto_closed_tag = undefined;
constructor(template, options) {
if (typeof template !== 'string') {
throw new TypeError('Template must be a string');
}
this.template = template.trimRight();
this.filename = options.filename;
this.customElement = options.customElement;
this.css_mode = options.css;
this.html = {
start: null,
end: null,
type: 'Fragment',
children: []
};
this.stack.push(this.html);
let state = fragment;
while (this.index < this.template.length) {
state = state(this) || fragment;
}
if (this.stack.length > 1) {
const current = this.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
const slug = current.type === 'Element' ? 'element' : 'block';
this.error(
{
code: `unclosed-${slug}`,
message: `${type} was left open`
},
current.start
);
}
if (state !== fragment) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
});
}
if (this.html.children.length) {
let start = this.html.children[0].start;
while (regex_whitespace.test(template[start])) start += 1;
let end = this.html.children[this.html.children.length - 1].end;
while (regex_whitespace.test(template[end - 1])) end -= 1;
this.html.start = start;
this.html.end = end;
} else {
this.html.start = this.html.end = null;
}
}
current() {
return this.stack[this.stack.length - 1];
}
/** /**
* @type {LastAutoClosedTag} */ * @param {any} err */
last_auto_closed_tag = undefined; acorn_error(err) {
constructor(template, options) { this.error(
if (typeof template !== 'string') { {
throw new TypeError('Template must be a string'); code: 'parse-error',
} message: err.message.replace(regex_position_indicator, '')
this.template = template.trimRight(); },
this.filename = options.filename; err.pos
this.customElement = options.customElement; );
this.css_mode = options.css; }
this.html = {
start: null,
end: null,
type: 'Fragment',
children: []
};
this.stack.push(this.html);
let state = fragment;
while (this.index < this.template.length) {
state = state(this) || fragment;
}
if (this.stack.length > 1) {
const current = this.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
const slug = current.type === 'Element' ? 'element' : 'block';
this.error({
code: `unclosed-${slug}`,
message: `${type} was left open`
}, current.start);
}
if (state !== fragment) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
});
}
if (this.html.children.length) {
let start = this.html.children[0].start;
while (regex_whitespace.test(template[start]))
start += 1;
let end = this.html.children[this.html.children.length - 1].end;
while (regex_whitespace.test(template[end - 1]))
end -= 1;
this.html.start = start;
this.html.end = end;
}
else {
this.html.start = this.html.end = null;
}
}
current() {
return this.stack[this.stack.length - 1];
}
/** /**
* @param {any} err */ * @param {{ code: string; message: string }} */
acorn_error(err) { error({ code, message }, index = this.index) {
this.error({ error(message, {
code: 'parse-error', name: 'ParseError',
message: err.message.replace(regex_position_indicator, '') code,
}, err.pos); source: this.template,
} start: index,
filename: this.filename
});
}
/** /**
* @param {{ code: string; message: string }} */ * @param {string} str
error({ code, message }, index = this.index) { * @param {boolean} required
error(message, { * @param {{ code: string; message: string }} error
name: 'ParseError', */
code, eat(str, required, error) {
source: this.template, if (this.match(str)) {
start: index, this.index += str.length;
filename: this.filename return true;
}); }
} if (required) {
this.error(
error ||
(this.index === this.template.length
? parser_errors.unexpected_eof_token(str)
: parser_errors.unexpected_token(str))
);
}
return false;
}
/** /**
* @param {string} str * @param {string} str */
* @param {boolean} required match(str) {
* @param {{ code: string; message: string }} error return this.template.slice(this.index, this.index + str.length) === str;
*/ }
eat(str, required, error) { /**
if (this.match(str)) { * Match a regex at the current index
this.index += str.length; * @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
return true; */
} match_regex(pattern) {
if (required) { const match = pattern.exec(this.template.slice(this.index));
this.error(error || if (!match || match.index !== 0) return null;
(this.index === this.template.length return match[0];
? parser_errors.unexpected_eof_token(str) }
: parser_errors.unexpected_token(str))); allow_whitespace() {
} while (this.index < this.template.length && regex_whitespace.test(this.template[this.index])) {
return false; 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;
}
read_identifier(allow_reserved = false) {
const start = this.index;
let i = this.index;
const code = full_char_code_at(this.template, i);
if (!isIdentifierStart(code, true)) return null;
i += code <= 0xffff ? 1 : 2;
while (i < this.template.length) {
const code = full_char_code_at(this.template, i);
if (!isIdentifierChar(code, true)) break;
i += code <= 0xffff ? 1 : 2;
}
const identifier = this.template.slice(this.index, (this.index = i));
if (!allow_reserved && reserved.has(identifier)) {
this.error(
{
code: 'unexpected-reserved-word',
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
},
start
);
}
return identifier;
}
/** /**
* @param {string} str */ * @param {RegExp} pattern
match(str) { * @param {Parameters<Parser['error']>[0]} error_message
return this.template.slice(this.index, this.index + str.length) === str; */
} read_until(pattern, error_message) {
/** if (this.index >= this.template.length) {
* Match a regex at the current index this.error(
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance error_message || {
*/ code: 'unexpected-eof',
match_regex(pattern) { message: 'Unexpected end of input'
const match = pattern.exec(this.template.slice(this.index)); }
if (!match || match.index !== 0) );
return null; }
return match[0]; const start = this.index;
} const match = pattern.exec(this.template.slice(start));
allow_whitespace() { if (match) {
while (this.index < this.template.length && regex_whitespace.test(this.template[this.index])) { this.index = start + match.index;
this.index++; return this.template.slice(start, this.index);
} }
} this.index = this.template.length;
/** return this.template.slice(start);
* 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 require_whitespace() {
*/ if (!regex_whitespace.test(this.template[this.index])) {
read(pattern) { this.error({
const result = this.match_regex(pattern); code: 'missing-whitespace',
if (result) message: 'Expected whitespace'
this.index += result.length; });
return result; }
} this.allow_whitespace();
read_identifier(allow_reserved = false) { }
const start = this.index;
let i = this.index;
const code = full_char_code_at(this.template, i);
if (!isIdentifierStart(code, true))
return null;
i += code <= 0xffff ? 1 : 2;
while (i < this.template.length) {
const code = full_char_code_at(this.template, i);
if (!isIdentifierChar(code, true))
break;
i += code <= 0xffff ? 1 : 2;
}
const identifier = this.template.slice(this.index, (this.index = i));
if (!allow_reserved && reserved.has(identifier)) {
this.error({
code: 'unexpected-reserved-word',
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
}, start);
}
return identifier;
}
/**
* @param {RegExp} pattern
* @param {Parameters<Parser['error']>[0]} error_message
*/
read_until(pattern, error_message) {
if (this.index >= this.template.length) {
this.error(error_message || {
code: 'unexpected-eof',
message: 'Unexpected end of input'
});
}
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])) {
this.error({
code: 'missing-whitespace',
message: 'Expected whitespace'
});
}
this.allow_whitespace();
}
} }
/** /**
@ -248,33 +253,32 @@ export class Parser {
* @returns {Ast} * @returns {Ast}
*/ */
export default function parse(template, options = {}) { 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],
instance: instance_scripts[0], instance: instance_scripts[0],
module: module_scripts[0] module: module_scripts[0]
}; };
} }
/** @typedef {(parser: Parser) => ParserState | void} ParserState */ /** @typedef {(parser: Parser) => ParserState | void} ParserState */
/** @typedef {Object} LastAutoClosedTag /** @typedef {Object} LastAutoClosedTag
* @property {string} tag * @property {string} tag
* @property {string} reason * @property {string} reason
* @property {number} depth * @property {number} depth
*/ */

Loading…
Cancel
Save