/** * AST type definitions for the Liquid+HTML parser. * * Given this input: * {% if cond %}hi there!{% endif %} * * The parser produces this AST: * - LiquidTag/if * condition: LiquidVariableExpression * children: * - TextNode/"hi " * - HtmlElement/em * children: * - TextNode/"there!" */ import { Comparators, NamedTags, NodeTypes, RawTags } from './types'; import type { Position } from './types'; import { Environment } from './environment'; /** The union type of all possible node types inside a LiquidHTML AST. */ export type LiquidHtmlNode = DocumentNode | YAMLFrontmatter | LiquidNode | HtmlDoctype | HtmlNode | AttributeNode | LiquidVariable | ComplexLiquidExpression | LiquidFilter | LiquidNamedArgument | AssignMarkup | BlockMarkup | ContentForMarkup | CycleMarkup | ForMarkup | RenderMarkup | SectionMarkup | PaginateMarkup | RawMarkup | RenderVariableExpression | RenderAliasExpression | LiquidLogicalExpression | LiquidComparison | TextNode | LiquidDocParamNode | LiquidDocExampleNode | LiquidDocPromptNode | LiquidDocDescriptionNode | LiquidErrorNode; /** The root node of all LiquidHTML ASTs. */ export interface DocumentNode extends ASTNode { children: LiquidHtmlNode[]; name: '#document'; _source: string; } export interface YAMLFrontmatter extends ASTNode { body: string; } /** The union type of every node that are considered Liquid. {% ... %}, {{ ... }} */ export type LiquidNode = LiquidRawTag | LiquidTag | LiquidVariableOutput | LiquidBranch; /** The union type of every node that could should up in a {% liquid %} tag */ export type LiquidStatement = LiquidRawTag | LiquidTag | LiquidBranch; export interface HasChildren { children?: LiquidHtmlNode[]; } export interface HasAttributes { attributes: AttributeNode[]; } export interface HasValue { value: (TextNode | LiquidNode)[]; } export interface HasName { name: string | LiquidVariableOutput; } export type CompoundNameSegment = TextNode | LiquidVariableOutput | LiquidTag | LiquidRawTag; export interface HasCompoundName { name: (TextNode | LiquidNode)[]; } export type ParentNode = Extract; /** * A LiquidRawTag is one that is parsed such that its body is a raw string. * * Examples: * - {% raw %}...{% endraw %} * - {% javascript %}...{% endjavascript %} * - {% style %}...{% endstyle %} */ export interface LiquidRawTag extends ASTNode { name: RawTags; /** The non-name part inside the opening Liquid tag. {% tagName [markup] %} */ markup: string; /** * The source range of the raw markup content between the tag name and * closing delimiter. This can include bytes that the parsed markup AST * skips; use markup.position for the parsed markup node's own range. */ markupPosition: Position; /** String body of the tag. We don't try to parse it. */ body: RawMarkup; /** {%- tag %} */ whitespaceStart: '-' | ''; /** {% tag -%} */ whitespaceEnd: '-' | ''; /** {%- endtag %} */ delimiterWhitespaceStart: '-' | ''; /** {% endtag -%} */ delimiterWhitespaceEnd: '-' | ''; /** the range of the opening tag {% tag %} */ blockStartPosition: Position; /** the range of the closing tag {% endtag %}*/ blockEndPosition: Position; } /** The union type of strictly typed and loosely typed Liquid tag nodes */ export type LiquidTag = LiquidTagNamed | LiquidTagBaseCase; /** The union type of all strictly typed LiquidTag nodes */ export type LiquidTagNamed = LiquidTagAssign | LiquidTagBlock | LiquidTagCase | LiquidTagCapture | LiquidTagContentFor | LiquidTagCycle | LiquidTagDecrement | LiquidTagEcho | LiquidTagFor | LiquidTagForm | LiquidTagIf | LiquidTagIfchanged | LiquidTagInclude | LiquidTagIncrement | LiquidTagLayout | LiquidTagLiquid | LiquidTagPaginate | LiquidTagPartial | LiquidTagRender | LiquidTagSection | LiquidTagSections | LiquidTagTablerow | LiquidTagUnless; export interface LiquidTagNode extends ASTNode { /** e.g. if, ifchanged, for, etc. */ name: Name; /** The non-name part inside the opening Liquid tag. {% tagName [markup] %} */ markup: Markup; /** If the node has child nodes, the array of child nodes */ children?: LiquidHtmlNode[]; /** {%- tag %} */ whitespaceStart: '-' | ''; /** {% tag -%} */ whitespaceEnd: '-' | ''; /** {%- endtag %}, if it has one */ delimiterWhitespaceStart?: '-' | ''; /** {% endtag -%}, if it has one */ delimiterWhitespaceEnd?: '-' | ''; /** the range of the opening tag {% tag %} */ blockStartPosition: Position; /** the range of the closing tag {% endtag %}, if it has one */ blockEndPosition?: Position; /** * The source range of the raw markup content between the tag name and * closing delimiter. This can include bytes that the parsed markup AST * skips; use markup.position for the parsed markup node's own range. */ markupPosition: Position; } /** * LiquidTagBaseCase exists as a fallback for when we can't strictly parse a tag. * * For any of the following reasons: * - there's a syntax error in the markup and we want to be resilient * - the parser does not support the tag (yet) and we want to be resilient * * As such, when we parse `{% tagName [markup] %}`, LiquidTagBaseCase is the * case where `markup` is parsed as a string instead of anything specific. */ export interface LiquidTagBaseCase extends LiquidTagNode { reason?: string; } /** https://shopify.dev/docs/api/liquid/tags#echo */ export interface LiquidTagEcho extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#assign */ export interface LiquidTagAssign extends LiquidTagNode { } /** {% assign name = value %} */ export interface AssignMarkup extends ASTNode { /** the name of the variable that is being assigned */ name: string; /** the value of the variable that is being assigned */ value: LiquidVariable; } /** https://shopify.dev/docs/api/liquid/tags#increment */ export interface LiquidTagIncrement extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#decrement */ export interface LiquidTagDecrement extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#capture */ export interface LiquidTagCapture extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#ifchanged */ export interface LiquidTagIfchanged extends LiquidTagNode { } /** Shopify partial tag: {% partial 'name' %} */ export interface LiquidTagPartial extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#cycle */ export interface LiquidTagCycle extends LiquidTagNode { } /** {% cycle [groupName:] arg1, arg2, arg3 %} */ export interface CycleMarkup extends ASTNode { /** {% cycle groupName: arg1, arg2, arg3 %} */ groupName: LiquidExpression | null; /** {% cycle arg1, arg2, arg3, ... %} */ args: LiquidExpression[]; } /** https://shopify.dev/docs/api/liquid/tags#case */ export interface LiquidTagCase extends LiquidTagNode { } /** * {% when expression1, expression2 or expression3 %} * children */ export interface LiquidBranchWhen extends LiquidBranchNode { } /** https://shopify.dev/docs/api/liquid/tags#form */ export interface LiquidTagForm extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#for */ export interface LiquidTagFor extends LiquidTagNode { } /** {% for variableName in collection [reversed] [...namedArguments] %} */ export interface ForMarkup extends ASTNode { /** {% for variableName in collection %} */ variableName: string; /** {% for variableName in collection %} */ collection: LiquidExpression; /** Whether the for loop is reversed */ reversed: boolean; /** Holds arguments such as limit: 10, offset: 3 */ args: LiquidNamedArgument[]; } /** https://shopify.dev/docs/api/liquid/tags#tablerow */ export interface LiquidTagTablerow extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#if */ export interface LiquidTagIf extends LiquidTagConditional { } /** https://shopify.dev/docs/api/liquid/tags#unless */ export interface LiquidTagUnless extends LiquidTagConditional { } /** {% elsif cond %} */ export interface LiquidBranchElsif extends LiquidBranchNode { } export interface LiquidTagConditional extends LiquidTagNode { } /** The union type of all conditional expression nodes */ export type LiquidConditionalExpression = LiquidLogicalExpression | LiquidComparison | LiquidExpression; /** Represents `left (and|or) right` conditional expressions */ export interface LiquidLogicalExpression extends ASTNode { relation: 'and' | 'or'; left: LiquidConditionalExpression; right: LiquidConditionalExpression; } /** Represents `left (<|<=|=|>=|>|contains) right` conditional expressions */ export interface LiquidComparison extends ASTNode { comparator: Comparators; left: LiquidExpression; right: LiquidExpression; } /** https://shopify.dev/docs/api/liquid/tags#paginate */ export interface LiquidTagPaginate extends LiquidTagNode { } /** {% paginate collection by pageSize [...namedArgs] %} */ export interface PaginateMarkup extends ASTNode { /** {% paginate collection by pageSize %} */ collection: LiquidExpression; /** {% paginate collection by pageSize %} */ pageSize: LiquidExpression; /** optional named arguments such as `window_size: 10` */ args: LiquidNamedArgument[]; } /** https://shopify.dev/docs/api/liquid/tags#content_for */ export interface LiquidTagContentFor extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#render */ export interface LiquidTagRender extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#include */ export interface LiquidTagInclude extends LiquidTagNode { } /** Shopify block tag: {% block 'name' [, kwargs] %}...{% endblock %} */ export interface LiquidTagBlock extends LiquidTagNode { } /** {% block 'name' [, key: value] %} */ export interface BlockMarkup extends ASTNode { /** The name of the block template */ name: LiquidString; args: (LiquidNamedArgument | BlockArrayArgument)[]; } /** https://shopify.dev/docs/api/liquid/tags#section */ export interface LiquidTagSection extends LiquidTagNode { } /** {% section 'name' [, key: value] %} */ export interface SectionMarkup extends ASTNode { /** The name of the section template */ name: LiquidString; /** Optional named arguments */ args: (LiquidNamedArgument | BlockArrayArgument)[]; } /** https://shopify.dev/docs/api/liquid/tags#sections */ export interface LiquidTagSections extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#layout */ export interface LiquidTagLayout extends LiquidTagNode { } /** https://shopify.dev/docs/api/liquid/tags#liquid */ export interface LiquidTagLiquid extends LiquidTagNode { } /** {% content_for 'contentForType' [...namedArguments] %} */ export interface ContentForMarkup extends ASTNode { /** {% content_for 'contentForType' %} */ contentForType: LiquidString; /** * WARNING: `args` could contain LiquidVariableLookup when we are in a completion context * because the NamedArgument isn't fully typed out. * E.g. {% content_for 'contentForType', arg1: value1, arg2█ %} * * @example {% content_for 'contentForType', arg1: value1, arg2: value2 %} */ args: LiquidNamedArgument[]; } /** {% render 'snippet' [(with|for) variable [as alias]], [...namedArguments] %} */ export interface RenderMarkup extends ASTNode { /** * {% render snippet %} * * For `render`, the parser restricts this to a quoted string or variable * lookup. For `include`, Ruby accepts any expression as the template name and * raises "Illegal template name" at render time when it is not a String, so * the snippet may also be a number / literal (e.g. `{% include 123 %}`, * `{% include nil %}`). */ snippet: LiquidExpression; /** {% render 'snippet' with thing as alias %} */ alias: RenderAliasExpression | null; /** {% render 'snippet' [with variable] %} */ variable: RenderVariableExpression | null; /** * WARNING: `args` could contain LiquidVariableLookup when we are in a completion context * because the NamedArgument isn't fully typed out. * E.g. {% render 'snippet', arg1: value1, arg2█ %} * * @example {% render 'snippet', arg1: value1, arg2: value2 %} */ args: LiquidNamedArgument[]; } /** Represents the `for name` or `with name` expressions in render nodes */ export interface RenderVariableExpression extends ASTNode { /** {% render 'snippet' (for|with) name %} */ kind: 'for' | 'with'; /** {% render 'snippet' (for|with) name %} */ name: LiquidExpression; } /** Represents the `as name` expressions in render nodes */ export interface RenderAliasExpression extends ASTNode { /** {% render 'snippet' for array as name %}` or `{% render 'snippet' with object as name %} */ value: string; } /** The union type of the strictly and loosely typed LiquidBranch nodes */ export type LiquidBranch = LiquidBranchUnnamed | LiquidBranchBaseCase | LiquidBranchNamed; /** The union type of the strictly typed LiquidBranch nodes */ export type LiquidBranchNamed = LiquidBranchElsif | LiquidBranchWhen; interface LiquidBranchNode extends ASTNode { /** * The liquid tag name of the branch, null when the first branch. * * {% if condA %} * defaultBranchContents * {% elseif condB %} * elsifBranchContents * {% endif %} * * This creates the following AST: * type: LiquidTag * name: if * markup: condA * children: * - type: LiquidBranch * name: null * markup: '' * children: * - defaultBranchContents * - type: LiquidBranch * name: elsif * markup: condB * children: * - elsifBranchContents */ name: Name; /** {% name [markup] %} */ markup: Markup; /** * The source range of the raw markup content between the branch name and * delimiter. This can include bytes that the parsed markup AST skips; use * markup.position for the parsed markup node's own range. */ markupPosition: Position; /** The child nodes of the branch */ children: LiquidHtmlNode[]; /** {%- elsif %} */ whitespaceStart: '-' | ''; /** {% elsif -%} */ whitespaceEnd: '-' | ''; /** Range of the LiquidTag that delimits the branch (does not include children) */ blockStartPosition: Position; /** 0-size range that delimits where the branch ends, (-1, -1) when unclosed */ blockEndPosition: Position; } /** * The first branch inside branched statements (e.g. if, when, for) * * This one is different in the sense that it doesn't have a name or markup * since that information is held by the parent node. */ export interface LiquidBranchUnnamed extends LiquidBranchNode { } /** Loosely typed LiquidBranch nodes. Markup is a string because we can't strictly parse it. */ export interface LiquidBranchBaseCase extends LiquidBranchNode { } /** Represents {{ expression (| filters)* }}. Its position includes the braces. */ export interface LiquidVariableOutput extends ASTNode { /** The body of the variable output. May contain filters. Not trimmed. */ markup: string | LiquidVariable; /** * The source range of the raw markup content between output delimiters. * This can include bytes that the parsed markup AST skips; use * markup.position for the parsed markup node's own range. */ markupPosition: Position; /** {{- variable }} */ whitespaceStart: '-' | ''; /** {{ variable -}} */ whitespaceEnd: '-' | ''; } /** Represents an expression and filters, e.g. expression | filter1 | filter2 */ export interface LiquidVariable extends ASTNode { /** expression | filter1 | filter2 */ expression: ComplexLiquidExpression; /** expression | filter1 | filter2 */ filters: LiquidFilter[]; /** Used internally */ rawSource: string; } /** The union type of all Liquid expression nodes */ export type LiquidExpression = LiquidString | LiquidNumber | LiquidLiteral | LiquidRange | LiquidVariableLookup; export type ComplexLiquidExpression = LiquidBooleanExpression | LiquidExpression; /** https://shopify.dev/docs/api/liquid/filters */ export interface LiquidFilter extends ASTNode { /** name: arg1, arg2, namedArg3: value3 */ name: string; /** name: arg1, arg2, namedArg3: value3 */ args: LiquidArgument[]; } /** Represents the union type of positional and named arguments */ export type LiquidArgument = LiquidExpression | LiquidNamedArgument; /** Named arguments are the ones used in kwargs, such as `name: value` */ export interface LiquidNamedArgument extends ASTNode { /** name: value */ name: string; /** name: value */ value: LiquidExpression; } export interface LiquidBooleanExpression extends ASTNode { condition: LiquidConditionalExpression; } /** https://shopify.dev/docs/api/liquid/basics#string */ export interface LiquidString extends ASTNode { /** single or double quote? */ single: boolean; /** The contents of the string, parsed, does not included the delimiting quote characters */ value: string; } /** https://shopify.dev/docs/api/liquid/basics#number */ export interface LiquidNumber extends ASTNode { /** as a string for compatibility with numbers like 100_000 */ value: string; } /** https://shopify.dev/docs/api/liquid/tags/for#for-range */ export interface LiquidRange extends ASTNode { start: LiquidExpression; end: LiquidExpression; } /** * A single-level array literal, e.g. `['a', 'b']` or `[1, 2, x]`. * Supported as a block or section tag argument value. Nested arrays are rejected. * * Reachable only via `BlockArrayArgument.value` and the block/section parser path. */ export interface BlockArrayLiteral extends ASTNode<'BlockArrayLiteral'> { /** The parsed element expressions, in source order */ elements: LiquidExpression[]; } /** * Tag arg whose value is a literal array. Lives on block and section markup. */ export interface BlockArrayArgument extends ASTNode { /** name: value */ name: string; /** name: value (always an array literal) */ value: BlockArrayLiteral; } /** Mapping from literal keyword to its JS value */ export declare const LiquidLiteralValues: { nil: null; null: null; true: true; false: false; blank: ""; empty: ""; }; /** empty, null, true, false, etc. */ export interface LiquidLiteral extends ASTNode { /** string representation of the literal (e.g. 'nil', 'true', 'blank') */ keyword: keyof typeof LiquidLiteralValues; /** value representation of the literal (e.g. null, true, '') */ value: (typeof LiquidLiteralValues)[keyof typeof LiquidLiteralValues]; } /** * What we think of when we think of variables. * variable.lookup1[lookup2].lookup3 */ export interface LiquidVariableLookup extends ASTNode { /** * The root name of the lookup, `null` for the global access exception: * {{ product }} -> name = 'product', lookups = [] * {{ ['product'] }} -> name = null, lookups = ['product'] */ name: string | null; /** name.lookup1[lookup2] */ lookups: LiquidExpression[]; /** * Per-segment access mode, parallel to {@link lookups}: `"bareword"` for a * dot/identifier segment (`.first`), `"subscript"` for a bracket segment * (`["first"]`). Mirrors Ruby `VariableLookup#@command_flags` * (variable_lookup.rb:27-45): only a bareword segment may resolve a command * method (`first`/`last`/`size`); a string subscript on an Array must not. * * Optional and purely additive — strict `toLiquidHtmlAST` consumers and * `toMatchObject`-style assertions are unaffected when it is absent. */ lookupModes?: LiquidVariableLookupMode[]; } /** Access mode of a single {@link LiquidVariableLookup} segment. */ export type LiquidVariableLookupMode = 'bareword' | 'subscript'; /** The union type of all HTML nodes */ export type HtmlNode = HtmlComment | HtmlElement | HtmlDanglingMarkerClose | HtmlVoidElement | HtmlSelfClosingElement | HtmlRawNode; /** The basic HTML node with an opening and closing tags. */ export interface HtmlElement extends HtmlNodeBase { /** * The name of the tag can be compound * e.g. `<{{ header_type }}--header />` */ name: CompoundNameSegment[]; /** The child nodes delimited by the start and end tags */ children: LiquidHtmlNode[]; /** The range covered by the end tag */ blockEndPosition: Position; } /** * The node used to represent close tags without its matching opening tag. * * Typically found inside if statements. * ``` * {% if cond %} * * {% endif %} * ``` */ export interface HtmlDanglingMarkerClose extends ASTNode { /** * The name of the tag can be compound * e.g. `<{{ header_type }}--header />` */ name: CompoundNameSegment[]; /** The range covered by the dangling end tag */ blockStartPosition: Position; } export interface HtmlSelfClosingElement extends HtmlNodeBase { /** * The name of the tag can be compound * @example `<{{ header_type }}--header />` */ name: CompoundNameSegment[]; } /** * Represents HTML Void elements. The ones that cannot have child nodes. * * https://developer.mozilla.org/en-US/docs/Glossary/Void_element */ export interface HtmlVoidElement extends HtmlNodeBase { /** This one can't have a compound name since they come from a list */ name: string; } /** * Special case of HTML Element for which we don't want to try to parse the * children. The children is parsed as a raw string. * * e.g. `script`, `style` */ export interface HtmlRawNode extends HtmlNodeBase { /** The innerHTML of the tag as a string. Not trimmed. Not parsed. */ body: RawMarkup; /** script, style, etc. */ name: string; /** The range covered by the end tag */ blockEndPosition: Position; } /** * The infered kind of raw markup * - `