diff --git a/grammar/liquid-html.ohm b/grammar/liquid-html.ohm index 9a673184..fd92628e 100644 --- a/grammar/liquid-html.ohm +++ b/grammar/liquid-html.ohm @@ -104,12 +104,9 @@ LiquidHTML { // TODO liquidExpression = - | liquidLiteral - - // TODO - liquidLiteral = | liquidString | liquidNumber + | liquidLiteral liquidString = liquidSingleQuotedString | liquidDoubleQuotedString liquidSingleQuotedString = "'" anyExceptStar<("'"| "%}" | "}}")> "'" @@ -122,6 +119,14 @@ LiquidHTML { // coming from shopify/liquid... liquidFloat = "-"? digit (digit | ".")+ + liquidLiteral = + | "true" + | "false" + | "blank" + | "empty" + | "nil" + | "null" + // 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 4b534cc9..a9823128 100644 --- a/src/parser/ast.spec.ts +++ b/src/parser/ast.spec.ts @@ -54,6 +54,28 @@ describe('Unit: toLiquidHtmlAST', () => { expectPosition(ast, 'children.0.markup.expression'); }); }); + + it('should parse numbers as LiquidVariable > LiquidLiteral', () => { + [ + { expression: `nil`, value: null }, + { expression: `null`, value: null }, + { expression: `true`, value: true }, + { expression: `blank`, value: '' }, + { expression: `empty`, value: '' }, + ].forEach(({ expression, value }) => { + 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('LiquidLiteral'); + expectPath(ast, 'children.0.markup.expression.keyword').to.eql(expression); + expectPath(ast, 'children.0.markup.expression.value').to.eql(value); + expectPosition(ast, 'children.0'); + expectPosition(ast, 'children.0.markup'); + expectPosition(ast, 'children.0.markup.expression'); + }); + }); }); it('should transform a basic Liquid Tag into a LiquidTag', () => { diff --git a/src/parser/ast.ts b/src/parser/ast.ts index ea454265..7533215b 100644 --- a/src/parser/ast.ts +++ b/src/parser/ast.ts @@ -15,6 +15,7 @@ import { ConcreteAttrDoubleQuoted, ConcreteAttrUnquoted, ConcreteLiquidVariable, + ConcreteLiquidLiteral, ConcreteLiquidFilters, ConcreteLiquidExpression, } from '~/parser/cst'; @@ -130,7 +131,7 @@ interface LiquidVariable extends ASTNode { } // TODO -type LiquidExpression = LiquidString | LiquidNumber; +type LiquidExpression = LiquidString | LiquidNumber | LiquidLiteral; // TODO type LiquidFilter = undefined; @@ -144,6 +145,11 @@ interface LiquidNumber extends ASTNode { value: string; } +interface LiquidLiteral extends ASTNode { + keyword: ConcreteLiquidLiteral['keyword']; + value: ConcreteLiquidLiteral['value']; +} + export type HtmlNode = | HtmlComment | HtmlElement @@ -647,6 +653,15 @@ function toExpression( source, }; } + case ConcreteNodeTypes.LiquidLiteral: { + return { + type: NodeTypes.LiquidLiteral, + position: position(node), + value: node.value, + keyword: node.keyword, + source, + }; + } default: { return assertNever(node); } diff --git a/src/parser/cst.spec.ts b/src/parser/cst.spec.ts index 510046ab..422e92f7 100644 --- a/src/parser/cst.spec.ts +++ b/src/parser/cst.spec.ts @@ -198,6 +198,29 @@ describe('Unit: toLiquidHtmlCST(text)', () => { expectLocation(cst, '0.markup.expression'); }); }); + + it('should parse Liquid literals', () => { + [ + { expression: `nil`, value: null }, + { expression: `null`, value: null }, + { expression: `true`, value: true }, + { expression: `blank`, value: '' }, + { expression: `empty`, value: '' }, + ].forEach(({ expression, value }) => { + 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('LiquidLiteral'); + expectPath(cst, '0.markup.expression.keyword').to.equal(expression); + expectPath(cst, '0.markup.expression.value').to.equal(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'); + }); + }); }); describe('Case: LiquidNode', () => { diff --git a/src/parser/cst.ts b/src/parser/cst.ts index dda412a0..6c0d2a2e 100644 --- a/src/parser/cst.ts +++ b/src/parser/cst.ts @@ -23,10 +23,20 @@ export enum ConcreteNodeTypes { TextNode = 'TextNode', LiquidVariable = 'LiquidVariable', + LiquidLiteral = 'LiquidLiteral', String = 'String', Number = 'Number', } +export const LiquidLiteralValues = { + nil: null, + null: null, + true: true as true, + false: false as false, + blank: '' as '', + empty: '' as '', +}; + export interface Parsers { [astFormat: string]: Parser; } @@ -149,7 +159,8 @@ export type ConcreteLiquidFilters = undefined; // TODO // TODO export type ConcreteLiquidExpression = | ConcreteStringLiteral - | ConcreteNumberLiteral; + | ConcreteNumberLiteral + | ConcreteLiquidLiteral; export interface ConcreteStringLiteral extends ConcreteBasicNode { @@ -162,6 +173,12 @@ export interface ConcreteNumberLiteral value: string; // float parsing is weird but supported } +export interface ConcreteLiquidLiteral + extends ConcreteBasicNode { + keyword: keyof typeof LiquidLiteralValues; + value: typeof LiquidLiteralValues[keyof typeof LiquidLiteralValues]; +} + export type ConcreteHtmlNode = | ConcreteHtmlComment | ConcreteHtmlRawTag @@ -342,7 +359,17 @@ export function toLiquidHtmlCST(text: string): LiquidHtmlCST { liquidDropCases: 0, liquidExpression: 0, - liquidLiteral: 0, + liquidLiteral: { + type: ConcreteNodeTypes.LiquidLiteral, + value: (tokens: Node[]) => { + const keyword = tokens[0] + .sourceString as keyof typeof LiquidLiteralValues; + return LiquidLiteralValues[keyword]; + }, + keyword: 0, + locStart, + locEnd, + }, liquidDropBaseCase: (sw: Node) => sw.sourceString.trimEnd(), liquidVariable: { type: ConcreteNodeTypes.LiquidVariable, diff --git a/src/printer/preprocess/augment-with-css-properties.ts b/src/printer/preprocess/augment-with-css-properties.ts index be63986e..a61d3b21 100644 --- a/src/printer/preprocess/augment-with-css-properties.ts +++ b/src/printer/preprocess/augment-with-css-properties.ts @@ -81,6 +81,7 @@ function getCssDisplay( return 'block'; case NodeTypes.LiquidVariable: + case NodeTypes.LiquidLiteral: case NodeTypes.String: case NodeTypes.Number: return 'should not be relevant'; @@ -128,6 +129,7 @@ function getNodeCssStyleWhiteSpace(node: AugmentedNode): string { return CSS_WHITE_SPACE_DEFAULT; case NodeTypes.LiquidVariable: + case NodeTypes.LiquidLiteral: case NodeTypes.String: case NodeTypes.Number: return 'should not be relevant'; diff --git a/src/printer/printer-liquid-html.ts b/src/printer/printer-liquid-html.ts index 68290f62..47821cc2 100644 --- a/src/printer/printer-liquid-html.ts +++ b/src/printer/printer-liquid-html.ts @@ -390,6 +390,14 @@ function printNode( return node.value; } + case NodeTypes.LiquidLiteral: { + // We prefer nil over null. + if (node.keyword === 'null') { + return 'nil'; + } + return node.keyword; + } + case NodeTypes.LiquidVariable: { // TODO this is where you'll do the pipe first/last logic. return [path.call(print, 'expression')]; diff --git a/src/types.ts b/src/types.ts index 304b6991..f72b9581 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,7 @@ export enum NodeTypes { TextNode = 'TextNode', LiquidVariable = 'LiquidVariable', + LiquidLiteral = 'LiquidLiteral', String = 'String', Number = 'Number', // Range = 'Range', diff --git a/test/liquid-drop-liquid-literal/fixed.liquid b/test/liquid-drop-liquid-literal/fixed.liquid new file mode 100644 index 00000000..b6d48752 --- /dev/null +++ b/test/liquid-drop-liquid-literal/fixed.liquid @@ -0,0 +1,9 @@ +It should print literals +{{ true }} +{{ false }} +{{ empty }} +{{ blank }} + +It should prefer nil over null +{{ nil }} +{{ nil }} diff --git a/test/liquid-drop-liquid-literal/index.liquid b/test/liquid-drop-liquid-literal/index.liquid new file mode 100644 index 00000000..ce9a5d36 --- /dev/null +++ b/test/liquid-drop-liquid-literal/index.liquid @@ -0,0 +1,9 @@ +It should print literals +{{ true }} +{{ false }} +{{ empty }} +{{ blank }} + +It should prefer nil over null +{{ nil }} +{{ null }} diff --git a/test/liquid-drop-liquid-literal/index.spec.ts b/test/liquid-drop-liquid-literal/index.spec.ts new file mode 100644 index 00000000..4587999a --- /dev/null +++ b/test/liquid-drop-liquid-literal/index.spec.ts @@ -0,0 +1,6 @@ +import { assertFormattedEqualsFixed } from '../test-helpers'; +import * as path from 'path'; + +describe(`Unit: ${path.basename(__dirname)}`, () => { + assertFormattedEqualsFixed(__dirname); +});