summaryrefslogtreecommitdiff
path: root/packages/astro-parser/src/parse/state
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro-parser/src/parse/state')
-rw-r--r--packages/astro-parser/src/parse/state/fragment.ts21
-rw-r--r--packages/astro-parser/src/parse/state/mustache.ts413
-rw-r--r--packages/astro-parser/src/parse/state/setup.ts35
-rw-r--r--packages/astro-parser/src/parse/state/tag.ts579
-rw-r--r--packages/astro-parser/src/parse/state/text.ts24
5 files changed, 1072 insertions, 0 deletions
diff --git a/packages/astro-parser/src/parse/state/fragment.ts b/packages/astro-parser/src/parse/state/fragment.ts
new file mode 100644
index 000000000..97398b227
--- /dev/null
+++ b/packages/astro-parser/src/parse/state/fragment.ts
@@ -0,0 +1,21 @@
+import tag from './tag.js';
+import setup from './setup.js';
+import mustache from './mustache.js';
+import text from './text.js';
+import { Parser } from '../index.js';
+
+export default function fragment(parser: Parser) {
+ if (parser.html.children.length === 0 && parser.match_regex(/^---/m)) {
+ return setup;
+ }
+
+ if (parser.match('<')) {
+ return tag;
+ }
+
+ if (parser.match('{')) {
+ return mustache;
+ }
+
+ return text;
+}
diff --git a/packages/astro-parser/src/parse/state/mustache.ts b/packages/astro-parser/src/parse/state/mustache.ts
new file mode 100644
index 000000000..79372d8d9
--- /dev/null
+++ b/packages/astro-parser/src/parse/state/mustache.ts
@@ -0,0 +1,413 @@
+import read_context from '../read/context.js';
+import read_expression from '../read/expression.js';
+import { closing_tag_omitted } from '../utils/html.js';
+import { whitespace } from '../../utils/patterns.js';
+import { trim_start, trim_end } from '../../utils/trim.js';
+import { to_string } from '../utils/node.js';
+import { Parser } from '../index.js';
+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
+
+ 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: 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/packages/astro-parser/src/parse/state/setup.ts b/packages/astro-parser/src/parse/state/setup.ts
new file mode 100644
index 000000000..f64d8c52b
--- /dev/null
+++ b/packages/astro-parser/src/parse/state/setup.ts
@@ -0,0 +1,35 @@
+// @ts-nocheck
+
+import { Parser } from '../index.js';
+
+export default function setup(parser: Parser): void {
+ // TODO: Error if not at top of file? currently, we ignore / just treat as text.
+ // if (parser.html.children.length > 0) {
+ // parser.error({
+ // code: 'unexpected-token',
+ // message: 'Frontmatter scripts only supported at the top of file.',
+ // });
+ // }
+
+ const start = parser.index;
+ parser.index += 3;
+ const content_start = parser.index;
+ const setupScriptContent = parser.read_until(/^---/m);
+ const content_end = parser.index;
+ parser.eat('---', true);
+ const end = parser.index;
+ parser.js.push({
+ type: 'Script',
+ context: 'setup',
+ start,
+ end,
+ content: setupScriptContent,
+ // attributes,
+ // content: {
+ // start: content_start,
+ // end: content_end,
+ // styles,
+ // },
+ });
+ return;
+}
diff --git a/packages/astro-parser/src/parse/state/tag.ts b/packages/astro-parser/src/parse/state/tag.ts
new file mode 100644
index 000000000..a8b919a49
--- /dev/null
+++ b/packages/astro-parser/src/parse/state/tag.ts
@@ -0,0 +1,579 @@
+// @ts-nocheck
+
+import read_expression from '../read/expression.js';
+import read_script from '../read/script.js';
+import read_style from '../read/style.js';
+import { decode_character_references, closing_tag_omitted } from '../utils/html.js';
+import { is_void } from '../../utils/names.js';
+import { Parser } from '../index.js';
+import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces.js';
+import fuzzymatch from '../../utils/fuzzymatch.js';
+import list from '../../utils/list.js';
+
+// eslint-disable-next-line no-useless-escape
+const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
+
+const meta_tags = new Map([
+ ['astro:head', 'Head'],
+ // ['slot:body', 'Body'],
+ // ['astro:options', 'Options'],
+ // ['astro:window', 'Window'],
+ // ['astro:body', 'Body'],
+]);
+
+const valid_meta_tags = Array.from(meta_tags.keys()); //.concat('astro:self', 'astro:component', 'astro:fragment');
+
+const specials = new Map([
+ // Now handled as "setup" in setup.ts
+ // [
+ // 'script',
+ // {
+ // read: read_script,
+ // property: 'js',
+ // },
+ // ],
+ [
+ 'style',
+ {
+ read: read_style,
+ property: 'css',
+ },
+ ],
+]);
+
+const SELF = /^astro:self(?=[\s/>])/;
+const COMPONENT = /^astro:component(?=[\s/>])/;
+const SLOT = /^astro:fragment(?=[\s/>])/;
+const HEAD = /^head(?=[\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;
+}
+
+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 === 'astro:window' || name === 'astro: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 === 'astro:self' || name === 'astro:component'
+ ? 'InlineComponent'
+ : name === 'astro: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 === 'astro:component') {
+ const index = element.attributes.findIndex((attr) => attr.type === 'Attribute' && attr.name === 'this');
+ if (!~index) {
+ parser.error(
+ {
+ code: 'missing-component-definition',
+ message: "<astro: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: '<astro:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components',
+ },
+ start
+ );
+ }
+
+ return 'astro:self';
+ }
+
+ if (parser.read(COMPONENT)) return 'astro:component';
+
+ if (parser.read(SLOT)) return 'astro:fragment';
+
+ if (parser.read(HEAD)) return 'head';
+
+ const name = parser.read_until(/(\s|\/|>)/);
+
+ if (meta_tags.has(name)) return name;
+
+ if (name.startsWith('astro:')) {
+ const match = fuzzymatch(name.slice(7), valid_meta_tags);
+
+ let message = `Valid <astro:...> 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,
+ };
+}
+
+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';
+}
+
+function read_attribute_value(parser: Parser) {
+ const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
+
+ const regex = quote_mark === "'" ? /'/ : quote_mark === '"' ? /"/ : /(\/>|[\s"'=<>`])/;
+
+ const value = read_sequence(parser, () => !!parser.match_regex(regex));
+
+ 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',
+ });
+}
diff --git a/packages/astro-parser/src/parse/state/text.ts b/packages/astro-parser/src/parse/state/text.ts
new file mode 100644
index 000000000..cca83f2d4
--- /dev/null
+++ b/packages/astro-parser/src/parse/state/text.ts
@@ -0,0 +1,24 @@
+// @ts-nocheck
+
+import { decode_character_references } from '../utils/html.js';
+import { Parser } from '../index.js';
+
+export default function text(parser: Parser) {
+ const start = parser.index;
+
+ let data = '';
+
+ while (parser.index < parser.template.length && !parser.match('---') && !parser.match('<') && !parser.match('{')) {
+ data += parser.template[parser.index++];
+ }
+
+ const node = {
+ start,
+ end: parser.index,
+ type: 'Text',
+ raw: data,
+ data: decode_character_references(data),
+ };
+
+ parser.current().children.push(node);
+}