diff --git a/src/parse/index.ts b/src/parse/index.ts index c725ab90c3..ff2714cb03 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -3,6 +3,7 @@ import fragment from './state/fragment'; import { whitespace } from '../utils/patterns'; import { trimStart, trimEnd } from '../utils/trim'; import getCodeFrame from '../utils/getCodeFrame'; +import reservedNames from '../utils/reservedNames'; import hash from './utils/hash'; import { Node, Parsed } from '../interfaces'; import CompileError from '../utils/CompileError'; @@ -139,6 +140,17 @@ export class Parser { return match[0]; } + readIdentifier() { + const start = this.index; + const identifier = this.read(/[a-zA-Z_$][a-zA-Z0-9_$]*/); + + if (reservedNames.has(identifier)) { + this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start); + } + + return identifier; + } + readUntil(pattern: RegExp) { if (this.index >= this.template.length) this.error('Unexpected end of input'); diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index 3ca6003242..49d22322b1 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -5,8 +5,6 @@ import reservedNames from '../../utils/reservedNames'; import { Parser } from '../index'; import { Node } from '../../interfaces'; -const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/; - function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) { const firstChild = block.children[0]; const lastChild = block.children[block.children.length - 1]; @@ -41,16 +39,20 @@ export default function mustache(parser: Parser) { let block = parser.current(); let expected; - if (block.type === 'ElseBlock') { + if (block.type === 'ElseBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') { block.end = start; parser.stack.pop(); block = parser.current(); + + expected = 'await'; } if (block.type === 'IfBlock') { expected = 'if'; } else if (block.type === 'EachBlock') { expected = 'each'; + } else if (block.type === 'AwaitBlock') { + expected = 'await'; } else { parser.error(`Unexpected block closing tag`); } @@ -131,6 +133,50 @@ export default function mustache(parser: Parser) { }; parser.stack.push(block.else); + } else if (parser.eat('then')) { + // {{then}} is valid by itself — we need to check that a) + // we're inside an await block, and b) there's an expression + const awaitBlock = parser.current(); + if (awaitBlock.type === 'AwaitBlock') { + parser.requireWhitespace(); + awaitBlock.value = parser.readIdentifier(); + + parser.allowWhitespace(); + parser.eat('}}', true); + + const thenBlock: Node = { + start, + end: null, + type: 'ThenBlock', + children: [] + }; + + awaitBlock.then = thenBlock; + parser.stack.push(thenBlock); + } + } else if (parser.eat('catch')) { + const thenBlock = parser.current(); + if (thenBlock.type === 'ThenBlock') { + thenBlock.end = start; + parser.stack.pop(); + const awaitBlock = parser.current(); + + parser.requireWhitespace(); + awaitBlock.error = parser.readIdentifier(); + + parser.allowWhitespace(); + parser.eat('}}', true); + + const catchBlock: Node = { + start, + end: null, + type: 'CatchBlock', + children: [] + }; + + awaitBlock.catch = catchBlock; + parser.stack.push(catchBlock); + } } else if (parser.eat('#')) { // {{#if foo}} or {{#each foo}} let type; @@ -139,8 +185,10 @@ export default function mustache(parser: Parser) { type = 'IfBlock'; } else if (parser.eat('each')) { type = 'EachBlock'; + } else if (parser.eat('await')) { + type = 'AwaitBlock'; } else { - parser.error(`Expected if or each`); + parser.error(`Expected if, each or await`); } parser.requireWhitespace(); @@ -170,13 +218,8 @@ export default function mustache(parser: Parser) { do { parser.allowWhitespace(); - const start = parser.index; - const destructuredContext = parser.read(validIdentifier); - + const destructuredContext = parser.readIdentifier(); if (!destructuredContext) parser.error(`Expected name`); - if (reservedNames.has(destructuredContext)) { - parser.error(`'${destructuredContext}' is a reserved word in JavaScript and cannot be used here`, start); - } block.destructuredContexts.push(destructuredContext); parser.allowWhitespace(); @@ -188,12 +231,7 @@ export default function mustache(parser: Parser) { parser.allowWhitespace(); parser.eat(']', true); } else { - const start = parser.index; - block.context = parser.read(validIdentifier); - if (reservedNames.has(block.context)) { - parser.error(`'${block.context}' is a reserved word in JavaScript and cannot be used here`, start); - } - + block.context = parser.readIdentifier(); if (!block.context) parser.error(`Expected name`); } @@ -201,13 +239,13 @@ export default function mustache(parser: Parser) { if (parser.eat(',')) { parser.allowWhitespace(); - block.index = parser.read(validIdentifier); + block.index = parser.readIdentifier(); if (!block.index) parser.error(`Expected name`); parser.allowWhitespace(); } if (parser.eat('@')) { - block.key = parser.read(validIdentifier); + block.key = parser.readIdentifier(); if (!block.key) parser.error(`Expected name`); parser.allowWhitespace(); } diff --git a/test/parser/index.js b/test/parser/index.js index 6bdd1b6376..33e36e98cd 100644 --- a/test/parser/index.js +++ b/test/parser/index.js @@ -41,7 +41,8 @@ describe('parse', () => { assert.deepEqual(err.loc, expected.loc); assert.equal(err.pos, expected.pos); } catch (err2) { - throw err2.code === 'MODULE_NOT_FOUND' ? err : err2; + const e = err2.code === 'MODULE_NOT_FOUND' ? err : err2; + throw e; } } }); diff --git a/test/parser/samples/await-then-catch/input.html b/test/parser/samples/await-then-catch/input.html new file mode 100644 index 0000000000..36489b9043 --- /dev/null +++ b/test/parser/samples/await-then-catch/input.html @@ -0,0 +1,7 @@ +{{#await thePromise}} +
loading...
+{{then theValue}} +the value is {{theValue}}
+{{catch theError}} +oh no! {{theError.message}}
+{{/await}} \ No newline at end of file diff --git a/test/parser/samples/await-then-catch/output.json b/test/parser/samples/await-then-catch/output.json new file mode 100644 index 0000000000..baf2df5fdf --- /dev/null +++ b/test/parser/samples/await-then-catch/output.json @@ -0,0 +1,144 @@ +{ + "hash": 1040536517, + "html": { + "start": 0, + "end": 158, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 158, + "type": "AwaitBlock", + "expression": { + "type": "Identifier", + "start": 9, + "end": 19, + "name": "thePromise" + }, + "children": [ + { + "start": 23, + "end": 40, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 26, + "end": 36, + "type": "Text", + "data": "loading..." + } + ] + } + ], + "value": "theValue", + "then": { + "start": 41, + "end": 93, + "type": "ThenBlock", + "children": [ + { + "start": 58, + "end": 60, + "type": "Text", + "data": "\n\t" + }, + { + "start": 60, + "end": 92, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 63, + "end": 76, + "type": "Text", + "data": "the value is " + }, + { + "start": 76, + "end": 88, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 78, + "end": 86, + "name": "theValue" + } + } + ] + }, + { + "start": 92, + "end": 93, + "type": "Text", + "data": "\n" + } + ] + }, + "error": "theError", + "catch": { + "start": 93, + "end": 148, + "type": "CatchBlock", + "children": [ + { + "start": 111, + "end": 113, + "type": "Text", + "data": "\n\t" + }, + { + "start": 113, + "end": 147, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 116, + "end": 123, + "type": "Text", + "data": "oh no! " + }, + { + "start": 123, + "end": 143, + "type": "MustacheTag", + "expression": { + "type": "MemberExpression", + "start": 125, + "end": 141, + "object": { + "type": "Identifier", + "start": 125, + "end": 133, + "name": "theError" + }, + "property": { + "type": "Identifier", + "start": 134, + "end": 141, + "name": "message" + }, + "computed": false + } + } + ] + }, + { + "start": 147, + "end": 148, + "type": "Text", + "data": "\n" + } + ] + } + } + ] + }, + "css": null, + "js": null +} \ No newline at end of file