diff options
author | 2021-03-19 17:07:45 -0400 | |
---|---|---|
committer | 2021-03-19 17:07:45 -0400 | |
commit | 17c3c98f07628b43b941b84831e8e1f9bcd7ca46 (patch) | |
tree | 2e2b3c7d6bd67ebaabe6636ae6867ad368ac6c3a /src/compiler/parse/index.ts | |
parent | 8ebc077cb0d9f50aae22d2651bd5ef13fe4641d3 (diff) | |
download | astro-17c3c98f07628b43b941b84831e8e1f9bcd7ca46.tar.gz astro-17c3c98f07628b43b941b84831e8e1f9bcd7ca46.tar.zst astro-17c3c98f07628b43b941b84831e8e1f9bcd7ca46.zip |
Initial tests set up (#10)
* Begin debugging
* Initial tests set up
This adds tests using uvu (we can switch if people want) and restructures things a bit so that it's easier to test.
Like in snowpack you set up a little project. In our tests you can say:
```js
const result = await runtime.load('/blog/hello-world')
```
And analyze the result. I included a `test-helpers.js` which has a function that will turn HTML into a cheerio instance, for inspecting the result HTML.
* Add CI
* Remove extra console logs
* Formatting
Diffstat (limited to 'src/compiler/parse/index.ts')
-rw-r--r-- | src/compiler/parse/index.ts | 480 |
1 files changed, 246 insertions, 234 deletions
diff --git a/src/compiler/parse/index.ts b/src/compiler/parse/index.ts index f5c5a592b..eab2c42c5 100644 --- a/src/compiler/parse/index.ts +++ b/src/compiler/parse/index.ts @@ -8,245 +8,257 @@ import full_char_code_at from '../utils/full_char_code_at.js'; import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces.js'; import error from '../utils/error.js'; -type ParserState = (parser: Parser) => (ParserState | void); +type ParserState = (parser: Parser) => ParserState | void; interface LastAutoClosedTag { - tag: string; - reason: string; - depth: number; + tag: string; + reason: string; + depth: number; } export class Parser { - readonly template: string; - readonly filename?: string; - readonly customElement: boolean; - - index = 0; - stack: TemplateNode[] = []; - - html: Fragment; - css: Style[] = []; - js: Script[] = []; - meta_tags = {}; - last_auto_closed_tag?: LastAutoClosedTag; - - constructor(template: string, options: ParserOptions) { - if (typeof template !== 'string') { - throw new TypeError('Template must be a string'); - } - - this.template = template.replace(/\s+$/, ''); - this.filename = options.filename; - this.customElement = options.customElement; - - this.html = { - start: null, - end: null, - type: 'Fragment', - children: [] - }; - - this.stack.push(this.html); - - let state: ParserState = 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 (whitespace.test(template[start])) start += 1; - - let end = this.html.children[this.html.children.length - 1].end; - while (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]; - } - - acorn_error(err: any) { - this.error({ - code: 'parse-error', - message: err.message.replace(/ \(\d+:\d+\)$/, '') - }, err.pos); - } - - error({ code, message }: { code: string; message: string }, index = this.index) { - error(message, { - name: 'ParseError', - code, - source: this.template, - start: index, - filename: this.filename - }); - } - - eat(str: string, required?: boolean, message?: string) { - if (this.match(str)) { - this.index += str.length; - return true; - } - - if (required) { - this.error({ - code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`, - message: message || `Expected ${str}` - }); - } - - return false; - } - - match(str: string) { - return this.template.slice(this.index, this.index + str.length) === str; - } - - match_regex(pattern: RegExp) { - 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 && - whitespace.test(this.template[this.index]) - ) { - this.index++; - } - } - - read(pattern: RegExp) { - 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; - } - - read_until(pattern: RegExp) { - if (this.index >= this.template.length) { - this.error({ - 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 (!whitespace.test(this.template[this.index])) { - this.error({ - code: 'missing-whitespace', - message: 'Expected whitespace' - }); - } - - this.allow_whitespace(); - } + readonly template: string; + readonly filename?: string; + readonly customElement: boolean; + + index = 0; + stack: TemplateNode[] = []; + + html: Fragment; + css: Style[] = []; + js: Script[] = []; + meta_tags = {}; + last_auto_closed_tag?: LastAutoClosedTag; + + constructor(template: string, options: ParserOptions) { + if (typeof template !== 'string') { + throw new TypeError('Template must be a string'); + } + + this.template = template.replace(/\s+$/, ''); + this.filename = options.filename; + this.customElement = options.customElement; + + this.html = { + start: null, + end: null, + type: 'Fragment', + children: [], + }; + + this.stack.push(this.html); + + let state: ParserState = 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 (whitespace.test(template[start])) start += 1; + + let end = this.html.children[this.html.children.length - 1].end; + while (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]; + } + + acorn_error(err: any) { + this.error( + { + code: 'parse-error', + message: err.message.replace(/ \(\d+:\d+\)$/, ''), + }, + err.pos + ); + } + + error({ code, message }: { code: string; message: string }, index = this.index) { + error(message, { + name: 'ParseError', + code, + source: this.template, + start: index, + filename: this.filename, + }); + } + + eat(str: string, required?: boolean, message?: string) { + if (this.match(str)) { + this.index += str.length; + return true; + } + + if (required) { + this.error({ + code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`, + message: message || `Expected ${str}`, + }); + } + + return false; + } + + match(str: string) { + return this.template.slice(this.index, this.index + str.length) === str; + } + + match_regex(pattern: RegExp) { + 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 && whitespace.test(this.template[this.index])) { + this.index++; + } + } + + read(pattern: RegExp) { + 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; + } + + read_until(pattern: RegExp) { + if (this.index >= this.template.length) { + this.error({ + 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 (!whitespace.test(this.template[this.index])) { + this.error({ + code: 'missing-whitespace', + message: 'Expected whitespace', + }); + } + + this.allow_whitespace(); + } } -export default function parse( - template: string, - options: ParserOptions = {} -): Ast { - const parser = new Parser(template, options); - - // TODO we may want to allow multiple <style> tags — - // one scoped, one global. for now, only allow one - if (parser.css.length > 1) { - parser.error({ - code: 'duplicate-style', - message: 'You can only have one top-level <style> tag per component' - }, parser.css[1].start); - } - - const instance_scripts = parser.js.filter(script => script.context === 'default'); - const module_scripts = parser.js.filter(script => script.context === 'module'); - - if (instance_scripts.length > 1) { - parser.error({ - code: 'invalid-script', - message: 'A component can only have one instance-level <script> element' - }, instance_scripts[1].start); - } - - if (module_scripts.length > 1) { - parser.error({ - code: 'invalid-script', - message: 'A component can only have one <script context="module"> element' - }, module_scripts[1].start); - } - - return { - html: parser.html, - css: parser.css[0], - instance: instance_scripts[0], - module: module_scripts[0] - }; +export default function parse(template: string, options: ParserOptions = {}): Ast { + const parser = new Parser(template, options); + + // TODO we may want to allow multiple <style> tags — + // one scoped, one global. for now, only allow one + if (parser.css.length > 1) { + parser.error( + { + code: 'duplicate-style', + message: 'You can only have one top-level <style> tag per component', + }, + parser.css[1].start + ); + } + + const instance_scripts = parser.js.filter((script) => script.context === 'default'); + const module_scripts = parser.js.filter((script) => script.context === 'module'); + + if (instance_scripts.length > 1) { + parser.error( + { + code: 'invalid-script', + message: 'A component can only have one instance-level <script> element', + }, + instance_scripts[1].start + ); + } + + if (module_scripts.length > 1) { + parser.error( + { + code: 'invalid-script', + message: 'A component can only have one <script context="module"> element', + }, + module_scripts[1].start + ); + } + + return { + html: parser.html, + css: parser.css[0], + instance: instance_scripts[0], + module: module_scripts[0], + }; } |