From afc4caa7a5040885f71a7a3fe087fe81cbdc7d5f Mon Sep 17 00:00:00 2001 From: "Charles-P. Clermont" Date: Wed, 3 Aug 2022 09:08:28 -0400 Subject: [PATCH] Add support for Liquid ranges ```liquid {{ (0..5) }} {{ (true..false) }} etc... ``` --- grammar/liquid-html.ohm | 4 ++ src/parser/ast.spec.ts | 36 ++++++++++++++++++ src/parser/ast.ts | 20 +++++++++- src/parser/cst.spec.ts | 37 +++++++++++++++++++ src/parser/cst.ts | 18 ++++++++- .../preprocess/augment-with-css-properties.ts | 2 + src/printer/printer-liquid-html.ts | 16 +++++++- src/types.ts | 3 +- test/liquid-drop-range/fixed.liquid | 8 ++++ test/liquid-drop-range/index.liquid | 8 ++++ test/liquid-drop-range/index.spec.ts | 6 +++ 11 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 test/liquid-drop-range/fixed.liquid create mode 100644 test/liquid-drop-range/index.liquid create mode 100644 test/liquid-drop-range/index.spec.ts 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); +});