Skip to content

Commit f945dad

Browse files
pweglikxenova
andauthored
✨ Add block assignment functionality in jinja package. (#1341)
This PR implements functionality of block assignments (https://jinja.palletsprojects.com/en/stable/templates/#block-assignments) in lexer, parser and engine= I felt encouraged by the reply here: #1087 (comment) It was necessary to parse some chat_templates e.g. https://huggingface.co/MadeAgents/Hammer2.1-1.5b/blob/main/tokenizer_config.json (nicely formatted `chat_template`: https://pastebin.com/xBmhbwLi) Let me know if you have any feedback. --------- Co-authored-by: Joshua Lochner <[email protected]> Co-authored-by: Joshua Lochner <[email protected]>
1 parent 0cbd641 commit f945dad

File tree

6 files changed

+61
-5
lines changed

6 files changed

+61
-5
lines changed

packages/jinja/src/ast.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ export class SetStatement extends Statement {
5151
override type = "Set";
5252
constructor(
5353
public assignee: Expression,
54-
public value: Expression
54+
public value: Expression | null,
55+
public body: Statement[]
5556
) {
5657
super();
5758
}

packages/jinja/src/lexer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const TOKEN_TYPES = Object.freeze({
3939
Is: "Is",
4040
NotIn: "NotIn",
4141
Else: "Else",
42+
EndSet: "EndSet",
4243
EndIf: "EndIf",
4344
ElseIf: "ElseIf",
4445
EndFor: "EndFor",
@@ -61,6 +62,7 @@ const KEYWORDS = Object.freeze({
6162
is: TOKEN_TYPES.Is,
6263
if: TOKEN_TYPES.If,
6364
else: TOKEN_TYPES.Else,
65+
endset: TOKEN_TYPES.EndSet,
6466
endif: TOKEN_TYPES.EndIf,
6567
elif: TOKEN_TYPES.ElseIf,
6668
endfor: TOKEN_TYPES.EndFor,

packages/jinja/src/parser.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,24 @@ export function parse(tokens: Token[]): Program {
131131

132132
if (is(TOKEN_TYPES.Equals)) {
133133
++current;
134-
const value = parseSetStatement();
134+
const value = parseExpression();
135+
136+
return new SetStatement(left, value, []);
137+
} else {
138+
// parsing multiline set here
139+
const body: Statement[] = [];
140+
expect(TOKEN_TYPES.CloseStatement, "Expected %} token");
141+
while (
142+
!(tokens[current]?.type === TOKEN_TYPES.OpenStatement && tokens[current + 1]?.type === TOKEN_TYPES.EndSet)
143+
) {
144+
const another = parseAny();
145+
body.push(another);
146+
}
147+
expect(TOKEN_TYPES.OpenStatement, "Expected {% token");
148+
expect(TOKEN_TYPES.EndSet, "Expected endset token");
135149

136-
return new SetStatement(left, value);
150+
return new SetStatement(left, null, body);
137151
}
138-
return left;
139152
}
140153

141154
function parseIfStatement(): If {

packages/jinja/src/runtime.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,8 @@ export class Interpreter {
587587
);
588588
case "join":
589589
return new StringValue(operand.value.map((x) => x.value).join(""));
590+
case "string":
591+
return new StringValue(toJSON(operand));
590592
default:
591593
throw new Error(`Unknown ArrayValue filter: ${filter.value}`);
592594
}
@@ -916,7 +918,7 @@ export class Interpreter {
916918
}
917919

918920
private evaluateSet(node: SetStatement, environment: Environment): NullValue {
919-
const rhs = this.evaluate(node.value, environment);
921+
const rhs = node.value ? this.evaluate(node.value, environment) : this.evaluateBlock(node.body, environment);
920922
if (node.assignee.type === "Identifier") {
921923
const variableName = (node.assignee as Identifier).value;
922924
environment.setVariable(variableName, rhs);

packages/jinja/test/e2e.test.js

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/jinja/test/templates.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const TEST_STRINGS = {
3737
// Set variables
3838
VARIABLES: `{% set x = 'Hello' %}{% set y = 'World' %}{{ x + ' ' + y }}`,
3939
VARIABLES_2: `{% set x = 'Hello'.split('el')[-1] %}{{ x }}`,
40+
VARIABLES_BLOCK: `{% set x %}Hello!\nMultiline/block set!\n{% endset %}{{ x }}`,
4041

4142
// Numbers
4243
NUMBERS: `|{{ 5 }}|{{ -5 }}|{{ add(3, -1) }}|{{ (3 - 1) + (a - 5) - (a + 5)}}|`,
@@ -90,6 +91,7 @@ const TEST_STRINGS = {
9091
FILTER_OPERATOR_10: `|{{ " 1 \n 2 \n 3 \n\n " | indent }}|{{ " 1 \n 2 \n 3 \n\n " | indent(2) }}|{{ " 1 \n 2 \n 3 \n\n " | indent(first=True) }}|{{ " 1 \n 2 \n 3 \n\n " | indent(blank=True) }}|{{ " 1 \n 2 \n 3 \n\n " | indent(4, first=True) }}|`,
9192
FILTER_OPERATOR_11: `{{ items | rejectattr('key') | length }}`,
9293
FILTER_OPERATOR_12: `{{ messages | rejectattr('role', 'equalto', 'system') | length }}`,
94+
FILTER_OPERATOR_13: `{{ tools | string }}`,
9395

9496
// Logical operators between non-Booleans
9597
BOOLEAN_NUMERICAL: `|{{ 1 and 2 }}|{{ 1 and 0 }}|{{ 0 and 1 }}|{{ 0 and 0 }}|{{ 1 or 2 }}|{{ 1 or 0 }}|{{ 0 or 1 }}|{{ 0 or 0 }}|{{ not 1 }}|{{ not 0 }}|`,
@@ -705,6 +707,19 @@ const TEST_PARSED = {
705707
{ value: "x", type: "Identifier" },
706708
{ value: "}}", type: "CloseExpression" },
707709
],
710+
VARIABLES_BLOCK: [
711+
{ value: "{%", type: "OpenStatement" },
712+
{ value: "set", type: "Set" },
713+
{ value: "x", type: "Identifier" },
714+
{ value: "%}", type: "CloseStatement" },
715+
{ value: `Hello!\nMultiline/block set!\n`, type: "Text" },
716+
{ value: "{%", type: "OpenStatement" },
717+
{ value: "endset", type: "EndSet" },
718+
{ value: "%}", type: "CloseStatement" },
719+
{ value: "{{", type: "OpenExpression" },
720+
{ value: "x", type: "Identifier" },
721+
{ value: "}}", type: "CloseExpression" },
722+
],
708723

709724
// Numbers
710725
NUMBERS: [
@@ -1682,6 +1697,13 @@ const TEST_PARSED = {
16821697
{ value: "length", type: "Identifier" },
16831698
{ value: "}}", type: "CloseExpression" },
16841699
],
1700+
FILTER_OPERATOR_13: [
1701+
{ value: "{{", type: "OpenExpression" },
1702+
{ value: "tools", type: "Identifier" },
1703+
{ value: "|", type: "Pipe" },
1704+
{ value: "string", type: "Identifier" },
1705+
{ value: "}}", type: "CloseExpression" },
1706+
],
16851707

16861708
// Logical operators between non-Booleans
16871709
BOOLEAN_NUMERICAL: [
@@ -3038,6 +3060,7 @@ const TEST_CONTEXT = {
30383060
// Set variables
30393061
VARIABLES: {},
30403062
VARIABLES_2: {},
3063+
VARIABLES_BLOCK: {},
30413064

30423065
// Numbers
30433066
NUMBERS: {
@@ -3173,6 +3196,9 @@ const TEST_CONTEXT = {
31733196
FILTER_OPERATOR_12: {
31743197
messages: [{ role: "system" }, { role: "user" }, { role: "assistant" }],
31753198
},
3199+
FILTER_OPERATOR_13: {
3200+
tools: [{ name: "some_tool", arguments: { some_name: "string" } }],
3201+
},
31763202

31773203
// Logical operators between non-Booleans
31783204
BOOLEAN_NUMERICAL: {},
@@ -3300,6 +3326,7 @@ const EXPECTED_OUTPUTS = {
33003326
// Set variables
33013327
VARIABLES: "Hello World",
33023328
VARIABLES_2: "lo",
3329+
VARIABLES_BLOCK: "Hello!\nMultiline/block set!\n",
33033330

33043331
// Numbers
33053332
NUMBERS: "|5|-5|2|-8|",
@@ -3353,6 +3380,7 @@ const EXPECTED_OUTPUTS = {
33533380
FILTER_OPERATOR_10: `| 1 \n 2 \n 3 \n\n | 1 \n 2 \n 3 \n\n | 1 \n 2 \n 3 \n\n | 1 \n 2 \n 3 \n \n | 1 \n 2 \n 3 \n\n |`,
33543381
FILTER_OPERATOR_11: `3`,
33553382
FILTER_OPERATOR_12: `2`,
3383+
FILTER_OPERATOR_13: `[{"name": "some_tool", "arguments": {"some_name": "string"}}]`,
33563384

33573385
// Logical operators between non-Booleans
33583386
BOOLEAN_NUMERICAL: `|2|0|0|0|1|1|1|0|false|true|`,

0 commit comments

Comments
 (0)