Skip to content
This repository has been archived by the owner on Jun 20, 2024. It is now read-only.

Commit

Permalink
Add support for if and unless tags
Browse files Browse the repository at this point in the history
Decisions:
- We're going logical operators first
- We're maybe breaking on comparator (but grouped)
- Same syntax as others `{% if $condition1`, ..., `%}`

Partial fix of #62
  • Loading branch information
charlespwd committed Aug 15, 2022
1 parent 0af1bdf commit 04e0235
Show file tree
Hide file tree
Showing 12 changed files with 434 additions and 4 deletions.
18 changes: 18 additions & 0 deletions grammar/liquid-html.ohm
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ LiquidHTML {

liquidTagOpen =
| liquidTagOpenForm
| liquidTagOpenIf
| liquidTagOpenUnless
| liquidTagOpenPaginate
| liquidTagOpenBaseCase
liquidTagClose = "{%" "-"? space* "end" blockName space* tagMarkup "-"? "%}"
Expand Down Expand Up @@ -118,6 +120,22 @@ LiquidHTML {
liquidTagOpenForm = liquidTagOpenRule<"form", liquidTagOpenFormMarkup>
liquidTagOpenFormMarkup = arguments space*

liquidTagOpenIf = liquidTagOpenRule<"if", liquidTagOpenConditionalMarkup>
liquidTagOpenUnless = liquidTagOpenRule<"unless", liquidTagOpenConditionalMarkup>
liquidTagOpenConditionalMarkup = nonemptyListOf<condition, conditionSeparator> space*
conditionSeparator = &logicalOperator
condition = logicalOperator? space* (comparison | liquidExpression) space*
logicalOperator = "and" | "or"
comparison = liquidExpression space* comparator space* liquidExpression
comparator =
( "=="
| "!="
| ">"
| "<"
| ">="
| "<=")
| ("contains" ~identifier)

liquidTagOpenPaginate = liquidTagOpenRule<"paginate", liquidTagOpenPaginateMarkup>
liquidTagOpenPaginateMarkup =
liquidExpression space+ "by" space+ liquidExpression (argumentSeparatorOptionalComma tagArguments)? space*
Expand Down
79 changes: 79 additions & 0 deletions src/parser/ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,85 @@ describe('Unit: toLiquidHtmlAST', () => {
expectPosition(ast, 'children.0.markup');
});
});

it('should parse conditional tags into conditional expressions', () => {
['if', 'unless'].forEach((tagName) => {
[
{
expression: 'a',
markup: {
type: 'VariableLookup',
},
},
{
expression: 'a and "string"',
markup: {
type: 'LogicalExpression',
relation: 'and',
left: { type: 'VariableLookup' },
right: { type: 'String' },
},
},
{
expression: 'a and "string" or a<1',
markup: {
type: 'LogicalExpression',
relation: 'and',
left: { type: 'VariableLookup' },
right: {
type: 'LogicalExpression',
relation: 'or',
left: { type: 'String' },
right: {
type: 'Comparison',
comparator: '<',
left: { type: 'VariableLookup' },
right: { type: 'Number' },
},
},
},
},
].forEach(({ expression, markup }) => {
ast = toLiquidHtmlAST(`{% ${tagName} ${expression} -%}`);
expectPath(ast, 'children.0.type').to.equal('LiquidTag');
expectPath(ast, 'children.0.name').to.equal(tagName);
let cursor: any = markup;
let prefix = '';
while (cursor) {
switch (cursor.type) {
case 'LogicalExpression': {
expectPath(ast, `children.0.markup${prefix}.type`).to.equal(cursor.type);
expectPath(ast, `children.0.markup${prefix}.relation`).to.equal(cursor.relation);
expectPath(ast, `children.0.markup${prefix}.left.type`).to.equal(cursor.left.type);
cursor = cursor.right;
prefix = prefix + '.right';
break;
}
case 'Comparison': {
expectPath(ast, `children.0.markup${prefix}.type`).to.equal(cursor.type);
expectPath(ast, `children.0.markup${prefix}.comparator`).to.equal(
cursor.comparator,
);
expectPath(ast, `children.0.markup${prefix}.left.type`).to.equal(cursor.left.type);
expectPath(ast, `children.0.markup${prefix}.right.type`).to.equal(
cursor.right.type,
);
cursor = cursor.right;
prefix = prefix + '.right';
break;
}
default: {
expectPath(ast, `children.0.markup${prefix}.type`).to.equal(cursor.type);
cursor = null;
break;
}
}
}

expectPosition(ast, 'children.0');
});
});
});
});

it('should parse a basic text node into a TextNode', () => {
Expand Down
100 changes: 98 additions & 2 deletions src/parser/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ import {
ConcreteLiquidTagOpen,
ConcreteLiquidArgument,
ConcretePaginateMarkup,
ConcreteLiquidCondition,
ConcreteLiquidComparison,
} from '~/parser/cst';
import { isLiquidHtmlNode, NamedTags, NodeTypes, Position } from '~/types';
import {
Comparators,
isLiquidHtmlNode,
NamedTags,
NodeTypes,
Position,
} from '~/types';
import { assertNever, deepGet, dropLast } from '~/utils';
import { LiquidHTMLASTParsingError } from '~/parser/errors';
import { TAGS_WITHOUT_MARKUP } from '~/parser/grammar';
Expand All @@ -53,6 +61,8 @@ export type LiquidHtmlNode =
| RenderMarkup
| PaginateMarkup
| RenderVariableExpression
| LiquidLogicalExpression
| LiquidComparison
| TextNode;

export interface DocumentNode extends ASTNode<NodeTypes.Document> {
Expand Down Expand Up @@ -107,10 +117,12 @@ export type LiquidTagNamed =
| LiquidTagAssign
| LiquidTagEcho
| LiquidTagForm
| LiquidTagIf
| LiquidTagInclude
| LiquidTagPaginate
| LiquidTagRender
| LiquidTagSection;
| LiquidTagSection
| LiquidTagUnless;

export interface LiquidTagNode<Name, Markup>
extends ASTNode<NodeTypes.LiquidTag> {
Expand Down Expand Up @@ -146,6 +158,30 @@ export interface AssignMarkup extends ASTNode<NodeTypes.AssignMarkup> {
export interface LiquidTagForm
extends LiquidTagNode<NamedTags.form, LiquidArgument[]> {}

export interface LiquidTagIf extends LiquidTagConditional<NamedTags.if> {}
export interface LiquidTagUnless
extends LiquidTagConditional<NamedTags.unless> {}
export interface LiquidTagConditional<Name>
extends LiquidTagNode<Name, LiquidConditionalExpression> {}

export type LiquidConditionalExpression =
| LiquidLogicalExpression
| LiquidComparison
| LiquidExpression;

export interface LiquidLogicalExpression
extends ASTNode<NodeTypes.LogicalExpression> {
relation: 'and' | 'or';
left: LiquidConditionalExpression;
right: LiquidConditionalExpression;
}

export interface LiquidComparison extends ASTNode<NodeTypes.Comparison> {
comparator: Comparators;
left: LiquidConditionalExpression;
right: LiquidConditionalExpression;
}

export interface LiquidTagPaginate
extends LiquidTagNode<NamedTags.paginate, PaginateMarkup> {}
export interface PaginateMarkup extends ASTNode<NodeTypes.PaginateMarkup> {
Expand Down Expand Up @@ -786,6 +822,16 @@ function toNamedLiquidTag(
};
}

case NamedTags.if:
case NamedTags.unless: {
return {
name: node.name,
markup: toConditionalExpression(node.markup, source),
children: [],
...liquidTagBaseAttributes(node, source),
};
}

default: {
return assertNever(node);
}
Expand Down Expand Up @@ -850,6 +896,56 @@ function toRenderVariableExpression(
};
}

function toConditionalExpression(
nodes: ConcreteLiquidCondition[],
source: string,
): LiquidConditionalExpression {
if (nodes.length === 1) {
return toComparisonOrExpression(nodes[0], source);
}

const [first, second] = nodes;
const [, ...rest] = nodes;
return {
type: NodeTypes.LogicalExpression,
relation: second.relation as 'and' | 'or',
left: toComparisonOrExpression(first, source),
right: toConditionalExpression(rest, source),
position: {
start: first.locStart,
end: nodes[nodes.length - 1].locEnd,
},
source,
};
}

function toComparisonOrExpression(
node: ConcreteLiquidCondition,
source: string,
): LiquidComparison | LiquidExpression {
const expression = node.expression;
switch (expression.type) {
case ConcreteNodeTypes.Comparison:
return toComparison(expression, source);
default:
return toExpression(expression, source);
}
}

function toComparison(
node: ConcreteLiquidComparison,
source: string,
): LiquidComparison {
return {
type: NodeTypes.Comparison,
comparator: node.comparator,
left: toExpression(node.left, source),
right: toExpression(node.right, source),
position: position(node),
source,
};
}

function toLiquidDrop(node: ConcreteLiquidDrop, source: string): LiquidDrop {
return {
type: NodeTypes.LiquidDrop,
Expand Down
56 changes: 55 additions & 1 deletion src/parser/cst.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,61 @@ describe('Unit: toLiquidHtmlCST(text)', () => {
expectLocation(cst, '0.markup');
});
});

it('should parse the if and unless tag arguments as a list of conditions', () => {
['if', 'unless'].forEach((tagName) => {
[
{
expression: 'a',
conditions: [{ relation: null, conditional: { type: 'VariableLookup' } }],
},
{
expression: 'a and "string"',
conditions: [
{ relation: null, conditional: { type: 'VariableLookup' } },
{ relation: 'and', conditional: { type: 'String' } },
],
},
{
expression: 'a and "string" or a<1',
conditions: [
{ relation: null, conditional: { type: 'VariableLookup' } },
{ relation: 'and', conditional: { type: 'String' } },
{
relation: 'or',
conditional: {
type: 'Comparison',
comparator: '<',
left: { type: 'VariableLookup' },
right: { type: 'Number' },
},
},
],
},
].forEach(({ expression, conditions }) => {
cst = toLiquidHtmlCST(`{% ${tagName} ${expression} -%}`);
expectPath(cst, '0.type').to.equal('LiquidTagOpen');
expectPath(cst, '0.name').to.equal(tagName);
expectPath(cst, '0.markup').to.have.lengthOf(conditions.length);
conditions.forEach(({ relation, conditional }, i) => {
expectPath(cst, `0.markup.${i}.type`).to.equal('Condition');
expectPath(cst, `0.markup.${i}.relation`).to.equal(relation);
expectPath(cst, `0.markup.${i}.expression.type`).to.equal(conditional.type);
if (conditional.type === 'Comparison') {
expectPath(cst, `0.markup.${i}.expression.comparator`).to.equal(
conditional.comparator,
);
expectPath(cst, `0.markup.${i}.expression.left.type`).to.equal(conditional.left.type);
expectPath(cst, `0.markup.${i}.expression.right.type`).to.equal(
conditional.right.type,
);
}
expectLocation(cst, `0.markup.${i}`);
});
expectLocation(cst, '0');
});
});
});
});

describe('Case: LiquidNode', () => {
Expand Down Expand Up @@ -660,7 +715,6 @@ describe('Unit: toLiquidHtmlCST(text)', () => {
expectPath(cst, '0.whitespaceEnd').to.equal(null);
expectPath(cst, '1.type').to.equal('LiquidTagOpen');
expectPath(cst, '1.name').to.equal('if');
expectPath(cst, '1.markup').to.equal('hi');
expectPath(cst, '1.whitespaceStart').to.equal(null);
expectPath(cst, '1.whitespaceEnd').to.equal('-');
expectPath(cst, '2.type').to.equal('LiquidTagClose');
Expand Down
Loading

0 comments on commit 04e0235

Please sign in to comment.