diff --git a/src/compiler/parse/index.js b/src/compiler/parse/index.js index 10de82c26d..55a5d7b872 100644 --- a/src/compiler/parse/index.js +++ b/src/compiler/parse/index.js @@ -8,238 +8,243 @@ import parser_errors from './errors'; const regex_position_indicator = / \(\d+:\d+\)$/; /** */ export class Parser { + /** + * @readonly + * @type {string} + */ + template = undefined; - /** - * @readonly - * @type {string} - */ - template = undefined; + /** + * @readonly + * @type {string} + */ + filename = undefined; - /** - * @readonly - * @type {string} - */ - filename = undefined; + /** + * @readonly + * @type {boolean} + */ + customElement = undefined; - /** - * @readonly - * @type {boolean} - */ - customElement = undefined; + /** + * @readonly + * @type {'injected' | 'external' | 'none' | boolean} + */ + css_mode = undefined; - /** - * @readonly - * @type {'injected' | 'external' | 'none' | boolean} - */ - css_mode = undefined; + /** + * @default 0 */ + index = 0; - /** - * @default 0 */ - index = 0; + /** + * @default [] + * @type {TemplateNode[]} + */ + stack = []; - /** - * @default [] - * @type {TemplateNode[]} - */ - stack = []; + /** + * @type {Fragment} */ + html = undefined; - /** - * @type {Fragment} */ - html = undefined; + /** + * @default [] + * @type {Style[]} + */ + css = []; - /** - * @default [] - * @type {Style[]} - */ - css = []; + /** + * @default [] + * @type {Script[]} + */ + js = []; - /** - * @default [] - * @type {Script[]} - */ - js = []; + /** + * @default {} */ + meta_tags = {}; - /** - * @default {} */ - meta_tags = {}; + /** + * @type {LastAutoClosedTag} */ + 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} */ - 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]; - } + /** + * @param {any} err */ + acorn_error(err) { + this.error( + { + code: 'parse-error', + message: err.message.replace(regex_position_indicator, '') + }, + err.pos + ); + } - /** - * @param {any} err */ - acorn_error(err) { - this.error({ - code: 'parse-error', - message: err.message.replace(regex_position_indicator, '') - }, err.pos); - } + /** + * @param {{ code: string; message: string }} */ + error({ code, message }, index = this.index) { + error(message, { + name: 'ParseError', + code, + source: this.template, + start: index, + filename: this.filename + }); + } - /** - * @param {{ code: string; message: string }} */ - error({ code, message }, index = this.index) { - error(message, { - name: 'ParseError', - code, - source: this.template, - start: index, - filename: this.filename - }); - } + /** + * @param {string} str + * @param {boolean} required + * @param {{ code: string; message: string }} error + */ + eat(str, required, error) { + if (this.match(str)) { + this.index += str.length; + 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 {boolean} required - * @param {{ code: string; message: string }} error - */ - eat(str, required, error) { - if (this.match(str)) { - this.index += str.length; - 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 */ + match(str) { + return this.template.slice(this.index, this.index + str.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; + } + 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 */ - match(str) { - return this.template.slice(this.index, this.index + str.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; - } - 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[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(); - } + /** + * @param {RegExp} pattern + * @param {Parameters[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} */ export default function parse(template, options = {}) { - const parser = new Parser(template, options); - // TODO we may want to allow multiple