svelte/src/compiler/parse/state/mustache.ts

405 lines
9.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import read_context from '../read/context';
import read_expression from '../read/expression';
import { closing_tag_omitted } from '../utils/html';
import { whitespace } from '../../utils/patterns';
import { trim_start, trim_end } from '../../utils/trim';
import { to_string } from '../utils/node';
import { Parser } from '../index';
import { TemplateNode } from '../../interfaces';
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
if (!block.children || block.children.length === 0) return; // AwaitBlock
const first_child = block.children[0];
const last_child = block.children[block.children.length - 1];
if (first_child.type === 'Text' && trim_before) {
first_child.data = trim_start(first_child.data);
if (!first_child.data) block.children.shift();
}
if (last_child.type === 'Text' && trim_after) {
last_child.data = trim_end(last_child.data);
if (!last_child.data) block.children.pop();
}
if (block.else) {
trim_whitespace(block.else, trim_before, trim_after);
}
if (first_child.elseif) {
trim_whitespace(first_child, trim_before, trim_after);
}
}
export default function mustache(parser: Parser) {
const start = parser.index;
parser.index += 1;
parser.allow_whitespace();
// {/if}, {/each}, {/await} or {/key}
if (parser.eat('/')) {
let block = parser.current();
let expected;
if (closing_tag_omitted(block.name)) {
block.end = start;
parser.stack.pop();
block = parser.current();
}
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 if (block.type === 'KeyBlock') {
expected = 'key';
} else {
parser.error({
code: 'unexpected-block-close',
message: 'Unexpected block closing tag'
});
}
parser.eat(expected, true);
parser.allow_whitespace();
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 char_before = parser.template[block.start - 1];
const char_after = parser.template[parser.index];
const trim_before = !char_before || whitespace.test(char_before);
const trim_after = !char_after || whitespace.test(char_after);
trim_whitespace(block, trim_before, trim_after);
block.end = parser.index;
parser.stack.pop();
} else if (parser.eat(':else')) {
if (parser.eat('if')) {
parser.error({
code: 'invalid-elseif',
message: "'elseif' should be 'else if'"
});
}
parser.allow_whitespace();
// :else if
if (parser.eat('if')) {
const block = parser.current();
if (block.type !== 'IfBlock') {
parser.error({
code: 'invalid-elseif-placement',
message: parser.stack.some(block => block.type === 'IfBlock')
? `Expected to close ${to_string(block)} before seeing {:else if ...} block`
: 'Cannot have an {:else if ...} block outside an {#if ...} block'
});
}
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
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 {
// :else
const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error({
code: 'invalid-else-placement',
message: parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock')
? `Expected to close ${to_string(block)} before seeing {:else} block`
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block'
});
}
parser.allow_whitespace();
parser.eat('}', true);
block.else = {
start: parser.index,
end: null,
type: 'ElseBlock',
children: []
};
parser.stack.push(block.else);
}
} else if (parser.match(':then') || parser.match(':catch')) {
const block = parser.current();
const is_then = parser.eat(':then') || !parser.eat(':catch');
if (is_then) {
if (block.type !== 'PendingBlock') {
parser.error({
code: 'invalid-then-placement',
message: parser.stack.some(block => block.type === 'PendingBlock')
? `Expected to close ${to_string(block)} before seeing {:then} block`
: 'Cannot have an {:then} block outside an {#await ...} block'
});
}
} else {
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
parser.error({
code: 'invalid-catch-placement',
message: parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock')
? `Expected to close ${to_string(block)} before seeing {:catch} block`
: 'Cannot have an {:catch} block outside an {#await ...} block'
});
}
}
block.end = start;
parser.stack.pop();
const await_block = parser.current();
if (!parser.eat('}')) {
parser.require_whitespace();
await_block[is_then ? 'value': 'error'] = read_context(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
const new_block: TemplateNode = {
start,
end: null,
type: is_then ? 'ThenBlock': 'CatchBlock',
children: [],
skip: false
};
await_block[is_then ? 'then' : 'catch'] = new_block;
parser.stack.push(new_block);
} 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 if (parser.eat('key')) {
type = 'KeyBlock';
} else {
parser.error({
code: 'expected-block-type',
message: 'Expected if, each, await or key'
});
}
parser.require_whitespace();
const expression = read_expression(parser);
const block: TemplateNode = type === 'AwaitBlock' ?
{
start,
end: null,
type,
expression,
value: null,
error: null,
pending: {
start: null,
end: null,
type: 'PendingBlock',
children: [],
skip: true
},
then: {
start: null,
end: null,
type: 'ThenBlock',
children: [],
skip: true
},
catch: {
start: null,
end: null,
type: 'CatchBlock',
children: [],
skip: true
}
} :
{
start,
end: null,
type,
expression,
children: []
};
parser.allow_whitespace();
// {#each} blocks must declare a context {#each list as item}
if (type === 'EachBlock') {
parser.eat('as', true);
parser.require_whitespace();
block.context = read_context(parser);
parser.allow_whitespace();
if (parser.eat(',')) {
parser.allow_whitespace();
block.index = parser.read_identifier();
if (!block.index) {
parser.error({
code: 'expected-name',
message: 'Expected name'
});
}
parser.allow_whitespace();
}
if (parser.eat('(')) {
parser.allow_whitespace();
block.key = read_expression(parser);
parser.allow_whitespace();
parser.eat(')', true);
parser.allow_whitespace();
}
}
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
if (await_block_shorthand) {
parser.require_whitespace();
block.value = read_context(parser);
parser.allow_whitespace();
}
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
if (await_block_catch_shorthand) {
parser.require_whitespace();
block.error = read_context(parser);
parser.allow_whitespace();
}
parser.eat('}', true);
parser.current().children.push(block);
parser.stack.push(block);
if (type === 'AwaitBlock') {
let child_block;
if (await_block_shorthand) {
block.then.skip = false;
child_block = block.then;
} else if (await_block_catch_shorthand) {
block.catch.skip = false;
child_block = block.catch;
} else {
block.pending.skip = false;
child_block = block.pending;
}
child_block.start = parser.index;
parser.stack.push(child_block);
}
} else if (parser.eat('@html')) {
// {@html content} tag
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
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 = read_expression(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.allow_whitespace();
parser.eat('}', true);
}
parser.current().children.push({
start,
end: parser.index,
type: 'DebugTag',
identifiers
});
} else {
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'MustacheTag',
expression
});
}
}