Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jinja] Add support for macros and | tojson #692

Merged
merged 13 commits into from
Aug 7, 2024
34 changes: 33 additions & 1 deletion packages/jinja/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@ export class If extends Statement {
}
}

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

constructor(
public loopvar: Identifier | TupleLiteral,
public iterable: Expression,
public body: Statement[]
public body: Statement[],
public defaultBlock: Statement[] // if no iteration took place
) {
super();
}
Expand All @@ -52,6 +57,18 @@ export class SetStatement extends Statement {
}
}

export class Macro extends Statement {
override type = "Macro";

constructor(
public name: Identifier,
public args: Expression[],
public body: Statement[]
) {
super();
}
}

/**
* Expressions will result in a value at runtime (unlike statements).
*/
Expand Down Expand Up @@ -182,6 +199,21 @@ export class FilterExpression extends Expression {
}
}

/**
* An operation which filters a sequence of objects by applying a test to each object,
* and only selecting the objects with the test succeeding.
*/
export class SelectExpression extends Expression {
override type = "SelectExpression";

constructor(
public iterable: Expression,
public test: Expression
) {
super();
}
}

/**
* An operation with two sides, separated by the "is" operator.
*/
Expand Down
11 changes: 11 additions & 0 deletions packages/jinja/src/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export const TOKEN_TYPES = Object.freeze({
And: "And",
Or: "Or",
Not: "UnaryOperator",
Macro: "Macro",
EndMacro: "EndMacro",
});

export type TokenType = keyof typeof TOKEN_TYPES;
Expand All @@ -65,10 +67,19 @@ const KEYWORDS = Object.freeze({
or: TOKEN_TYPES.Or,
not: TOKEN_TYPES.Not,
"not in": TOKEN_TYPES.NotIn,
macro: TOKEN_TYPES.Macro,
endmacro: TOKEN_TYPES.EndMacro,

// Literals
true: TOKEN_TYPES.BooleanLiteral,
false: TOKEN_TYPES.BooleanLiteral,

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

/**
Expand Down
69 changes: 59 additions & 10 deletions packages/jinja/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
SliceExpression,
KeywordArgumentExpression,
TupleLiteral,
Macro,
SelectExpression,
} from "./ast";

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

case TOKEN_TYPES.Macro:
++current;
result = parseMacroStatement();
expect(TOKEN_TYPES.OpenStatement, "Expected {% token");
expect(TOKEN_TYPES.EndMacro, "Expected endmacro token");
expect(TOKEN_TYPES.CloseStatement, "Expected %} token");
break;

case TOKEN_TYPES.For:
++current;
result = parseForStatement();
Expand Down Expand Up @@ -173,6 +183,25 @@ export function parse(tokens: Token[]): Program {
return new If(test, body, alternate);
}

function parseMacroStatement(): Macro {
const name = parsePrimaryExpression();
if (name.type !== "Identifier") {
throw new SyntaxError(`Expected identifier following macro statement`);
}
const args = parseArgs();
expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token");

// Body of macro
const body: Statement[] = [];

// Keep going until we hit {% endmacro
while (not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.EndMacro)) {
body.push(parseAny());
}

return new Macro(name as Identifier, args, body);
}

function parseExpressionSequence(primary = false): Statement {
const fn = primary ? parsePrimaryExpression : parseExpression;
const expressions = [fn()];
Expand All @@ -189,7 +218,7 @@ export function parse(tokens: Token[]): Program {

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

// Keep going until we hit {% endfor
while (not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.EndFor)) {
// Keep going until we hit {% endfor or {% else
while (not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.EndFor) && not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.Else)) {
body.push(parseAny());
}

return new For(loopVariable, iterable, body);
// (Optional) else block
const alternative: Statement[] = [];
if (is(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.Else)) {
++current; // consume {%
++current; // consume else
expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token");

// keep going until we hit {% endfor
while (not(TOKEN_TYPES.OpenStatement, TOKEN_TYPES.EndFor)) {
alternative.push(parseAny());
}
}

return new For(loopVariable, iterable, body, alternative);
}

function parseExpression(): Statement {
// Choose parse function with lowest precedence
return parseTernaryExpression();
return parseIfExpression();
}

function parseTernaryExpression(): Statement {
function parseIfExpression(): Statement {
const a = parseLogicalOrExpression();
if (is(TOKEN_TYPES.If)) {
// Ternary expression
++current; // consume if
const predicate = parseLogicalOrExpression();
expect(TOKEN_TYPES.Else, "Expected else token");
const b = parseLogicalOrExpression();
return new If(predicate, [a], [b]);

if (is(TOKEN_TYPES.Else)) {
// Ternary expression with else
++current; // consume else
const b = parseLogicalOrExpression();
return new If(predicate, [a], [b]);
} else {
// Select expression on iterable
return new SelectExpression(a, predicate);
}
}
return a;
}
Expand Down Expand Up @@ -477,7 +526,7 @@ export function parse(tokens: Token[]): Program {
return new StringLiteral(token.value);
case TOKEN_TYPES.BooleanLiteral:
++current;
return new BooleanLiteral(token.value === "true");
return new BooleanLiteral(token.value.toLowerCase() === "true");
case TOKEN_TYPES.Identifier:
++current;
return new Identifier(token.value);
Expand Down
Loading
Loading