diff --git a/grammar/liquid-html.ohm b/grammar/liquid-html.ohm
index 233e19a7..a6cd6dda 100644
--- a/grammar/liquid-html.ohm
+++ b/grammar/liquid-html.ohm
@@ -107,6 +107,7 @@ LiquidHTML {
| liquidString
| liquidNumber
| liquidLiteral
+ | liquidRange
liquidString = liquidSingleQuotedString | liquidDoubleQuotedString
liquidSingleQuotedString = "'" anyExceptStar<("'"| "%}" | "}}")> "'"
@@ -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 =
diff --git a/src/parser/ast.spec.ts b/src/parser/ast.spec.ts
index a9823128..aac1c472 100644
--- a/src/parser/ast.spec.ts
+++ b/src/parser/ast.spec.ts
@@ -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', () => {
diff --git a/src/parser/ast.ts b/src/parser/ast.ts
index 7533215b..55ed48f5 100644
--- a/src/parser/ast.ts
+++ b/src/parser/ast.ts
@@ -131,7 +131,11 @@ interface LiquidVariable extends ASTNode {
}
// TODO
-type LiquidExpression = LiquidString | LiquidNumber | LiquidLiteral;
+type LiquidExpression =
+ | LiquidString
+ | LiquidNumber
+ | LiquidLiteral
+ | LiquidRange;
// TODO
type LiquidFilter = undefined;
@@ -145,6 +149,11 @@ interface LiquidNumber extends ASTNode {
value: string;
}
+interface LiquidRange extends ASTNode {
+ start: LiquidExpression;
+ end: LiquidExpression;
+}
+
interface LiquidLiteral extends ASTNode {
keyword: ConcreteLiquidLiteral['keyword'];
value: ConcreteLiquidLiteral['value'];
@@ -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);
}
diff --git a/src/parser/cst.spec.ts b/src/parser/cst.spec.ts
index 422e92f7..df2ea973 100644
--- a/src/parser/cst.spec.ts
+++ b/src/parser/cst.spec.ts
@@ -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', () => {
diff --git a/src/parser/cst.ts b/src/parser/cst.ts
index 6c0d2a2e..dca00525 100644
--- a/src/parser/cst.ts
+++ b/src/parser/cst.ts
@@ -26,6 +26,7 @@ export enum ConcreteNodeTypes {
LiquidLiteral = 'LiquidLiteral',
String = 'String',
Number = 'Number',
+ Range = 'Range',
}
export const LiquidLiteralValues = {
@@ -160,7 +161,8 @@ export type ConcreteLiquidFilters = undefined; // TODO
export type ConcreteLiquidExpression =
| ConcreteStringLiteral
| ConcreteNumberLiteral
- | ConcreteLiquidLiteral;
+ | ConcreteLiquidLiteral
+ | ConcreteLiquidRange;
export interface ConcreteStringLiteral
extends ConcreteBasicNode {
@@ -179,6 +181,12 @@ export interface ConcreteLiquidLiteral
value: typeof LiquidLiteralValues[keyof typeof LiquidLiteralValues];
}
+export interface ConcreteLiquidRange
+ extends ConcreteBasicNode {
+ start: ConcreteLiquidExpression;
+ end: ConcreteLiquidExpression;
+}
+
export type ConcreteHtmlNode =
| ConcreteHtmlComment
| ConcreteHtmlRawTag
@@ -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,
diff --git a/src/printer/preprocess/augment-with-css-properties.ts b/src/printer/preprocess/augment-with-css-properties.ts
index a61d3b21..69389027 100644
--- a/src/printer/preprocess/augment-with-css-properties.ts
+++ b/src/printer/preprocess/augment-with-css-properties.ts
@@ -84,6 +84,7 @@ function getCssDisplay(
case NodeTypes.LiquidLiteral:
case NodeTypes.String:
case NodeTypes.Number:
+ case NodeTypes.Range:
return 'should not be relevant';
default:
@@ -132,6 +133,7 @@ function getNodeCssStyleWhiteSpace(node: AugmentedNode): string {
case NodeTypes.LiquidLiteral:
case NodeTypes.String:
case NodeTypes.Number:
+ case NodeTypes.Range:
return 'should not be relevant';
default:
diff --git a/src/printer/printer-liquid-html.ts b/src/printer/printer-liquid-html.ts
index 47821cc2..044e4900 100644
--- a/src/printer/printer-liquid-html.ts
+++ b/src/printer/printer-liquid-html.ts
@@ -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: {
diff --git a/src/types.ts b/src/types.ts
index f72b9581..85ae95df 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -27,7 +27,7 @@ export enum NodeTypes {
LiquidLiteral = 'LiquidLiteral',
String = 'String',
Number = 'Number',
- // Range = 'Range',
+ Range = 'Range',
// VariableLookup = 'VariableLookup',
}
@@ -54,6 +54,7 @@ export type LiquidParserOptions = ParserOptions & {
export type LiquidPrinterArgs = {
leadingSpaceGroupId?: symbol[] | symbol;
trailingSpaceGroupId?: symbol[] | symbol;
+ truncate?: boolean;
};
export type LiquidPrinter = (
path: AstPath,
diff --git a/test/liquid-drop-range/fixed.liquid b/test/liquid-drop-range/fixed.liquid
new file mode 100644
index 00000000..8d2bb6ec
--- /dev/null
+++ b/test/liquid-drop-range/fixed.liquid
@@ -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) }}
diff --git a/test/liquid-drop-range/index.liquid b/test/liquid-drop-range/index.liquid
new file mode 100644
index 00000000..29e416f0
--- /dev/null
+++ b/test/liquid-drop-range/index.liquid
@@ -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 ) }}
diff --git a/test/liquid-drop-range/index.spec.ts b/test/liquid-drop-range/index.spec.ts
new file mode 100644
index 00000000..4587999a
--- /dev/null
+++ b/test/liquid-drop-range/index.spec.ts
@@ -0,0 +1,6 @@
+import { assertFormattedEqualsFixed } from '../test-helpers';
+import * as path from 'path';
+
+describe(`Unit: ${path.basename(__dirname)}`, () => {
+ assertFormattedEqualsFixed(__dirname);
+});