Skip to content

Commit 717d69d

Browse files
authored
[jinja] Add support for macros and | tojson (#692)
Adds support for Macros. See here for more information: https://jinja.palletsprojects.com/en/3.1.x/templates/#macros cc @Rocketknight1
1 parent e6431d2 commit 717d69d

File tree

6 files changed

+1034
-46
lines changed

6 files changed

+1034
-46
lines changed

packages/jinja/src/ast.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,18 @@ export class If extends Statement {
3030
}
3131
}
3232

33+
/**
34+
* Loop over each item in a sequence
35+
* https://jinja.palletsprojects.com/en/3.0.x/templates/#for
36+
*/
3337
export class For extends Statement {
3438
override type = "For";
3539

3640
constructor(
3741
public loopvar: Identifier | TupleLiteral,
3842
public iterable: Expression,
39-
public body: Statement[]
43+
public body: Statement[],
44+
public defaultBlock: Statement[] // if no iteration took place
4045
) {
4146
super();
4247
}
@@ -52,6 +57,18 @@ export class SetStatement extends Statement {
5257
}
5358
}
5459

60+
export class Macro extends Statement {
61+
override type = "Macro";
62+
63+
constructor(
64+
public name: Identifier,
65+
public args: Expression[],
66+
public body: Statement[]
67+
) {
68+
super();
69+
}
70+
}
71+
5572
/**
5673
* Expressions will result in a value at runtime (unlike statements).
5774
*/
@@ -182,6 +199,21 @@ export class FilterExpression extends Expression {
182199
}
183200
}
184201

202+
/**
203+
* An operation which filters a sequence of objects by applying a test to each object,
204+
* and only selecting the objects with the test succeeding.
205+
*/
206+
export class SelectExpression extends Expression {
207+
override type = "SelectExpression";
208+
209+
constructor(
210+
public iterable: Expression,
211+
public test: Expression
212+
) {
213+
super();
214+
}
215+
}
216+
185217
/**
186218
* An operation with two sides, separated by the "is" operator.
187219
*/

packages/jinja/src/lexer.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export const TOKEN_TYPES = Object.freeze({
4444
And: "And",
4545
Or: "Or",
4646
Not: "UnaryOperator",
47+
Macro: "Macro",
48+
EndMacro: "EndMacro",
4749
});
4850

4951
export type TokenType = keyof typeof TOKEN_TYPES;
@@ -65,10 +67,19 @@ const KEYWORDS = Object.freeze({
6567
or: TOKEN_TYPES.Or,
6668
not: TOKEN_TYPES.Not,
6769
"not in": TOKEN_TYPES.NotIn,
70+
macro: TOKEN_TYPES.Macro,
71+
endmacro: TOKEN_TYPES.EndMacro,
6872

6973
// Literals
7074
true: TOKEN_TYPES.BooleanLiteral,
7175
false: TOKEN_TYPES.BooleanLiteral,
76+
77+
// NOTE: According to the Jinja docs: The special constants true, false, and none are indeed lowercase.
78+
// Because that caused confusion in the past, (True used to expand to an undefined variable that was considered false),
79+
// all three can now also be written in title case (True, False, and None). However, for consistency, (all Jinja identifiers are lowercase)
80+
// you should use the lowercase versions.
81+
True: TOKEN_TYPES.BooleanLiteral,
82+
False: TOKEN_TYPES.BooleanLiteral,
7283
});
7384

7485
/**

packages/jinja/src/parser.ts

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
SliceExpression,
2222
KeywordArgumentExpression,
2323
TupleLiteral,
24+
Macro,
25+
SelectExpression,
2426
} from "./ast";
2527

2628
/**
@@ -90,6 +92,14 @@ export function parse(tokens: Token[]): Program {
9092
expect(TOKEN_TYPES.CloseStatement, "Expected %} token");
9193
break;
9294

95+
case TOKEN_TYPES.Macro:
96+
++current;
97+
result = parseMacroStatement();
98+
expect(TOKEN_TYPES.OpenStatement, "Expected {% token");
99+
expect(TOKEN_TYPES.EndMacro, "Expected endmacro token");
100+
expect(TOKEN_TYPES.CloseStatement, "Expected %} token");
101+
break;
102+
93103
case TOKEN_TYPES.For:
94104
++current;
95105
result = parseForStatement();
@@ -173,6 +183,25 @@ export function parse(tokens: Token[]): Program {
173183
return new If(test, body, alternate);
174184
}
175185

186+
function parseMacroStatement(): Macro {
187+
const name = parsePrimaryExpression();
188+
if (name.type !== "Identifier") {
189+
throw new SyntaxError(`Expected identifier following macro statement`);
190+
}
191+
const args = parseArgs();
192+
expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token");
193+
194+
// Body of macro
195+
const body: Statement[] = [];
196+
197+
// Keep going until we hit {% endmacro
198+
while (not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.EndMacro)) {
199+
body.push(parseAny());
200+
}
201+
202+
return new Macro(name as Identifier, args, body);
203+
}
204+
176205
function parseExpressionSequence(primary = false): Statement {
177206
const fn = primary ? parsePrimaryExpression : parseExpression;
178207
const expressions = [fn()];
@@ -189,7 +218,7 @@ export function parse(tokens: Token[]): Program {
189218

190219
function parseForStatement(): For {
191220
// e.g., `message` in `for message in messages`
192-
const loopVariable = parseExpressionSequence(true); // should be an identifier
221+
const loopVariable = parseExpressionSequence(true); // should be an identifier/tuple
193222
if (!(loopVariable instanceof Identifier || loopVariable instanceof TupleLiteral)) {
194223
throw new SyntaxError(`Expected identifier/tuple for the loop variable, got ${loopVariable.type} instead`);
195224
}
@@ -204,28 +233,48 @@ export function parse(tokens: Token[]): Program {
204233
// Body of for loop
205234
const body: Statement[] = [];
206235

207-
// Keep going until we hit {% endfor
208-
while (not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.EndFor)) {
236+
// Keep going until we hit {% endfor or {% else
237+
while (not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.EndFor) && not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.Else)) {
209238
body.push(parseAny());
210239
}
211240

212-
return new For(loopVariable, iterable, body);
241+
// (Optional) else block
242+
const alternative: Statement[] = [];
243+
if (is(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.Else)) {
244+
++current; // consume {%
245+
++current; // consume else
246+
expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token");
247+
248+
// keep going until we hit {% endfor
249+
while (not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.EndFor)) {
250+
alternative.push(parseAny());
251+
}
252+
}
253+
254+
return new For(loopVariable, iterable, body, alternative);
213255
}
214256

215257
function parseExpression(): Statement {
216258
// Choose parse function with lowest precedence
217-
return parseTernaryExpression();
259+
return parseIfExpression();
218260
}
219261

220-
function parseTernaryExpression(): Statement {
262+
function parseIfExpression(): Statement {
221263
const a = parseLogicalOrExpression();
222264
if (is(TOKEN_TYPES.If)) {
223265
// Ternary expression
224266
++current; // consume if
225267
const predicate = parseLogicalOrExpression();
226-
expect(TOKEN_TYPES.Else, "Expected else token");
227-
const b = parseLogicalOrExpression();
228-
return new If(predicate, [a], [b]);
268+
269+
if (is(TOKEN_TYPES.Else)) {
270+
// Ternary expression with else
271+
++current; // consume else
272+
const b = parseLogicalOrExpression();
273+
return new If(predicate, [a], [b]);
274+
} else {
275+
// Select expression on iterable
276+
return new SelectExpression(a, predicate);
277+
}
229278
}
230279
return a;
231280
}
@@ -477,7 +526,7 @@ export function parse(tokens: Token[]): Program {
477526
return new StringLiteral(token.value);
478527
case TOKEN_TYPES.BooleanLiteral:
479528
++current;
480-
return new BooleanLiteral(token.value === "true");
529+
return new BooleanLiteral(token.value.toLowerCase() === "true");
481530
case TOKEN_TYPES.Identifier:
482531
++current;
483532
return new Identifier(token.value);

0 commit comments

Comments
 (0)