/**
* 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
* - `