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 Liquid ranges
Browse files Browse the repository at this point in the history
```liquid
{{ (0..5) }}
{{ (true..false) }}
etc...
```
  • Loading branch information
charlespwd committed Aug 3, 2022
1 parent 420a97e commit afc4caa
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 4 deletions.
4 changes: 4 additions & 0 deletions grammar/liquid-html.ohm
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ LiquidHTML {
| liquidString
| liquidNumber
| liquidLiteral
| liquidRange

liquidString = liquidSingleQuotedString | liquidDoubleQuotedString
liquidSingleQuotedString = "'" anyExceptStar<("'"| "%}" | "}}")> "'"
Expand All @@ -124,6 +125,9 @@ LiquidHTML {
| "nil"
| "null"

liquidRange =
"(" space* liquidExpression space* ".." space* liquidExpression space* ")"

// https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#void-element
// Cheating a bit with by stretching it to the doctype
voidElementName =
Expand Down
36 changes: 36 additions & 0 deletions src/parser/ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,42 @@ describe('Unit: toLiquidHtmlAST', () => {
expectPosition(ast, 'children.0.markup.expression');
});
});

it('should parse ranges as LiquidVariable > Range', () => {
[
{
expression: `(0..5)`,
start: { value: '0', type: 'Number' },
end: { value: '5', type: 'Number' },
},
{
expression: `( 0 .. 5 )`,
start: { value: '0', type: 'Number' },
end: { value: '5', type: 'Number' },
},
{
expression: `(true..false)`,
start: { value: true, type: 'LiquidLiteral' },
end: { value: false, type: 'LiquidLiteral' },
},
].forEach(({ expression, start, end }) => {
ast = toLiquidHtmlAST(`{{ ${expression} }}`);
expectPath(ast, 'children.0').to.exist;
expectPath(ast, 'children.0.type').to.eql('LiquidDrop');
expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable');
expectPath(ast, 'children.0.markup.rawSource').to.eql(expression);
expectPath(ast, 'children.0.markup.expression.type').to.eql('Range');
expectPath(ast, 'children.0.markup.expression.start.type').to.eql(start.type);
expectPath(ast, 'children.0.markup.expression.start.value').to.eql(start.value);
expectPath(ast, 'children.0.markup.expression.end.type').to.eql(end.type);
expectPath(ast, 'children.0.markup.expression.end.value').to.eql(end.value);
expectPosition(ast, 'children.0');
expectPosition(ast, 'children.0.markup');
expectPosition(ast, 'children.0.markup.expression');
expectPosition(ast, 'children.0.markup.expression.start');
expectPosition(ast, 'children.0.markup.expression.end');
});
});
});

it('should transform a basic Liquid Tag into a LiquidTag', () => {
Expand Down
20 changes: 19 additions & 1 deletion src/parser/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ interface LiquidVariable extends ASTNode<NodeTypes.LiquidVariable> {
}

// TODO
type LiquidExpression = LiquidString | LiquidNumber | LiquidLiteral;
type LiquidExpression =
| LiquidString
| LiquidNumber
| LiquidLiteral
| LiquidRange;

// TODO
type LiquidFilter = undefined;
Expand All @@ -145,6 +149,11 @@ interface LiquidNumber extends ASTNode<NodeTypes.Number> {
value: string;
}

interface LiquidRange extends ASTNode<NodeTypes.Range> {
start: LiquidExpression;
end: LiquidExpression;
}

interface LiquidLiteral extends ASTNode<NodeTypes.LiquidLiteral> {
keyword: ConcreteLiquidLiteral['keyword'];
value: ConcreteLiquidLiteral['value'];
Expand Down Expand Up @@ -662,6 +671,15 @@ function toExpression(
source,
};
}
case ConcreteNodeTypes.Range: {
return {
type: NodeTypes.Range,
start: toExpression(node.start, source),
end: toExpression(node.end, source),
position: position(node),
source,
};
}
default: {
return assertNever(node);
}
Expand Down
37 changes: 37 additions & 0 deletions src/parser/cst.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,43 @@ describe('Unit: toLiquidHtmlCST(text)', () => {
expectLocation(cst, '0.markup.expression');
});
});

it('should parse ranges', () => {
[
{
expression: `(0..5)`,
start: { value: '0', type: 'Number' },
end: { value: '5', type: 'Number' },
},
{
expression: `( 0 .. 5 )`,
start: { value: '0', type: 'Number' },
end: { value: '5', type: 'Number' },
},
{
expression: `(true..false)`,
start: { value: true, type: 'LiquidLiteral' },
end: { value: false, type: 'LiquidLiteral' },
},
].forEach(({ expression, start, end }) => {
cst = toLiquidHtmlCST(`{{ ${expression} }}`);
expectPath(cst, '0.type').to.equal('LiquidDrop');
expectPath(cst, '0.markup.type').to.equal('LiquidVariable', expression);
expectPath(cst, '0.markup.rawSource').to.equal(expression);
expectPath(cst, '0.markup.expression.type').to.equal('Range');
expectPath(cst, '0.markup.expression.start.type').to.equal(start.type);
expectPath(cst, '0.markup.expression.start.value').to.equal(start.value);
expectPath(cst, '0.markup.expression.end.type').to.equal(end.type);
expectPath(cst, '0.markup.expression.end.value').to.equal(end.value);
expectPath(cst, '0.whitespaceStart').to.equal(null);
expectPath(cst, '0.whitespaceEnd').to.equal(null);
expectLocation(cst, '0');
expectLocation(cst, '0.markup');
expectLocation(cst, '0.markup.expression');
expectLocation(cst, '0.markup.expression.start');
expectLocation(cst, '0.markup.expression.end');
});
});
});

describe('Case: LiquidNode', () => {
Expand Down
18 changes: 17 additions & 1 deletion src/parser/cst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum ConcreteNodeTypes {
LiquidLiteral = 'LiquidLiteral',
String = 'String',
Number = 'Number',
Range = 'Range',
}

export const LiquidLiteralValues = {
Expand Down Expand Up @@ -160,7 +161,8 @@ export type ConcreteLiquidFilters = undefined; // TODO
export type ConcreteLiquidExpression =
| ConcreteStringLiteral
| ConcreteNumberLiteral
| ConcreteLiquidLiteral;
| ConcreteLiquidLiteral
| ConcreteLiquidRange;

export interface ConcreteStringLiteral
extends ConcreteBasicNode<ConcreteNodeTypes.String> {
Expand All @@ -179,6 +181,12 @@ export interface ConcreteLiquidLiteral
value: typeof LiquidLiteralValues[keyof typeof LiquidLiteralValues];
}

export interface ConcreteLiquidRange
extends ConcreteBasicNode<ConcreteNodeTypes.Range> {
start: ConcreteLiquidExpression;
end: ConcreteLiquidExpression;
}

export type ConcreteHtmlNode =
| ConcreteHtmlComment
| ConcreteHtmlRawTag
Expand Down Expand Up @@ -407,6 +415,14 @@ export function toLiquidHtmlCST(text: string): LiquidHtmlCST {
locEnd,
},

liquidRange: {
type: ConcreteNodeTypes.Range,
start: 2,
end: 6,
locStart,
locEnd,
},

liquidInlineComment: {
type: ConcreteNodeTypes.LiquidTag,
name: 3,
Expand Down
2 changes: 2 additions & 0 deletions src/printer/preprocess/augment-with-css-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ function getCssDisplay(
case NodeTypes.LiquidLiteral:
case NodeTypes.String:
case NodeTypes.Number:
case NodeTypes.Range:
return 'should not be relevant';

default:
Expand Down Expand Up @@ -132,6 +133,7 @@ function getNodeCssStyleWhiteSpace(node: AugmentedNode<WithSiblings>): string {
case NodeTypes.LiquidLiteral:
case NodeTypes.String:
case NodeTypes.Number:
case NodeTypes.Range:
return 'should not be relevant';

default:
Expand Down
16 changes: 15 additions & 1 deletion src/printer/printer-liquid-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,21 @@ function printNode(
}

case NodeTypes.Number: {
return node.value;
if (args.truncate) {
return node.value.replace(/\.\d+$/, '');
} else {
return node.value;
}
}

case NodeTypes.Range: {
return [
'(',
path.call((p) => print(p, { truncate: true }), 'start'),
'..',
path.call((p) => print(p, { truncate: true }), 'end'),
')',
];
}

case NodeTypes.LiquidLiteral: {
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export enum NodeTypes {
LiquidLiteral = 'LiquidLiteral',
String = 'String',
Number = 'Number',
// Range = 'Range',
Range = 'Range',
// VariableLookup = 'VariableLookup',
}

Expand All @@ -54,6 +54,7 @@ export type LiquidParserOptions = ParserOptions<LiquidHtmlNode> & {
export type LiquidPrinterArgs = {
leadingSpaceGroupId?: symbol[] | symbol;
trailingSpaceGroupId?: symbol[] | symbol;
truncate?: boolean;
};
export type LiquidPrinter = (
path: AstPath<LiquidHtmlNode>,
Expand Down
8 changes: 8 additions & 0 deletions test/liquid-drop-range/fixed.liquid
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
It should strip whitespace
{{ (0..1) }}

It should truncate floats to ints the same way .to_i would
{{ (0..2) }}

It should not do anything with weird types
{{ (true..false) }}
8 changes: 8 additions & 0 deletions test/liquid-drop-range/index.liquid
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
It should strip whitespace
{{ ( 0 .. 1 ) }}

It should convert floats to ints the same way .to_i would
{{ (0.5..2.5) }}

It should not do anything with weird types (but still still whitespace)
{{ ( true .. false ) }}
6 changes: 6 additions & 0 deletions test/liquid-drop-range/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { assertFormattedEqualsFixed } from '../test-helpers';
import * as path from 'path';

describe(`Unit: ${path.basename(__dirname)}`, () => {
assertFormattedEqualsFixed(__dirname);
});

0 comments on commit afc4caa

Please sign in to comment.