Skip to content

Commit

Permalink
Fixed when tag expression parsing.
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Dec 4, 2022
1 parent 428848b commit 9f673f2
Show file tree
Hide file tree
Showing 11 changed files with 874 additions and 22 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Version 1.5.0 (unreleased)

**Fixes**

- Fixed `case`/`when` tag expression parsing. `when` expressions no longer fail when presented with a string containing a comma. Handling of comma and `or` separated "sub-expressions" is now consistent with the reference implementation.

**Compatibility**

- `for` tag arguments can now be separated by commas as well as whitespace. See [Shopify/liquid#1658](https://github.com/Shopify/liquid/pull/1658).
Expand Down
76 changes: 60 additions & 16 deletions src/builtin/tags/case.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { BlockNode, forcedOutput, Node, ChildNode } from "../../ast";
import { RenderContext } from "../../context";
import { Environment } from "../../environment";
import { BooleanExpression, Literal } from "../../expression";
import { parse } from "../../expressions/boolean/parse";
import {
BooleanExpression,
Expression,
InfixExpression,
Literal,
} from "../../expression";
import { RenderStream } from "../../io/output_stream";
import { Tag } from "../../tag";
import {
Expand All @@ -13,6 +17,14 @@ import {
TOKEN_TAG,
} from "../../token";

import {
TOKEN_COMMA,
TOKEN_OR,
ExpressionTokenStream,
} from "../../expressions";

import { tokenize, parse } from "../../expressions/standard";

const TAG_CASE = "case";
const TAG_ENDCASE = "endcase";
const TAG_WHEN = "when";
Expand All @@ -31,27 +43,26 @@ export class CaseTag implements Tag {
TAG_ELSE,
TOKEN_EOF,
]);

protected static END_CASE_BLOCK = new Set([TAG_ENDCASE]);
protected static DELIM_TOKENS = new Set([TOKEN_COMMA, TOKEN_OR]);

readonly block = true;
readonly name: string = TAG_CASE;
readonly end: string = TAG_ENDCASE;
protected nodeClass = CaseNode;

protected parseExpression(
_when: string,
obj: string,
stream: TokenStream
): BooleanExpression {
stream.expect(TOKEN_EXPRESSION);
return parse(`${_when} == ${obj}`);
}

public parse(stream: TokenStream, environment: Environment): Node {
const parser = environment.parser;
const token = stream.next();

// Parse the case expression
stream.expect(TOKEN_EXPRESSION);
const _case = stream.next().value;
const _case = this.parse_case_expression(
stream.current.value,
stream.current.index
);
stream.next();

// Eat whitespace or junk between `case` and when/else/endcase
while (
Expand All @@ -66,10 +77,14 @@ export class CaseTag implements Tag {
stream.current.value === TAG_WHEN
) {
const whenToken = stream.next();
// One conditional block for every object in a comma separated list.
const whenExprs = stream.current.value
.split(",")
.map((expr) => this.parseExpression(_case, expr, stream));
stream.expect(TOKEN_EXPRESSION);

const whenExprs = this.parse_when_expression(
stream.current.value,
stream.current.index
).map(
(expr) => new BooleanExpression(new InfixExpression(_case, "==", expr))
);

const whenBlock = parser.parseBlock(
stream,
Expand All @@ -96,8 +111,37 @@ export class CaseTag implements Tag {
parser.parseBlock(stream, CaseTag.END_CASE_BLOCK, stream.next())
);
}

stream.expectTag(TAG_ENDCASE);
return new this.nodeClass(token, whens);
}

protected parse_case_expression(
expr: string,
startIndex: number
): Expression {
return parse(new ExpressionTokenStream(tokenize(expr, startIndex)));
}

protected parse_when_expression(
expr: string,
startIndex: number
): Expression[] {
const expressions: Expression[] = [];
const stream = new ExpressionTokenStream(tokenize(expr, startIndex));

for (;;) {
expressions.push(parse(stream));
stream.next();
if (CaseTag.DELIM_TOKENS.has(stream.current.kind)) {
stream.next();
} else {
break;
}
}

return expressions;
}
}

export class CaseNode implements Node {
Expand Down
10 changes: 5 additions & 5 deletions src/expressions/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const enum MatchGroup {
IDENT_QUOTED = "identQuoted",
}

export type Tokenizer = (
expression: string,
startIndex?: number
) => Generator<Token>;

// Optional trailing question mark.
export const IDENTIFIER_PATTERN = "[a-zA-Z_][\\w\\-]*\\??";

Expand Down Expand Up @@ -232,8 +237,3 @@ export function makeParseRange(
}
return _parseRangeLiteral;
}

export type Tokenizer = (
expression: string,
startIndex?: number
) => Generator<Token>;
1 change: 1 addition & 0 deletions src/expressions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * as include from "./include";
export * as loop from "./loop";
export * as boolean_not from "./boolean_not";
export * as arguments from "./arguments";
export * as standard from "./standard";
2 changes: 2 additions & 0 deletions src/expressions/standard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./lex";
export * from "./parse";
Loading

0 comments on commit 9f673f2

Please sign in to comment.