diff options
Diffstat (limited to 'packages/astro-parser/src/parse/state')
-rw-r--r-- | packages/astro-parser/src/parse/state/fragment.ts | 21 | ||||
-rw-r--r-- | packages/astro-parser/src/parse/state/mustache.ts | 413 | ||||
-rw-r--r-- | packages/astro-parser/src/parse/state/setup.ts | 35 | ||||
-rw-r--r-- | packages/astro-parser/src/parse/state/tag.ts | 579 | ||||
-rw-r--r-- | packages/astro-parser/src/parse/state/text.ts | 24 |
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); +} |