Skip to content

[Jinja] Everything is an identifier #1418

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

Merged
merged 38 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bfb8c61
Everything is an identifier
xenova May 2, 2025
8c6aefe
Code improvements
xenova May 2, 2025
65680ea
Re-order tests
xenova May 2, 2025
a5abdb5
Add context-specific keyword tests
xenova May 2, 2025
a29f2e1
Handle special case for membership of undefined
xenova May 2, 2025
f93f308
Remove unused literals
xenova May 2, 2025
df588ab
Add `MEMBERSHIP_UNDEFINED` and `TERNARY_CONSECUTIVE` unit tests
xenova May 2, 2025
c973a66
Add new tests for filter + call statements
xenova May 2, 2025
df0eb97
Add tilde operator
xenova May 2, 2025
b91b7bb
Add minicpm e2e test
xenova May 2, 2025
7342226
Implement basic call/filter statements
xenova May 2, 2025
f3b3117
Improved lexing
xenova May 2, 2025
7542b4f
Implement call & macro statements fully
xenova May 2, 2025
e28ab57
Support consecutive string parsing
xenova May 3, 2025
288d641
Add support for iterable unpacking
xenova May 3, 2025
8bbc205
Add support for variable unpacking in set
xenova May 3, 2025
b82dc9c
Merge branch 'main' into identifiers
xenova May 3, 2025
cadeed1
Lint & formatting
xenova May 3, 2025
5cb4ae9
Support fractional numeric literals
xenova May 3, 2025
a5870be
Add support for int and float filters
xenova May 4, 2025
bacfbe9
Differentiate between integer and float types
xenova May 4, 2025
d06c782
Add jamba e2e test
xenova May 4, 2025
b094bc4
Allow comments to be added to the AST instead of stripped
xenova May 4, 2025
1101173
Assert comments end with #}
xenova May 4, 2025
2b1aa04
Add e2e llama vision test
xenova May 4, 2025
f4c9051
Differentiate between if statement and ternary expression
xenova May 4, 2025
5b6d280
New functionality
xenova May 5, 2025
abeb7a4
Add new unit tests
xenova May 5, 2025
50fc899
Merge branch 'main' into identifiers
xenova May 5, 2025
4e34ab4
Fix 'or' precendence
xenova May 5, 2025
e056139
Improve formatting of chained property accesses
xenova May 5, 2025
87681d9
Add formatting unit tests
xenova May 5, 2025
0ab1ae6
Improve formatting
xenova May 5, 2025
a425563
Improve object & membership formatting
xenova May 5, 2025
9804619
Improve formatting + add new tests
xenova May 5, 2025
e3325d4
Lint & format
xenova May 5, 2025
1e87ae6
Merge branch 'main' into identifiers
xenova May 5, 2025
1bb84ac
nit
xenova May 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 58 additions & 31 deletions packages/jinja/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export class Macro extends Statement {
}
}

export class Comment extends Statement {
override type = "Comment";
constructor(public value: string) {
super();
}
}

/**
* Expressions will result in a value at runtime (unlike statements).
*/
Expand Down Expand Up @@ -133,11 +140,12 @@ abstract class Literal<T> extends Expression {
}
}

/**
* Represents a numeric constant in the template.
*/
export class NumericLiteral extends Literal<number> {
override type = "NumericLiteral";
export class IntegerLiteral extends Literal<number> {
override type = "IntegerLiteral";
}

export class FloatLiteral extends Literal<number> {
override type = "FloatLiteral";
}

/**
Expand All @@ -147,20 +155,6 @@ export class StringLiteral extends Literal<string> {
override type = "StringLiteral";
}

/**
* Represents a boolean constant in the template.
*/
export class BooleanLiteral extends Literal<boolean> {
override type = "BooleanLiteral";
}

/**
* Represents null (none) in the template.
*/
export class NullLiteral extends Literal<null> {
override type = "NullLiteral";
}

/**
* Represents an array literal in the template.
*/
Expand Down Expand Up @@ -214,15 +208,28 @@ export class FilterExpression extends Expression {
}
}

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

constructor(
public filter: Identifier | CallExpression,
public body: Statement[]
) {
super();
}
}

/**
* An operation which filters a sequence of objects by applying a test to each object,
* and only selecting the objects with the test succeeding.
*
* It may also be used as a shortcut for a ternary operator.
*/
export class SelectExpression extends Expression {
override type = "SelectExpression";

constructor(
public iterable: Expression,
public lhs: Expression,
public test: Expression
) {
super();
Expand Down Expand Up @@ -258,17 +265,6 @@ export class UnaryExpression extends Expression {
}
}

/**
* Logical negation of an expression.
*/
export class LogicalNegationExpression extends Expression {
override type = "LogicalNegationExpression";

constructor(public argument: Expression) {
super();
}
}

export class SliceExpression extends Expression {
override type = "SliceExpression";

Expand All @@ -291,3 +287,34 @@ export class KeywordArgumentExpression extends Expression {
super();
}
}

export class SpreadExpression extends Expression {
override type = "SpreadExpression";

constructor(public argument: Expression) {
super();
}
}

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

constructor(
public call: CallExpression,
public callerArgs: Expression[] | null,
public body: Statement[]
) {
super();
}
}

export class Ternary extends Expression {
override type = "Ternary";
constructor(
public condition: Expression,
public trueExpr: Expression,
public falseExpr: Expression
) {
super();
}
}
136 changes: 96 additions & 40 deletions packages/jinja/src/format.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
Program,
Statement,
Comment,
If,
For,
SetStatement,
Expand All @@ -9,9 +10,9 @@ import type {
MemberExpression,
CallExpression,
Identifier,
NumericLiteral,
FloatLiteral,
IntegerLiteral,
StringLiteral,
BooleanLiteral,
ArrayLiteral,
TupleLiteral,
ObjectLiteral,
Expand All @@ -20,20 +21,33 @@ import type {
SelectExpression,
TestExpression,
UnaryExpression,
LogicalNegationExpression,
SliceExpression,
KeywordArgumentExpression,
CallStatement,
FilterStatement,
SpreadExpression,
Ternary,
} from "./ast";

const NEWLINE = "\n";
const OPEN_STATEMENT = "{%- ";
const CLOSE_STATEMENT = " -%}";

const OPERATOR_PRECEDENCE: Record<string, number> = {
MultiplicativeBinaryOperator: 2,
AdditiveBinaryOperator: 1,
ComparisonBinaryOperator: 0,
};
function getBinaryOperatorPrecedence(expr: BinaryExpression): number {
switch (expr.operator.type) {
case "MultiplicativeBinaryOperator":
return 4;
case "AdditiveBinaryOperator":
return 3;
case "ComparisonBinaryOperator":
return 2;
case "Identifier":
if (expr.operator.value === "and") return 1;
if (expr.operator.value === "in" || expr.operator.value === "not in") return 2;
return 0;
}
return 0;
}

export function format(program: Program, indent: string | number = "\t"): string {
const indentStr = typeof indent === "number" ? " ".repeat(indent) : indent;
Expand Down Expand Up @@ -66,6 +80,12 @@ function formatStatement(node: Statement, depth: number, indentStr: string): str
return pad + createStatement("break");
case "Continue":
return pad + createStatement("continue");
case "CallStatement":
return formatCallStatement(node as CallStatement, depth, indentStr);
case "FilterStatement":
return formatFilterStatement(node as FilterStatement, depth, indentStr);
case "Comment":
return pad + "{# " + (node as Comment).value + " #}";
default:
return pad + "{{- " + formatExpression(node as Expression) + " -}}";
}
Expand Down Expand Up @@ -93,7 +113,7 @@ function formatIf(node: If, depth: number, indentStr: string): string {
formatStatements(clauses[0].body, depth + 1, indentStr);

// ELIF(s)
for (let i = 1; i < clauses.length; i++) {
for (let i = 1; i < clauses.length; ++i) {
out +=
NEWLINE +
pad +
Expand All @@ -119,7 +139,7 @@ function formatFor(node: For, depth: number, indentStr: string): string {
if (node.iterable.type === "SelectExpression") {
// Handle special case: e.g., `for x in [1, 2, 3] if x > 2`
const n = node.iterable as SelectExpression;
formattedIterable = `${formatExpression(n.iterable)} if ${formatExpression(n.test)}`;
formattedIterable = `${formatExpression(n.lhs)} if ${formatExpression(n.test)}`;
} else {
formattedIterable = formatExpression(node.iterable);
}
Expand Down Expand Up @@ -166,20 +186,46 @@ function formatMacro(node: Macro, depth: number, indentStr: string): string {
);
}

function formatCallStatement(node: CallStatement, depth: number, indentStr: string): string {
const pad = indentStr.repeat(depth);
const params =
node.callerArgs && node.callerArgs.length > 0 ? `(${node.callerArgs.map(formatExpression).join(", ")})` : "";
const callExpr = formatExpression(node.call);
let out = pad + createStatement(`call${params}`, callExpr) + NEWLINE;
out += formatStatements(node.body, depth + 1, indentStr) + NEWLINE;
out += pad + createStatement("endcall");
return out;
}

function formatFilterStatement(node: FilterStatement, depth: number, indentStr: string): string {
const pad = indentStr.repeat(depth);
const spec =
node.filter.type === "Identifier"
? (node.filter as Identifier).value
: formatExpression(node.filter as CallExpression);
let out = pad + createStatement("filter", spec) + NEWLINE;
out += formatStatements(node.body, depth + 1, indentStr) + NEWLINE;
out += pad + createStatement("endfilter");
return out;
}

function formatExpression(node: Expression, parentPrec: number = -1): string {
switch (node.type) {
case "SpreadExpression": {
const n = node as SpreadExpression;
return `*${formatExpression(n.argument)}`;
}
case "Identifier":
return (node as Identifier).value;
case "NullLiteral":
return "none";
case "NumericLiteral":
case "BooleanLiteral":
return `${(node as NumericLiteral | BooleanLiteral).value}`;
case "IntegerLiteral":
return `${(node as IntegerLiteral).value}`;
case "FloatLiteral":
return `${(node as FloatLiteral).value}`;
case "StringLiteral":
return JSON.stringify((node as StringLiteral).value);
case "BinaryExpression": {
const n = node as BinaryExpression;
const thisPrecedence = OPERATOR_PRECEDENCE[n.operator.type] ?? 0;
const thisPrecedence = getBinaryOperatorPrecedence(n);
const left = formatExpression(n.left, thisPrecedence);
const right = formatExpression(n.right, thisPrecedence + 1);
const expr = `${left} ${n.operator.value} ${right}`;
Expand All @@ -190,20 +236,31 @@ function formatExpression(node: Expression, parentPrec: number = -1): string {
const val = n.operator.value + (n.operator.value === "not" ? " " : "") + formatExpression(n.argument, Infinity);
return val;
}
case "LogicalNegationExpression":
return `not ${formatExpression((node as LogicalNegationExpression).argument, Infinity)}`;
case "CallExpression": {
const n = node as CallExpression;
const args = n.args.map((a) => formatExpression(a, -1)).join(", ");
return `${formatExpression(n.callee, -1)}(${args})`;
const args = n.args.map(formatExpression).join(", ");
return `${formatExpression(n.callee)}(${args})`;
}
case "MemberExpression": {
const n = node as MemberExpression;
let obj = formatExpression(n.object, -1);
if (n.object.type !== "Identifier") {
let obj = formatExpression(n.object);
// only wrap if it's not a simple or chained access/call
if (
![
"Identifier",
"MemberExpression",
"CallExpression",
"StringLiteral",
"IntegerLiteral",
"FloatLiteral",
"ArrayLiteral",
"TupleLiteral",
"ObjectLiteral",
].includes(n.object.type)
) {
obj = `(${obj})`;
}
let prop = formatExpression(n.property, -1);
let prop = formatExpression(n.property);
if (!n.computed && n.property.type !== "Identifier") {
prop = `(${prop})`;
}
Expand All @@ -213,48 +270,47 @@ function formatExpression(node: Expression, parentPrec: number = -1): string {
const n = node as FilterExpression;
const operand = formatExpression(n.operand, Infinity);
if (n.filter.type === "CallExpression") {
return `${operand} | ${formatExpression(n.filter, -1)}`;
return `${operand} | ${formatExpression(n.filter)}`;
}
return `${operand} | ${(n.filter as Identifier).value}`;
}
case "SelectExpression": {
const n = node as SelectExpression;
return `${formatExpression(n.iterable, -1)} | select(${formatExpression(n.test, -1)})`;
return `${formatExpression(n.lhs)} if ${formatExpression(n.test)}`;
}
case "TestExpression": {
const n = node as TestExpression;
return `${formatExpression(n.operand, -1)} is${n.negate ? " not" : ""} ${n.test.value}`;
return `${formatExpression(n.operand)} is${n.negate ? " not" : ""} ${n.test.value}`;
}
case "ArrayLiteral":
case "TupleLiteral": {
const elems = ((node as ArrayLiteral | TupleLiteral).value as Expression[]).map((e) => formatExpression(e, -1));
const elems = ((node as ArrayLiteral | TupleLiteral).value as Expression[]).map(formatExpression);
const brackets = node.type === "ArrayLiteral" ? "[]" : "()";
return `${brackets[0]}${elems.join(", ")}${brackets[1]}`;
}
case "ObjectLiteral": {
const entries = Array.from((node as ObjectLiteral).value.entries()).map(
([k, v]) => `${formatExpression(k, -1)}: ${formatExpression(v, -1)}`
([k, v]) => `${formatExpression(k)}: ${formatExpression(v)}`
);
return `{ ${entries.join(", ")} }`;
return `{${entries.join(", ")}}`;
}
case "SliceExpression": {
const n = node as SliceExpression;
const s = n.start ? formatExpression(n.start, -1) : "";
const t = n.stop ? formatExpression(n.stop, -1) : "";
const st = n.step ? `:${formatExpression(n.step, -1)}` : "";
const s = n.start ? formatExpression(n.start) : "";
const t = n.stop ? formatExpression(n.stop) : "";
const st = n.step ? `:${formatExpression(n.step)}` : "";
return `${s}:${t}${st}`;
}
case "KeywordArgumentExpression": {
const n = node as KeywordArgumentExpression;
return `${n.key.value}=${formatExpression(n.value, -1)}`;
return `${n.key.value}=${formatExpression(n.value)}`;
}
case "If": {
// Special case for ternary operator (If as an expression, not a statement)
const n = node as If;
const test = formatExpression(n.test, -1);
const body = formatExpression(n.body[0], 0); // Ternary operators have a single body and alternate
const alternate = formatExpression(n.alternate[0], -1);
return `${body} if ${test} else ${alternate}`;
case "Ternary": {
const n = node as Ternary;
const expr = `${formatExpression(n.trueExpr)} if ${formatExpression(n.condition, 0)} else ${formatExpression(
n.falseExpr
)}`;
return parentPrec > -1 ? `(${expr})` : expr;
}
default:
throw new Error(`Unknown expression type: ${node.type}`);
Expand Down
Loading