summaryrefslogtreecommitdiff
path: root/src/compiler/parse/state
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@matthewphillips.info> 2021-03-19 17:07:45 -0400
committerGravatar GitHub <noreply@github.com> 2021-03-19 17:07:45 -0400
commit17c3c98f07628b43b941b84831e8e1f9bcd7ca46 (patch)
tree2e2b3c7d6bd67ebaabe6636ae6867ad368ac6c3a /src/compiler/parse/state
parent8ebc077cb0d9f50aae22d2651bd5ef13fe4641d3 (diff)
downloadastro-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/state')
-rw-r--r--src/compiler/parse/state/fragment.ts14
-rw-r--r--src/compiler/parse/state/mustache.ts783
-rw-r--r--src/compiler/parse/state/tag.ts1006
-rw-r--r--src/compiler/parse/state/text.ts30
4 files changed, 936 insertions, 897 deletions
diff --git a/src/compiler/parse/state/fragment.ts b/src/compiler/parse/state/fragment.ts
index 27c514741..a62748ae0 100644
--- a/src/compiler/parse/state/fragment.ts
+++ b/src/compiler/parse/state/fragment.ts
@@ -4,13 +4,13 @@ import text from './text.js';
import { Parser } from '../index.js';
export default function fragment(parser: Parser) {
- if (parser.match('<')) {
- return tag;
- }
+ if (parser.match('<')) {
+ return tag;
+ }
- if (parser.match('{')) {
- return mustache;
- }
+ if (parser.match('{')) {
+ return mustache;
+ }
- return text;
+ return text;
}
diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts
index d8aa857bf..79372d8d9 100644
--- a/src/compiler/parse/state/mustache.ts
+++ b/src/compiler/parse/state/mustache.ts
@@ -10,403 +10,404 @@ import { TemplateNode } from '../../interfaces.js';
type TODO = any;
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
- if (!block.children || block.children.length === 0) return; // AwaitBlock
+ if (!block.children || block.children.length === 0) return; // AwaitBlock
- const first_child = block.children[0];
- const last_child = block.children[block.children.length - 1];
+ 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 (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 (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 (block.else) {
+ trim_whitespace(block.else, trim_before, trim_after);
+ }
- if (first_child.elseif) {
- trim_whitespace(first_child, 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: TODO;
-
- 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,
- // @ts-ignore
- 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);
-
- // @ts-ignore
- 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);
-
- // @ts-ignore
- 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);
-
- // @ts-ignore
- 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
- // });
- throw new Error('@debug not yet supported');
- } else {
- const expression = read_expression(parser);
-
- parser.allow_whitespace();
- parser.eat('}', true);
-
- // @ts-ignore
- parser.current().children.push({
- start,
- end: parser.index,
- type: 'MustacheTag',
- expression
- });
- }
+ const start = parser.index;
+ parser.index += 1;
+
+ parser.allow_whitespace();
+
+ // {/if}, {/each}, {/await} or {/key}
+ if (parser.eat('/')) {
+ let block = parser.current();
+ let expected: TODO;
+
+ 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,
+ // @ts-ignore
+ 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);
+
+ // @ts-ignore
+ 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);
+
+ // @ts-ignore
+ 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);
+
+ // @ts-ignore
+ 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
+ // });
+ throw new Error('@debug not yet supported');
+ } else {
+ const expression = read_expression(parser);
+
+ parser.allow_whitespace();
+ parser.eat('}', true);
+
+ // @ts-ignore
+ parser.current().children.push({
+ start,
+ end: parser.index,
+ type: 'MustacheTag',
+ expression,
+ });
+ }
}
diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts
index 800b2fd5c..fa5ccb6e3 100644
--- a/src/compiler/parse/state/tag.ts
+++ b/src/compiler/parse/state/tag.ts
@@ -14,29 +14,29 @@ import list from '../../utils/list.js';
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const meta_tags = new Map([
- ['svelte:head', 'Head'],
- ['svelte:options', 'Options'],
- ['svelte:window', 'Window'],
- ['svelte:body', 'Body']
+ ['svelte:head', 'Head'],
+ ['svelte:options', 'Options'],
+ ['svelte:window', 'Window'],
+ ['svelte:body', 'Body'],
]);
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
const specials = new Map([
- [
- 'script',
- {
- read: read_script,
- property: 'js'
- }
- ],
- [
- 'style',
- {
- read: read_style,
- property: 'css'
- }
- ]
+ [
+ 'script',
+ {
+ read: read_script,
+ property: 'js',
+ },
+ ],
+ [
+ 'style',
+ {
+ read: read_style,
+ property: 'css',
+ },
+ ],
]);
const SELF = /^svelte:self(?=[\s/>])/;
@@ -44,489 +44,531 @@ const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/;
function parent_is_head(stack) {
- let i = stack.length;
- while (i--) {
- const { type } = stack[i];
- if (type === 'Head') return true;
- if (type === 'Element' || type === 'InlineComponent') return false;
- }
- return false;
+ let i = stack.length;
+ while (i--) {
+ const { type } = stack[i];
+ if (type === 'Head') return true;
+ if (type === 'Element' || type === 'InlineComponent') return false;
+ }
+ return false;
}
export default function tag(parser: Parser) {
- const start = parser.index++;
-
- let parent = parser.current();
-
- if (parser.eat('!--')) {
- const data = parser.read_until(/-->/);
- parser.eat('-->', true, 'comment was left open, expected -->');
-
- parser.current().children.push({
- start,
- end: parser.index,
- type: 'Comment',
- data
- });
-
- return;
- }
-
- const is_closing_tag = parser.eat('/');
-
- const name = read_tag_name(parser);
-
- if (meta_tags.has(name)) {
- const slug = meta_tags.get(name).toLowerCase();
- if (is_closing_tag) {
- if (
- (name === 'svelte:window' || name === 'svelte:body') &&
- parser.current().children.length
- ) {
- parser.error({
- code: `invalid-${slug}-content`,
- message: `<${name}> cannot have children`
- }, parser.current().children[0].start);
- }
- } else {
- if (name in parser.meta_tags) {
- parser.error({
- code: `duplicate-${slug}`,
- message: `A component can only have one <${name}> tag`
- }, start);
- }
-
- if (parser.stack.length > 1) {
- parser.error({
- code: `invalid-${slug}-placement`,
- message: `<${name}> tags cannot be inside elements or blocks`
- }, start);
- }
-
- parser.meta_tags[name] = true;
- }
- }
-
- const type = meta_tags.has(name)
- ? meta_tags.get(name)
- : (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
- : name === 'svelte:fragment' ? 'SlotTemplate'
- : name === 'title' && parent_is_head(parser.stack) ? 'Title'
- : name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
-
- const element: TemplateNode = {
- start,
- end: null, // filled in later
- type,
- name,
- attributes: [],
- children: []
- };
-
- parser.allow_whitespace();
-
- if (is_closing_tag) {
- if (is_void(name)) {
- parser.error({
- code: 'invalid-void-content',
- message: `<${name}> is a void element and cannot have children, or a closing tag`
- }, start);
- }
-
- parser.eat('>', true);
-
- // close any elements that don't have their own closing tags, e.g. <div><p></div>
- while (parent.name !== name) {
- if (parent.type !== 'Element') {
- const message = parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
- ? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
- : `</${name}> attempted to close an element that was not open`;
- parser.error({
- code: 'invalid-closing-tag',
- message
- }, start);
- }
-
- parent.end = start;
- parser.stack.pop();
-
- parent = parser.current();
- }
-
- parent.end = parser.index;
- parser.stack.pop();
-
- if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
- parser.last_auto_closed_tag = null;
- }
-
- return;
- } else if (closing_tag_omitted(parent.name, name)) {
- parent.end = start;
- parser.stack.pop();
- parser.last_auto_closed_tag = {
- tag: parent.name,
- reason: name,
- depth: parser.stack.length
- };
- }
-
- const unique_names: Set<string> = new Set();
-
- let attribute;
- while ((attribute = read_attribute(parser, unique_names))) {
- element.attributes.push(attribute);
- parser.allow_whitespace();
- }
-
- if (name === 'svelte:component') {
- const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
- if (!~index) {
- parser.error({
- code: 'missing-component-definition',
- message: "<svelte:component> must have a 'this' attribute"
- }, start);
- }
-
- const definition = element.attributes.splice(index, 1)[0];
- if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
- parser.error({
- code: 'invalid-component-definition',
- message: 'invalid component definition'
- }, definition.start);
- }
-
- element.expression = definition.value[0].expression;
- }
-
- // special cases – top-level <script> and <style>
- if (specials.has(name) && parser.stack.length === 1) {
- const special = specials.get(name);
-
- parser.eat('>', true);
- const content = special.read(parser, start, element.attributes);
- if (content) parser[special.property].push(content);
- return;
- }
-
- parser.current().children.push(element);
-
- const self_closing = parser.eat('/') || is_void(name);
-
- parser.eat('>', true);
-
- if (self_closing) {
- // don't push self-closing elements onto the stack
- element.end = parser.index;
- } else if (name === 'textarea') {
- // special case
- element.children = read_sequence(
- parser,
- () =>
- parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
- );
- parser.read(/<\/textarea>/);
- element.end = parser.index;
- } else if (name === 'script' || name === 'style') {
- // special case
- const start = parser.index;
- const data = parser.read_until(new RegExp(`</${name}>`));
- const end = parser.index;
- element.children.push({ start, end, type: 'Text', data });
- parser.eat(`</${name}>`, true);
- element.end = parser.index;
- } else {
- parser.stack.push(element);
- }
+ const start = parser.index++;
+
+ let parent = parser.current();
+
+ if (parser.eat('!--')) {
+ const data = parser.read_until(/-->/);
+ parser.eat('-->', true, 'comment was left open, expected -->');
+
+ parser.current().children.push({
+ start,
+ end: parser.index,
+ type: 'Comment',
+ data,
+ });
+
+ return;
+ }
+
+ const is_closing_tag = parser.eat('/');
+
+ const name = read_tag_name(parser);
+
+ if (meta_tags.has(name)) {
+ const slug = meta_tags.get(name).toLowerCase();
+ if (is_closing_tag) {
+ if ((name === 'svelte:window' || name === 'svelte:body') && parser.current().children.length) {
+ parser.error(
+ {
+ code: `invalid-${slug}-content`,
+ message: `<${name}> cannot have children`,
+ },
+ parser.current().children[0].start
+ );
+ }
+ } else {
+ if (name in parser.meta_tags) {
+ parser.error(
+ {
+ code: `duplicate-${slug}`,
+ message: `A component can only have one <${name}> tag`,
+ },
+ start
+ );
+ }
+
+ if (parser.stack.length > 1) {
+ parser.error(
+ {
+ code: `invalid-${slug}-placement`,
+ message: `<${name}> tags cannot be inside elements or blocks`,
+ },
+ start
+ );
+ }
+
+ parser.meta_tags[name] = true;
+ }
+ }
+
+ const type = meta_tags.has(name)
+ ? meta_tags.get(name)
+ : /[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component'
+ ? 'InlineComponent'
+ : name === 'svelte:fragment'
+ ? 'SlotTemplate'
+ : name === 'title' && parent_is_head(parser.stack)
+ ? 'Title'
+ : name === 'slot' && !parser.customElement
+ ? 'Slot'
+ : 'Element';
+
+ const element: TemplateNode = {
+ start,
+ end: null, // filled in later
+ type,
+ name,
+ attributes: [],
+ children: [],
+ };
+
+ parser.allow_whitespace();
+
+ if (is_closing_tag) {
+ if (is_void(name)) {
+ parser.error(
+ {
+ code: 'invalid-void-content',
+ message: `<${name}> is a void element and cannot have children, or a closing tag`,
+ },
+ start
+ );
+ }
+
+ parser.eat('>', true);
+
+ // close any elements that don't have their own closing tags, e.g. <div><p></div>
+ while (parent.name !== name) {
+ if (parent.type !== 'Element') {
+ const message =
+ parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
+ ? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
+ : `</${name}> attempted to close an element that was not open`;
+ parser.error(
+ {
+ code: 'invalid-closing-tag',
+ message,
+ },
+ start
+ );
+ }
+
+ parent.end = start;
+ parser.stack.pop();
+
+ parent = parser.current();
+ }
+
+ parent.end = parser.index;
+ parser.stack.pop();
+
+ if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
+ parser.last_auto_closed_tag = null;
+ }
+
+ return;
+ } else if (closing_tag_omitted(parent.name, name)) {
+ parent.end = start;
+ parser.stack.pop();
+ parser.last_auto_closed_tag = {
+ tag: parent.name,
+ reason: name,
+ depth: parser.stack.length,
+ };
+ }
+
+ const unique_names: Set<string> = new Set();
+
+ let attribute;
+ while ((attribute = read_attribute(parser, unique_names))) {
+ element.attributes.push(attribute);
+ parser.allow_whitespace();
+ }
+
+ if (name === 'svelte:component') {
+ const index = element.attributes.findIndex((attr) => attr.type === 'Attribute' && attr.name === 'this');
+ if (!~index) {
+ parser.error(
+ {
+ code: 'missing-component-definition',
+ message: "<svelte:component> must have a 'this' attribute",
+ },
+ start
+ );
+ }
+
+ const definition = element.attributes.splice(index, 1)[0];
+ if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
+ parser.error(
+ {
+ code: 'invalid-component-definition',
+ message: 'invalid component definition',
+ },
+ definition.start
+ );
+ }
+
+ element.expression = definition.value[0].expression;
+ }
+
+ // special cases – top-level <script> and <style>
+ if (specials.has(name) && parser.stack.length === 1) {
+ const special = specials.get(name);
+
+ parser.eat('>', true);
+ const content = special.read(parser, start, element.attributes);
+ if (content) parser[special.property].push(content);
+ return;
+ }
+
+ parser.current().children.push(element);
+
+ const self_closing = parser.eat('/') || is_void(name);
+
+ parser.eat('>', true);
+
+ if (self_closing) {
+ // don't push self-closing elements onto the stack
+ element.end = parser.index;
+ } else if (name === 'textarea') {
+ // special case
+ element.children = read_sequence(parser, () => parser.template.slice(parser.index, parser.index + 11) === '</textarea>');
+ parser.read(/<\/textarea>/);
+ element.end = parser.index;
+ } else if (name === 'script' || name === 'style') {
+ // special case
+ const start = parser.index;
+ const data = parser.read_until(new RegExp(`</${name}>`));
+ const end = parser.index;
+ element.children.push({ start, end, type: 'Text', data });
+ parser.eat(`</${name}>`, true);
+ element.end = parser.index;
+ } else {
+ parser.stack.push(element);
+ }
}
function read_tag_name(parser: Parser) {
- const start = parser.index;
-
- if (parser.read(SELF)) {
- // check we're inside a block, otherwise this
- // will cause infinite recursion
- let i = parser.stack.length;
- let legal = false;
-
- while (i--) {
- const fragment = parser.stack[i];
- if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
- legal = true;
- break;
- }
- }
-
- if (!legal) {
- parser.error({
- code: 'invalid-self-placement',
- message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components'
- }, start);
- }
-
- return 'svelte:self';
- }
-
- if (parser.read(COMPONENT)) return 'svelte:component';
-
- if (parser.read(SLOT)) return 'svelte:fragment';
-
- const name = parser.read_until(/(\s|\/|>)/);
-
- if (meta_tags.has(name)) return name;
-
- if (name.startsWith('svelte:')) {
- const match = fuzzymatch(name.slice(7), valid_meta_tags);
-
- let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
- if (match) message += ` (did you mean '${match}'?)`;
-
- parser.error({
- code: 'invalid-tag-name',
- message
- }, start);
- }
-
- if (!valid_tag_name.test(name)) {
- parser.error({
- code: 'invalid-tag-name',
- message: 'Expected valid tag name'
- }, start);
- }
-
- return name;
+ const start = parser.index;
+
+ if (parser.read(SELF)) {
+ // check we're inside a block, otherwise this
+ // will cause infinite recursion
+ let i = parser.stack.length;
+ let legal = false;
+
+ while (i--) {
+ const fragment = parser.stack[i];
+ if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
+ legal = true;
+ break;
+ }
+ }
+
+ if (!legal) {
+ parser.error(
+ {
+ code: 'invalid-self-placement',
+ message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components',
+ },
+ start
+ );
+ }
+
+ return 'svelte:self';
+ }
+
+ if (parser.read(COMPONENT)) return 'svelte:component';
+
+ if (parser.read(SLOT)) return 'svelte:fragment';
+
+ const name = parser.read_until(/(\s|\/|>)/);
+
+ if (meta_tags.has(name)) return name;
+
+ if (name.startsWith('svelte:')) {
+ const match = fuzzymatch(name.slice(7), valid_meta_tags);
+
+ let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
+ if (match) message += ` (did you mean '${match}'?)`;
+
+ parser.error(
+ {
+ code: 'invalid-tag-name',
+ message,
+ },
+ start
+ );
+ }
+
+ if (!valid_tag_name.test(name)) {
+ parser.error(
+ {
+ code: 'invalid-tag-name',
+ message: 'Expected valid tag name',
+ },
+ start
+ );
+ }
+
+ return name;
}
function read_attribute(parser: Parser, unique_names: Set<string>) {
- const start = parser.index;
-
- function check_unique(name: string) {
- if (unique_names.has(name)) {
- parser.error({
- code: 'duplicate-attribute',
- message: 'Attributes need to be unique'
- }, start);
- }
- unique_names.add(name);
- }
-
- if (parser.eat('{')) {
- parser.allow_whitespace();
-
- if (parser.eat('...')) {
- const expression = read_expression(parser);
-
- parser.allow_whitespace();
- parser.eat('}', true);
-
- return {
- start,
- end: parser.index,
- type: 'Spread',
- expression
- };
- } else {
- const value_start = parser.index;
-
- const name = parser.read_identifier();
- parser.allow_whitespace();
- parser.eat('}', true);
-
- check_unique(name);
-
- return {
- start,
- end: parser.index,
- type: 'Attribute',
- name,
- value: [{
- start: value_start,
- end: value_start + name.length,
- type: 'AttributeShorthand',
- expression: {
- start: value_start,
- end: value_start + name.length,
- type: 'Identifier',
- name
- }
- }]
- };
- }
- }
-
- // eslint-disable-next-line no-useless-escape
- const name = parser.read_until(/[\s=\/>"']/);
- if (!name) return null;
-
- let end = parser.index;
-
- parser.allow_whitespace();
-
- const colon_index = name.indexOf(':');
- const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
-
- let value: any[] | true = true;
- if (parser.eat('=')) {
- parser.allow_whitespace();
- value = read_attribute_value(parser);
- end = parser.index;
- } else if (parser.match_regex(/["']/)) {
- parser.error({
- code: 'unexpected-token',
- message: 'Expected ='
- }, parser.index);
- }
-
- if (type) {
- const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
-
- if (type === 'Binding' && directive_name !== 'this') {
- check_unique(directive_name);
- } else if (type !== 'EventHandler' && type !== 'Action') {
- check_unique(name);
- }
-
- if (type === 'Ref') {
- parser.error({
- code: 'invalid-ref-directive',
- message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`
- }, start);
- }
-
- if (type === 'Class' && directive_name === '') {
- parser.error({
- code: 'invalid-class-directive',
- message: 'Class binding name cannot be empty'
- }, start + colon_index + 1);
- }
-
- if (value[0]) {
- if ((value as any[]).length > 1 || value[0].type === 'Text') {
- parser.error({
- code: 'invalid-directive-value',
- message: 'Directive value must be a JavaScript expression enclosed in curly braces'
- }, value[0].start);
- }
- }
-
- const directive: Directive = {
- start,
- end,
- type,
- name: directive_name,
- modifiers,
- expression: (value[0] && value[0].expression) || null
- };
-
- if (type === 'Transition') {
- const direction = name.slice(0, colon_index);
- directive.intro = direction === 'in' || direction === 'transition';
- directive.outro = direction === 'out' || direction === 'transition';
- }
-
- if (!directive.expression && (type === 'Binding' || type === 'Class')) {
- directive.expression = {
- start: directive.start + colon_index + 1,
- end: directive.end,
- type: 'Identifier',
- name: directive.name
- } as any;
- }
-
- return directive;
- }
-
- check_unique(name);
-
- return {
- start,
- end,
- type: 'Attribute',
- name,
- value
- };
+ const start = parser.index;
+
+ function check_unique(name: string) {
+ if (unique_names.has(name)) {
+ parser.error(
+ {
+ code: 'duplicate-attribute',
+ message: 'Attributes need to be unique',
+ },
+ start
+ );
+ }
+ unique_names.add(name);
+ }
+
+ if (parser.eat('{')) {
+ parser.allow_whitespace();
+
+ if (parser.eat('...')) {
+ const expression = read_expression(parser);
+
+ parser.allow_whitespace();
+ parser.eat('}', true);
+
+ return {
+ start,
+ end: parser.index,
+ type: 'Spread',
+ expression,
+ };
+ } else {
+ const value_start = parser.index;
+
+ const name = parser.read_identifier();
+ parser.allow_whitespace();
+ parser.eat('}', true);
+
+ check_unique(name);
+
+ return {
+ start,
+ end: parser.index,
+ type: 'Attribute',
+ name,
+ value: [
+ {
+ start: value_start,
+ end: value_start + name.length,
+ type: 'AttributeShorthand',
+ expression: {
+ start: value_start,
+ end: value_start + name.length,
+ type: 'Identifier',
+ name,
+ },
+ },
+ ],
+ };
+ }
+ }
+
+ // eslint-disable-next-line no-useless-escape
+ const name = parser.read_until(/[\s=\/>"']/);
+ if (!name) return null;
+
+ let end = parser.index;
+
+ parser.allow_whitespace();
+
+ const colon_index = name.indexOf(':');
+ const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
+
+ let value: any[] | true = true;
+ if (parser.eat('=')) {
+ parser.allow_whitespace();
+ value = read_attribute_value(parser);
+ end = parser.index;
+ } else if (parser.match_regex(/["']/)) {
+ parser.error(
+ {
+ code: 'unexpected-token',
+ message: 'Expected =',
+ },
+ parser.index
+ );
+ }
+
+ if (type) {
+ const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
+
+ if (type === 'Binding' && directive_name !== 'this') {
+ check_unique(directive_name);
+ } else if (type !== 'EventHandler' && type !== 'Action') {
+ check_unique(name);
+ }
+
+ if (type === 'Ref') {
+ parser.error(
+ {
+ code: 'invalid-ref-directive',
+ message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`,
+ },
+ start
+ );
+ }
+
+ if (type === 'Class' && directive_name === '') {
+ parser.error(
+ {
+ code: 'invalid-class-directive',
+ message: 'Class binding name cannot be empty',
+ },
+ start + colon_index + 1
+ );
+ }
+
+ if (value[0]) {
+ if ((value as any[]).length > 1 || value[0].type === 'Text') {
+ parser.error(
+ {
+ code: 'invalid-directive-value',
+ message: 'Directive value must be a JavaScript expression enclosed in curly braces',
+ },
+ value[0].start
+ );
+ }
+ }
+
+ const directive: Directive = {
+ start,
+ end,
+ type,
+ name: directive_name,
+ modifiers,
+ expression: (value[0] && value[0].expression) || null,
+ };
+
+ if (type === 'Transition') {
+ const direction = name.slice(0, colon_index);
+ directive.intro = direction === 'in' || direction === 'transition';
+ directive.outro = direction === 'out' || direction === 'transition';
+ }
+
+ if (!directive.expression && (type === 'Binding' || type === 'Class')) {
+ directive.expression = {
+ start: directive.start + colon_index + 1,
+ end: directive.end,
+ type: 'Identifier',
+ name: directive.name,
+ } as any;
+ }
+
+ return directive;
+ }
+
+ check_unique(name);
+
+ return {
+ start,
+ end,
+ type: 'Attribute',
+ name,
+ value,
+ };
}
function get_directive_type(name: string): DirectiveType {
- if (name === 'use') return 'Action';
- if (name === 'animate') return 'Animation';
- if (name === 'bind') return 'Binding';
- if (name === 'class') return 'Class';
- if (name === 'on') return 'EventHandler';
- if (name === 'let') return 'Let';
- if (name === 'ref') return 'Ref';
- if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
+ if (name === 'use') return 'Action';
+ if (name === 'animate') return 'Animation';
+ if (name === 'bind') return 'Binding';
+ if (name === 'class') return 'Class';
+ if (name === 'on') return 'EventHandler';
+ if (name === 'let') return 'Let';
+ if (name === 'ref') return 'Ref';
+ if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
}
function read_attribute_value(parser: Parser) {
- const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
+ const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
- const regex = (
- quote_mark === "'" ? /'/ :
- quote_mark === '"' ? /"/ :
- /(\/>|[\s"'=<>`])/
- );
+ const regex = quote_mark === "'" ? /'/ : quote_mark === '"' ? /"/ : /(\/>|[\s"'=<>`])/;
- const value = read_sequence(parser, () => !!parser.match_regex(regex));
+ const value = read_sequence(parser, () => !!parser.match_regex(regex));
- if (quote_mark) parser.index += 1;
- return value;
+ if (quote_mark) parser.index += 1;
+ return value;
}
function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
- let current_chunk: Text = {
- start: parser.index,
- end: null,
- type: 'Text',
- raw: '',
- data: null
- };
-
- function flush() {
- if (current_chunk.raw) {
- current_chunk.data = decode_character_references(current_chunk.raw);
- current_chunk.end = parser.index;
- chunks.push(current_chunk);
- }
- }
-
- const chunks: TemplateNode[] = [];
-
- while (parser.index < parser.template.length) {
- const index = parser.index;
-
- if (done()) {
- flush();
- return chunks;
- } else if (parser.eat('{')) {
- flush();
-
- parser.allow_whitespace();
- const expression = read_expression(parser);
- parser.allow_whitespace();
- parser.eat('}', true);
-
- chunks.push({
- start: index,
- end: parser.index,
- type: 'MustacheTag',
- expression
- });
-
- current_chunk = {
- start: parser.index,
- end: null,
- type: 'Text',
- raw: '',
- data: null
- };
- } else {
- current_chunk.raw += parser.template[parser.index++];
- }
- }
-
- parser.error({
- code: 'unexpected-eof',
- message: 'Unexpected end of input'
- });
+ let current_chunk: Text = {
+ start: parser.index,
+ end: null,
+ type: 'Text',
+ raw: '',
+ data: null,
+ };
+
+ function flush() {
+ if (current_chunk.raw) {
+ current_chunk.data = decode_character_references(current_chunk.raw);
+ current_chunk.end = parser.index;
+ chunks.push(current_chunk);
+ }
+ }
+
+ const chunks: TemplateNode[] = [];
+
+ while (parser.index < parser.template.length) {
+ const index = parser.index;
+
+ if (done()) {
+ flush();
+ return chunks;
+ } else if (parser.eat('{')) {
+ flush();
+
+ parser.allow_whitespace();
+ const expression = read_expression(parser);
+ parser.allow_whitespace();
+ parser.eat('}', true);
+
+ chunks.push({
+ start: index,
+ end: parser.index,
+ type: 'MustacheTag',
+ expression,
+ });
+
+ current_chunk = {
+ start: parser.index,
+ end: null,
+ type: 'Text',
+ raw: '',
+ data: null,
+ };
+ } else {
+ current_chunk.raw += parser.template[parser.index++];
+ }
+ }
+
+ parser.error({
+ code: 'unexpected-eof',
+ message: 'Unexpected end of input',
+ });
}
diff --git a/src/compiler/parse/state/text.ts b/src/compiler/parse/state/text.ts
index d7183f8b6..d472a46b2 100644
--- a/src/compiler/parse/state/text.ts
+++ b/src/compiler/parse/state/text.ts
@@ -4,25 +4,21 @@ import { decode_character_references } from '../utils/html.js';
import { Parser } from '../index.js';
export default function text(parser: Parser) {
- const start = parser.index;
+ const start = parser.index;
- let data = '';
+ let data = '';
- while (
- parser.index < parser.template.length &&
- !parser.match('<') &&
- !parser.match('{')
- ) {
- data += parser.template[parser.index++];
- }
+ while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
+ data += parser.template[parser.index++];
+ }
- const node = {
- start,
- end: parser.index,
- type: 'Text',
- raw: data,
- data: decode_character_references(data)
- };
+ const node = {
+ start,
+ end: parser.index,
+ type: 'Text',
+ raw: data,
+ data: decode_character_references(data),
+ };
- parser.current().children.push(node);
+ parser.current().children.push(node);
}