import type { LiquidVariableLookup, BlockArrayArgument, LiquidConditionalExpression, ComplexLiquidExpression, LiquidVariable, LiquidFilter, LiquidArgument, LiquidNamedArgument } from '../ast'; import type { ValueExpression, Expression } from './expression-adapter'; import { MarkupTokenType } from './tokenizer'; import type { MarkupToken } from './tokenizer'; export declare class MarkupParser { /** * Ruby's `VariableSegment = /[\w\-]/`; `VariableParser` scans `[\w-]+\??` * runs. Used by lax lookup recovery to read a glued segment (digits, leading * dashes) directly from source rather than relying on token types. Sticky so * it can be anchored with `lastIndex`. */ private static readonly WORD_SEGMENT_RE; private tokens; private source; private p; private markupStart; private markupEnd; /** * When true, the parser performs Ruby-style lax recovery instead of throwing * on malformed markup. Defaults to false. This flag is NEVER set by the * document-layer parsers (so `toLiquidHtmlAST` and `theme-check` always parse * strictly); it is enabled exclusively by the render-tree's lax-recovery path * (`liquid-render-tree`) when it must reproduce Ruby's lax rendering of an * already-failed (string-markup) `{{ }}` or tag. See * `liquid-render-tree/src/lax-recover.ts`. */ private lax; /** * Tolerant recovery axis, DISJOINT from `lax`. Enabled exclusively by the * tolerant document parser (`TolerantDocumentParser`) so the formatter can * build a best-effort tag/markup node instead of discarding markup. It never * affects strict parsing (`toLiquidHtmlAST` / `theme-check`) and it does NOT * change lax (Ruby-render-parity) behavior. */ private tolerant; /** * True while parsing a condition (`if`/`unless`/`elsif`). In lax mode this * permits stripping meaningless grouping parens (`(false || true)`), which is * Ruby condition behavior. It must stay false in value contexts (`{{ }}`, * assign RHS) where a leading paren instead denotes a meaningless-paren * lookup that resolves to nil (`('X' | downcase)` → nil). */ private inCondition; constructor(tokens: MarkupToken[], source: string, markupStart?: number, markupEnd?: number); /** Enables lax recovery for subsequent parse calls. Returns `this` for * chaining. Only the render-tree lax-recovery path calls this. */ enableLax(): this; /** True when lax recovery is enabled. Tag parse callbacks consult this to add * tag-specific recovery (e.g. numeric assign names) without affecting strict * parsing. */ isLax(): boolean; /** Enables tolerant recovery for subsequent parse calls. Returns `this` for * chaining. Only the tolerant document parser calls this. Disjoint from * lax — enabling one does not enable the other. */ enableTolerant(): this; /** True when tolerant recovery is enabled. Tag/markup parse callbacks consult * this to build a best-effort node instead of discarding markup, without * affecting strict or lax parsing. */ isTolerant(): boolean; /** Returns the raw source text from the current token up to (but excluding) * the token whose type is `stop` (or end of markup), trimmed. Advances the * cursor past everything consumed. Used by lax recovery to capture an * invalid identifier (e.g. `123foo`) as a literal name. */ consumeRawUntil(stop: MarkupTokenType): string; /** Advances past every remaining token so isAtEnd() reports true. Used by lax * recovery to "silently eat" residual garbage after a value has been parsed. */ private discardRemaining; /** Public lax-only entry point to `discardRemaining()`. Tag parse callbacks * (e.g. `whenBranchParse`) call this to drop a residual trailing element * after a complete value, mirroring Ruby's lax fragment drop. No-op unless * lax recovery is enabled, so strict parsing is unaffected (DD-7). */ discardTrailing(): void; /** Lax helper: true when the current token can begin a value expression. * Public so tag branch parsers (e.g. `whenBranchParse`) can peek whether a * value follows a separator before consuming, to mirror Ruby dropping a * trailing separator with no following value. */ atValueStart(): boolean; /** Lax helper: true when the current token can begin a filter argument * (a value or a named-argument key). */ private atArgumentStart; /** Lax helper: true when an upcoming `[` directly continues a variable lookup * (`foo[0]`, `foo [0]`) rather than being a separate fragment split off by a * dropped unknown operator (`foo ! [0]`). The tokenizer silently drops the * operator characters it cannot tokenize (`!`, `~`, …), so a `[` the token * stream places right after an identifier may actually be separated from it * in the source by such an operator. Ruby's lax `if.rb` splits on that * operator run and captures the bracket as its own (opaque) right fragment, * so it must NOT be chained as bracket access — otherwise `foo ! [` parses as * the lookup `foo[…]` and throws on the unterminated bracket instead of * short-circuiting (`{% if false and foo ! [ %}…{% else %}NO{% endif %}` → * `NO` in Ruby). A whitespace-only gap (`foo [0]`) is a genuine lookup in * Ruby and stays chained. Strict mode never drops operator characters, so the * token adjacency the strict grammar relies on is authoritative there. */ private openSquareContinuesLookup; /** Lax helper: advance the cursor to the next `|` separator (leaving it * unconsumed) or to end-of-markup. Used by `filters()` to resync the filter * chain after a malformed filter argument. */ private skipToNextPipe; consume(type: MarkupTokenType): MarkupToken; consumeOptional(type: MarkupTokenType): MarkupToken | null; look(type: MarkupTokenType, ahead?: number): boolean; peek(): MarkupToken; id(keyword: string): boolean; isAtEnd(): boolean; /** Returns the start position of the EndOfString sentinel token. * This is the markup boundary — the position just past the last meaningful * content, including any trailing whitespace before the closing delimiter. */ eosStart(): number; /** Returns the document source and byte range of the full markup string. * Advances the parser to end-of-string so isAtEnd() returns true. */ bodyContext(): { source: string; bodyStart: number; bodyEnd: number; }; /** Returns the source text from the current token position to end of markup, trimmed. */ remainingSource(): string; valueExpression(): ValueExpression; /** Lax helper: scan from the current position (just past an OpenRound) to the * matching CloseRound and report whether a `..` range separator appears at * the same paren depth. Distinguishes a real range `(1..5)` from meaningless * parens `('X' | downcase)`. Does not advance the cursor. */ private parenContainsRange; variableLookup(): LiquidVariableLookup; /** * Lax helper for `variableLookup()`. Mirrors Ruby's `VariableParser` scanning * `[\w\-]+` runs out of a single space-free `QuotedFragment`, silently * skipping interleaved non-word punctuation (e.g. `foo=>bar` → lookups * `foo`/`bar`). Returns the recovered segment (value + source range, advancing * the cursor past every token the run covers) iff, starting at `prevEnd`, a * CONTIGUOUS run of non-word punctuation (`=`/`>`/`<`/`!`) is immediately * followed by a CONTIGUOUS `[\w-]+` segment — all with NO whitespace gap (a * whitespace gap ends the QuotedFragment, so the trailing word is a separate * fragment and is dropped). Otherwise returns null. * * The segment is read directly from `this.source` rather than from a single * trailing token, because Ruby's `VariableSegment = [\w\-]` treats digits and * `-` as word characters: `foo=>123` → `foo["123"]` and `foo=>-bar` → * `foo["-bar"]`. The JS tokenizer instead splits those into `Number` and * leading-`Dash` tokens, so matching token types alone would drop the numeric * segment and strip the leading dash. Scanning the raw `[\w-]+\??` run keeps * full Ruby parity. Tokens that are real value/list/filter separators (`.` * `[` `,` `|` `:` `..` `or`/`and`, spaced operators) are never crossed. * Lax-only; never reached by strict parsing (DD-7), so the strict AST is * unchanged. */ private tryConsumeLaxLookupSeparator; /** * Lax look-ahead for `valueExpression()`: reports whether, starting * immediately after `prevEnd` (the end of a just-peeked literal/number base), * a contiguous symbolic-operator run (`=`/`>`/`<`/`!`) follows with no * whitespace gap. In a CONDITION the run must also be followed by a contiguous * `[\w-]+` segment (the shape `tryConsumeLaxLookupSeparator` would absorb); in * a VALUE context the operator run alone suffices (see below). Does NOT advance * the cursor. * * Used to decide whether a literal keyword (`true=>bar`) or a bare number * (`1=>bar`) should be routed through `variableLookup()` so the contiguous * `=>bar` suffix folds into a lookup base (`true["bar"]` / `1["bar"]`), * matching Ruby's `Expression.parse` falling through to `VariableLookup.parse` * (`expression.rb:40-46`) instead of returning the bare literal/number. Without * this, `valueExpression()` returns the literal directly without ever entering * `variableLookup()`, so the lax fold never runs and the suffix is dropped by * `discardTrailing()`. The same routing also covers a DANGLING operator run * with no trailing word (`true=>`, `1<`) in a value context, which Ruby still * resolves to a `VariableLookup` with empty lookups (→ nil). Lax-only; never * reached by strict parsing (DD-7). */ private laxLookupSeparatorFollows; comparison(): Expression; /** Lax-condition helper: the unknown operator Ruby would capture from the raw * source — the contiguous `[=!<>a-z_]+` run starting at the first operator * character after the left operand (skipping intervening whitespace). Returns * `null` when the next non-space character is outside that class (no * operator: the bare left operand stands — `true && false`, `true TRUE`). * Mirrors `if.rb`'s lax `Syntax` operator group; the run is captured verbatim * so `condition.rb` raises `Unknown operator ` for multi-token garbage * (`=!`, `===`, `` at evaluate time (mirrors Ruby `condition.rb`, where the * operator is captured during lax parse but only rejected when interpreted). * Every token covered by the raw operator run is consumed — a multi-token * garbage operator (`a-z_]+)?\s*(QuotedFragment)?`, which * captures `arr` / `contains` / `0` and evaluates `arr contains 0` normally. * When nothing follows the run, Ruby's group-3 `QuotedFragment` is empty, so * the right operand resolves to nil (NOT the left operand) — a null-named * VariableLookup placeholder. Only called under * `lax && inCondition` for a run where `isComparisonOp(op.value)` is true. */ private mergedOperatorComparison; /** Lax helper: skip leading OpenRound tokens that do not introduce a range. * Meaningless parens in conditions (`(false || true)`) are stripped by * Ruby's lax condition parser. */ private skipLaxConditionParens; /** Lax helper: like parenContainsRange but assumes the cursor is ON the * OpenRound; scans its body for a depth-0 `..`. */ private parenAtCursorContainsRange; logicalExpr(): Expression; conditionalExpression(): LiquidConditionalExpression; expression(): ComplexLiquidExpression; liquidVariable(): LiquidVariable; filters(previousEnd?: number): LiquidFilter[]; filter(previousEnd: number): LiquidFilter; argument(): LiquidArgument; private lookaheadForNamedArgument; namedArgument(): LiquidNamedArgument; arguments(): LiquidArgument[]; namedArguments(): LiquidNamedArgument[]; blockNamedArguments(): (LiquidNamedArgument | BlockArrayArgument)[]; private pushBlockNamedArgument; private blockValueExpression; private assertNotNestedArray; private blockArrayLiteral; private computeLastTokenEnd; }