import readContext from '../read/context'; import readExpression from '../read/expression'; import { whitespace } from '../../utils/patterns'; import { trimStart, trimEnd } from '../../utils/trim'; import { Parser } from '../index'; import { Node } from '../../interfaces'; function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) { if (!block.children || block.children.length === 0) return; // AwaitBlock const firstChild = block.children[0]; const lastChild = block.children[block.children.length - 1]; if (firstChild.type === 'Text' && trimBefore) { firstChild.data = trimStart(firstChild.data); if (!firstChild.data) block.children.shift(); } if (lastChild.type === 'Text' && trimAfter) { lastChild.data = trimEnd(lastChild.data); if (!lastChild.data) block.children.pop(); } if (block.else) { trimWhitespace(block.else, trimBefore, trimAfter); } if (firstChild.elseif) { trimWhitespace(firstChild, trimBefore, trimAfter); } } export default function mustache(parser: Parser) { const start = parser.index; parser.index += 1; parser.allowWhitespace(); // {/if} or {/each} if (parser.eat('/')) { let block = parser.current(); let expected; if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || 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({ code: `unexpected-block-close`, message: `Unexpected block closing tag` }); } parser.eat(expected, true); parser.allowWhitespace(); parser.eat('}', true); while (block.elseif) { block.end = parser.index; parser.stack.pop(); block = parser.current(); if (block.else) { block.else.end = start; } } // strip leading/trailing whitespace as necessary const charBefore = parser.template[block.start - 1]; const charAfter = parser.template[parser.index]; const trimBefore = !charBefore || whitespace.test(charBefore); const trimAfter = !charAfter || whitespace.test(charAfter); trimWhitespace(block, trimBefore, trimAfter); block.end = parser.index; parser.stack.pop(); } else if (parser.eat(':elseif')) { const block = parser.current(); if (block.type !== 'IfBlock') parser.error({ code: `invalid-elseif-placement`, message: 'Cannot have an {:elseif ...} block outside an {#if ...} block' }); parser.requireWhitespace(); const expression = readExpression(parser); parser.allowWhitespace(); parser.eat('}', true); block.else = { start: parser.index, end: null, type: 'ElseBlock', children: [ { start: parser.index, end: null, type: 'IfBlock', elseif: true, expression, children: [], }, ], }; parser.stack.push(block.else.children[0]); } else if (parser.eat(':else')) { const block = parser.current(); if (block.type !== 'IfBlock' && block.type !== 'EachBlock') { parser.error({ code: `invalid-else-placement`, message: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block' }); } parser.allowWhitespace(); parser.eat('}', true); block.else = { start: parser.index, end: null, type: 'ElseBlock', children: [], }; parser.stack.push(block.else); } else if (parser.eat(':then')) { // TODO DRY out this and the next section const pendingBlock = parser.current(); if (pendingBlock.type === 'PendingBlock') { pendingBlock.end = start; parser.stack.pop(); const awaitBlock = parser.current(); if (!parser.eat('}')) { 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(); if (!parser.eat('}')) { 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}, {#each foo} or {#await foo} let type; if (parser.eat('if')) { type = 'IfBlock'; } else if (parser.eat('each')) { type = 'EachBlock'; } else if (parser.eat('await')) { type = 'AwaitBlock'; } else { parser.error({ code: `expected-block-type`, message: `Expected if, each or await` }); } parser.requireWhitespace(); const expression = readExpression(parser); const block: Node = type === 'AwaitBlock' ? { start, end: null, type, expression, value: null, error: null, pending: { start: null, end: null, type: 'PendingBlock', children: [] }, then: { start: null, end: null, type: 'ThenBlock', children: [] }, catch: { start: null, end: null, type: 'CatchBlock', children: [] }, } : { start, end: null, type, expression, children: [], }; parser.allowWhitespace(); // {#each} blocks must declare a context – {#each list as item} if (type === 'EachBlock') { parser.eat('as', true); parser.requireWhitespace(); block.context = readContext(parser); parser.allowWhitespace(); if (parser.eat(',')) { parser.allowWhitespace(); block.index = parser.readIdentifier(); if (!block.index) parser.error({ code: `expected-name`, message: `Expected name` }); parser.allowWhitespace(); } if (parser.eat('(')) { parser.allowWhitespace(); block.key = readExpression(parser); parser.allowWhitespace(); parser.eat(')', true); parser.allowWhitespace(); } else if (parser.eat('@')) { block.key = parser.readIdentifier(); if (!block.key) parser.error({ code: `expected-name`, message: `Expected name` }); parser.allowWhitespace(); } } let awaitBlockShorthand = type === 'AwaitBlock' && parser.eat('then'); if (awaitBlockShorthand) { parser.requireWhitespace(); block.value = parser.readIdentifier(); parser.allowWhitespace(); } parser.eat('}', true); parser.current().children.push(block); parser.stack.push(block); if (type === 'AwaitBlock') { const childBlock = awaitBlockShorthand ? block.then : block.pending; childBlock.start = parser.index; parser.stack.push(childBlock); } } else if (parser.eat('@html')) { // {@html content} tag const expression = readExpression(parser); parser.allowWhitespace(); parser.eat('}', true); parser.current().children.push({ start, end: parser.index, type: 'RawMustacheTag', expression, }); } else if (parser.eat('@debug')) { let identifiers; // Implies {@debug} which indicates "debug all" if (parser.read(/\s*}/)) { identifiers = []; } else { const expression = readExpression(parser); identifiers = expression.type === 'SequenceExpression' ? expression.expressions : [expression]; identifiers.forEach(node => { if (node.type !== 'Identifier') { parser.error({ code: 'invalid-debug-args', message: '{@debug ...} arguments must be identifiers, not arbitrary expressions' }, node.start); } }); parser.allowWhitespace(); parser.eat('}', true); } parser.current().children.push({ start, end: parser.index, type: 'DebugTag', identifiers }); } else { const expression = readExpression(parser); parser.allowWhitespace(); parser.eat('}', true); parser.current().children.push({ start, end: parser.index, type: 'MustacheTag', expression, }); } }