Skip to content
This repository has been archived by the owner on Jun 20, 2024. It is now read-only.

Commit

Permalink
Add parser and printer support for Liquid filters
Browse files Browse the repository at this point in the history
Fixes #45
  • Loading branch information
charlespwd committed Aug 4, 2022
1 parent 6bdfef0 commit 4ae0ccc
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 41 deletions.
17 changes: 13 additions & 4 deletions grammar/liquid-html.ohm
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ LiquidHTML {
// because we'd otherwise positively match the following string
// instead of falling back to the other rule:
// {{ 'string' | some_filter }}
liquidVariable = liquidExpression space* &("-}}" | "}}") // liquidFilter*
liquidVariable = liquidExpression liquidFilter* space* &("-}}" | "}}")

liquidExpression =
| liquidString
Expand Down Expand Up @@ -129,12 +129,21 @@ LiquidHTML {
"(" space* liquidExpression space* ".." space* liquidExpression space* ")"

liquidVariableLookup = variableSegment? lookup*
variableSegment = letter (alnum | "_" | "-")*
lookup =
| dotLookup
| indexLookup
dotLookup = space* "." space* variableSegment "?"?
| dotLookup
indexLookup = space* "[" space* liquidExpression space* "]"
dotLookup = space* "." space* identifier

liquidFilter = space* "|" space* identifier (space* ":" space* filterArguments)?
filterSeparator = space* "," space*
filterArguments = listOf<filterArgument, filterSeparator>
filterArgument = namedArgument | positionalArgument
positionalArgument = liquidExpression
namedArgument = variableSegment space* ":" space* liquidExpression

variableSegment = (letter | "_") (alnum | "_" | "-")*
identifier = variableSegment "?"?

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

it('should parse filters', () => {
interface Filter {
name: string;
args: FilterArgument[];
}
type FilterArgument = any;

const filter = (name: string, args: FilterArgument[] = []): Filter => ({ name, args });
const arg = (type: string, value: string) => ({ type, value });
const namedArg = (name: string, valueType: string) => ({
type: 'NamedArgument',
name,
valueType,
});
[
{ expression: `| filter1`, filters: [filter('filter1')] },
{ expression: `| filter1 | filter2`, filters: [filter('filter1'), filter('filter2')] },
{
expression: `| filter1: 'hi', 'there'`,
filters: [filter('filter1', [arg('String', 'hi'), arg('String', 'there')])],
},
{
expression: `| filter1: key: value, kind: 'string'`,
filters: [
filter('filter1', [namedArg('key', 'VariableLookup'), namedArg('kind', 'String')]),
],
},
{
expression: `| f1: 'hi', key: (0..1) | f2: key: value, kind: 'string'`,
filters: [
filter('f1', [arg('String', 'hi'), namedArg('key', 'Range')]),
filter('f2', [namedArg('key', 'VariableLookup'), namedArg('kind', 'String')]),
],
},
].forEach(({ expression, filters }) => {
ast = toLiquidHtmlAST(`{{ 'hello' ${expression} }}`);
expectPath(ast, 'children.0.type').to.equal('LiquidDrop');
expectPath(ast, 'children.0.markup.type').to.equal('LiquidVariable');
expectPath(ast, 'children.0.markup.rawSource').to.equal(`'hello' ` + expression);
expectPath(ast, 'children.0.markup.filters').to.have.lengthOf(filters.length);
filters.forEach((filter, i) => {
expectPath(ast, `children.0.markup.filters.${i}`).to.exist;
expectPath(ast, `children.0.markup.filters.${i}.type`).to.equal(
'LiquidFilter',
expression,
);
expectPath(ast, `children.0.markup.filters.${i}.name`).to.equal(filter.name);
expectPath(ast, `children.0.markup.filters.${i}.args`).to.exist;
expectPath(ast, `children.0.markup.filters.${i}.args`).to.have.lengthOf(
filter.args.length,
);
filter.args.forEach((arg: any, j) => {
expectPath(ast, `children.0.markup.filters.${i}.args`).to.exist;
switch (arg.type) {
case 'String': {
expectPath(ast, `children.0.markup.filters.${i}.args.${j}.type`).to.equal('String');
expectPath(ast, `children.0.markup.filters.${i}.args.${j}.value`).to.equal(
arg.value,
);
break;
}
case 'NamedArgument': {
expectPath(ast, `children.0.markup.filters.${i}.args`).to.not.be.empty;
expectPath(ast, `children.0.markup.filters.${i}.args.${j}.type`).to.equal(
'NamedArgument',
);
expectPath(ast, `children.0.markup.filters.${i}.args.${j}.name`).to.equal(arg.name);
expectPath(ast, `children.0.markup.filters.${i}.args.${j}.value.type`).to.equal(
arg.valueType,
);
break;
}
}
});
});
expectPath(ast, 'children.0.whitespaceStart').to.equal('');
expectPath(ast, 'children.0.whitespaceEnd').to.equal('');
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', () => {
Expand Down
53 changes: 45 additions & 8 deletions src/parser/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import {
ConcreteAttrUnquoted,
ConcreteLiquidVariable,
ConcreteLiquidLiteral,
ConcreteLiquidFilters,
ConcreteLiquidFilter,
ConcreteLiquidExpression,
ConcreteLiquidNamedArgument,
} from '~/parser/cst';
import { NodeTypes, Position } from '~/types';
import { assertNever, deepGet, dropLast } from '~/utils';
Expand All @@ -35,6 +36,8 @@ export type LiquidHtmlNode =
| AttributeNode
| LiquidVariable
| LiquidExpression
| LiquidFilter
| LiquidNamedArgument
| TextNode;

export interface DocumentNode extends ASTNode<NodeTypes.Document> {
Expand Down Expand Up @@ -137,8 +140,17 @@ export type LiquidExpression =
| LiquidRange
| LiquidVariableLookup;

// TODO
type LiquidFilter = undefined;
interface LiquidFilter extends ASTNode<NodeTypes.LiquidFilter> {
name: string;
args: FilterArgument[];
}

type FilterArgument = LiquidExpression | LiquidNamedArgument;

interface LiquidNamedArgument extends ASTNode<NodeTypes.NamedArgument> {
name: string;
value: LiquidExpression;
}

interface LiquidString extends ASTNode<NodeTypes.String> {
single: boolean;
Expand Down Expand Up @@ -638,7 +650,7 @@ function toLiquidVariable(
return {
type: NodeTypes.LiquidVariable,
expression: toExpression(node.expression, source),
filters: toFilters(node.filters, source),
filters: node.filters.map((filter) => toFilter(filter, source)),
position: position(node),
rawSource: node.rawSource,
source,
Expand Down Expand Up @@ -700,11 +712,36 @@ function toExpression(
}
}

function toFilters(
filters: ConcreteLiquidFilters[],
function toFilter(node: ConcreteLiquidFilter, source: string): LiquidFilter {
return {
type: NodeTypes.LiquidFilter,
name: node.name,
args: node.args.map((arg) => {
switch (arg.type) {
case ConcreteNodeTypes.NamedArgument: {
return toNamedArgument(arg, source);
}
default: {
return toExpression(arg, source);
}
}
}),
position: position(node),
source,
};
}

function toNamedArgument(
node: ConcreteLiquidNamedArgument,
source: string,
): LiquidFilter[] {
return [];
): LiquidNamedArgument {
return {
type: NodeTypes.NamedArgument,
name: node.name,
value: toExpression(node.value, source),
position: position(node),
source,
};
}

function toHtmlElement(node: ConcreteHtmlTagOpen, source: string): HtmlElement {
Expand Down
78 changes: 78 additions & 0 deletions src/parser/cst.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,84 @@ describe('Unit: toLiquidHtmlCST(text)', () => {
expectLocation(cst, '0.markup.expression.end');
});
});

it('should parse filters', () => {
interface Filter {
name: string;
args: FilterArgument[];
}
type FilterArgument = any;

const filter = (name: string, args: FilterArgument[] = []): Filter => ({ name, args });
const arg = (type: string, value: string) => ({ type, value });
const namedArg = (name: string, valueType: string) => ({
type: 'NamedArgument',
name,
valueType,
});
[
{ expression: '', filters: [] },
{ expression: `| filter1`, filters: [filter('filter1')] },
{ expression: `| filter1 | filter2`, filters: [filter('filter1'), filter('filter2')] },
{
expression: `| filter1: 'hi', 'there'`,
filters: [filter('filter1', [arg('String', 'hi'), arg('String', 'there')])],
},
{
expression: `| filter1: key: value, kind: 'string'`,
filters: [
filter('filter1', [namedArg('key', 'VariableLookup'), namedArg('kind', 'String')]),
],
},
{
expression: `| f1: 'hi', key: (0..1) | f2: key: value, kind: 'string'`,
filters: [
filter('f1', [arg('String', 'hi'), namedArg('key', 'Range')]),
filter('f2', [namedArg('key', 'VariableLookup'), namedArg('kind', 'String')]),
],
},
].forEach(({ expression, filters }) => {
cst = toLiquidHtmlCST(`{{ 'hello' ${expression} }}`);
expectPath(cst, '0.type').to.equal('LiquidDrop');
expectPath(cst, '0.markup.type').to.equal('LiquidVariable');
expectPath(cst, '0.markup.rawSource').to.equal((`'hello' ` + expression).trimEnd());
expectPath(cst, '0.markup.filters').to.exist;
expectPath(cst, '0.markup.filters').to.have.lengthOf(filters.length);
filters.forEach((filter, i) => {
expectPath(cst, `0.markup.filters.${i}`).to.exist;
expectPath(cst, `0.markup.filters.${i}.type`).to.equal('LiquidFilter', expression);
expectPath(cst, `0.markup.filters.${i}.name`).to.equal(filter.name);
expectPath(cst, `0.markup.filters.${i}.args`).to.exist;
expectPath(cst, `0.markup.filters.${i}.args`).to.have.lengthOf(
filter.args.length,
expression,
);
filter.args.forEach((arg: any, j) => {
switch (arg.type) {
case 'String': {
expectPath(cst, `0.markup.filters.${i}.args.${j}.type`).to.equal('String');
expectPath(cst, `0.markup.filters.${i}.args.${j}.value`).to.equal(arg.value);
break;
}
case 'NamedArgument': {
expectPath(cst, `0.markup.filters.${i}.args`).to.not.be.empty;
expectPath(cst, `0.markup.filters.${i}.args.${j}.type`).to.equal('NamedArgument');
expectPath(cst, `0.markup.filters.${i}.args.${j}.name`).to.equal(arg.name);
expectPath(cst, `0.markup.filters.${i}.args.${j}.value.type`).to.equal(
arg.valueType,
);
break;
}
}
});
});
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', () => {
Expand Down
Loading

0 comments on commit 4ae0ccc

Please sign in to comment.