machineuser commited on
Commit
43f8c77
1 Parent(s): 29387b0

Sync widgets demo

Browse files
packages/jinja/package.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "@huggingface/jinja",
3
  "packageManager": "pnpm@8.10.5",
4
- "version": "0.1.3",
5
  "description": "A minimalistic JavaScript implementation of the Jinja templating engine, specifically designed for parsing and rendering ML chat templates.",
6
  "repository": "https://github.com/huggingface/huggingface.js.git",
7
  "publishConfig": {
 
1
  {
2
  "name": "@huggingface/jinja",
3
  "packageManager": "pnpm@8.10.5",
4
+ "version": "0.2.0",
5
  "description": "A minimalistic JavaScript implementation of the Jinja templating engine, specifically designed for parsing and rendering ML chat templates.",
6
  "repository": "https://github.com/huggingface/huggingface.js.git",
7
  "publishConfig": {
packages/jinja/src/ast.ts CHANGED
@@ -165,6 +165,21 @@ export class FilterExpression extends Expression {
165
  }
166
  }
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  /**
169
  * An operation with one side (operator on the left).
170
  */
@@ -201,3 +216,14 @@ export class SliceExpression extends Expression {
201
  super();
202
  }
203
  }
 
 
 
 
 
 
 
 
 
 
 
 
165
  }
166
  }
167
 
168
+ /**
169
+ * An operation with two sides, separated by the "is" operator.
170
+ */
171
+ export class TestExpression extends Expression {
172
+ override type = "TestExpression";
173
+
174
+ constructor(
175
+ public operand: Expression,
176
+ public negate: boolean,
177
+ public test: Identifier // TODO: Add support for non-identifier tests
178
+ ) {
179
+ super();
180
+ }
181
+ }
182
+
183
  /**
184
  * An operation with one side (operator on the left).
185
  */
 
216
  super();
217
  }
218
  }
219
+
220
+ export class KeywordArgumentExpression extends Expression {
221
+ override type = "KeywordArgumentExpression";
222
+
223
+ constructor(
224
+ public key: Identifier,
225
+ public value: Expression
226
+ ) {
227
+ super();
228
+ }
229
+ }
packages/jinja/src/index.ts CHANGED
@@ -10,11 +10,12 @@
10
  *
11
  * @module index
12
  */
13
- import type { Program } from "./ast";
14
  import { tokenize } from "./lexer";
15
  import { parse } from "./parser";
16
- import type { StringValue } from "./runtime";
17
  import { Environment, Interpreter } from "./runtime";
 
 
 
18
 
19
  export class Template {
20
  parsed: Program;
@@ -23,11 +24,10 @@ export class Template {
23
  * @param {string} template The template string
24
  */
25
  constructor(template: string) {
26
- // Since `lstrip_blocks` is enabled, we strip tabs and spaces from the beginning of a line to the start of a block.
27
- // We can achieve the same effect by replacing all instances of `%}\s+{%` with `%}{%` (as a pre-processing step).
28
- template = template.replace(/%}\s+{%/g, "%}{%");
29
-
30
- const tokens = tokenize(template);
31
  this.parsed = parse(tokens);
32
  }
33
 
@@ -41,6 +41,7 @@ export class Template {
41
  env.set("raise_exception", (args: string) => {
42
  throw new Error(args);
43
  });
 
44
 
45
  // Add user-defined variables
46
  for (const [key, value] of Object.entries(items)) {
 
10
  *
11
  * @module index
12
  */
 
13
  import { tokenize } from "./lexer";
14
  import { parse } from "./parser";
 
15
  import { Environment, Interpreter } from "./runtime";
16
+ import type { Program } from "./ast";
17
+ import type { StringValue } from "./runtime";
18
+ import { range } from "./utils";
19
 
20
  export class Template {
21
  parsed: Program;
 
24
  * @param {string} template The template string
25
  */
26
  constructor(template: string) {
27
+ const tokens = tokenize(template, {
28
+ lstrip_blocks: true,
29
+ trim_blocks: true,
30
+ });
 
31
  this.parsed = parse(tokens);
32
  }
33
 
 
41
  env.set("raise_exception", (args: string) => {
42
  throw new Error(args);
43
  });
44
+ env.set("range", range);
45
 
46
  // Add user-defined variables
47
  for (const [key, value] of Object.entries(items)) {
packages/jinja/src/lexer.ts CHANGED
@@ -33,6 +33,7 @@ export const TOKEN_TYPES = Object.freeze({
33
  If: "If",
34
  For: "For",
35
  In: "In",
 
36
  NotIn: "NotIn",
37
  Else: "Else",
38
  EndIf: "EndIf",
@@ -52,6 +53,7 @@ const KEYWORDS = Object.freeze({
52
  set: TOKEN_TYPES.Set,
53
  for: TOKEN_TYPES.For,
54
  in: TOKEN_TYPES.In,
 
55
  if: TOKEN_TYPES.If,
56
  else: TOKEN_TYPES.Else,
57
  endif: TOKEN_TYPES.EndIf,
@@ -137,12 +139,46 @@ const ESCAPE_CHARACTERS = new Map([
137
  ["\\", "\\"], // Backslash
138
  ]);
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  /**
141
  * Generate a list of tokens from a source string.
142
  */
143
- export function tokenize(source: string): Token[] {
144
  const tokens: Token[] = [];
145
- const src: string = source;
146
 
147
  let cursorPosition = 0;
148
 
 
33
  If: "If",
34
  For: "For",
35
  In: "In",
36
+ Is: "Is",
37
  NotIn: "NotIn",
38
  Else: "Else",
39
  EndIf: "EndIf",
 
53
  set: TOKEN_TYPES.Set,
54
  for: TOKEN_TYPES.For,
55
  in: TOKEN_TYPES.In,
56
+ is: TOKEN_TYPES.Is,
57
  if: TOKEN_TYPES.If,
58
  else: TOKEN_TYPES.Else,
59
  endif: TOKEN_TYPES.EndIf,
 
139
  ["\\", "\\"], // Backslash
140
  ]);
141
 
142
+ export interface PreprocessOptions {
143
+ trim_blocks?: boolean;
144
+ lstrip_blocks?: boolean;
145
+ }
146
+
147
+ function preprocess(template: string, options: PreprocessOptions = {}): string {
148
+ // According to https://jinja.palletsprojects.com/en/3.0.x/templates/#whitespace-control
149
+
150
+ // In the default configuration:
151
+ // - a single trailing newline is stripped if present
152
+ // - other whitespace (spaces, tabs, newlines etc.) is returned unchanged
153
+ if (template.endsWith("\n")) {
154
+ template = template.slice(0, -1);
155
+ }
156
+
157
+ if (options.trim_blocks) {
158
+ // If an application configures Jinja to trim_blocks, the first newline after
159
+ // a template tag is removed automatically (like in PHP).
160
+ template = template.replace(/%}\n/g, "%}");
161
+ }
162
+ if (options.lstrip_blocks) {
163
+ // The lstrip_blocks option can also be set to strip tabs and spaces from the
164
+ // beginning of a line to the start of a block. (Nothing will be stripped if
165
+ // there are other characters before the start of the block.)
166
+ template = template.replace(/^[ \t]*{%/gm, "{%");
167
+ }
168
+
169
+ return template
170
+ .replace(/-%}\s*/g, "%}")
171
+ .replace(/\s*{%-/g, "{%")
172
+ .replace(/-}}\s*/g, "}}")
173
+ .replace(/\s*{{-/g, "{{");
174
+ }
175
+
176
  /**
177
  * Generate a list of tokens from a source string.
178
  */
179
+ export function tokenize(source: string, options: PreprocessOptions = {}): Token[] {
180
  const tokens: Token[] = [];
181
+ const src: string = preprocess(source, options);
182
 
183
  let cursorPosition = 0;
184
 
packages/jinja/src/parser.ts CHANGED
@@ -14,8 +14,10 @@ import {
14
  BooleanLiteral,
15
  BinaryExpression,
16
  FilterExpression,
 
17
  UnaryExpression,
18
  SliceExpression,
 
19
  } from "./ast";
20
 
21
  /**
@@ -26,6 +28,12 @@ export function parse(tokens: Token[]): Program {
26
  const program = new Program([]);
27
  let current = 0;
28
 
 
 
 
 
 
 
29
  function expect(type: string, error: string): Token {
30
  const prev = tokens[current++];
31
  if (!prev || prev.type !== type) {
@@ -191,6 +199,7 @@ export function parse(tokens: Token[]): Program {
191
  // Choose parse function with lowest precedence
192
  return parseLogicalOrExpression();
193
  }
 
194
  function parseLogicalOrExpression(): Statement {
195
  let left = parseLogicalAndExpression();
196
  while (is(TOKEN_TYPES.Or)) {
@@ -278,21 +287,36 @@ export function parse(tokens: Token[]): Program {
278
  // add (x + 5, foo())
279
  expect(TOKEN_TYPES.OpenParen, "Expected opening parenthesis for arguments list");
280
 
281
- const args = is(TOKEN_TYPES.CloseParen) ? [] : parseArgumentsList();
282
 
283
  expect(TOKEN_TYPES.CloseParen, "Expected closing parenthesis for arguments list");
284
  return args;
285
  }
286
  function parseArgumentsList(): Statement[] {
287
  // comma-separated arguments list
288
- const args = [parseExpression()]; // Update when we allow assignment expressions
289
 
290
- while (is(TOKEN_TYPES.Comma)) {
291
- ++current; // consume comma
292
- args.push(parseExpression());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  }
294
  return args;
295
  }
 
296
  function parseMemberExpressionArgumentsList(): Statement {
297
  // NOTE: This also handles slice expressions colon-separated arguments list
298
  // e.g., ['test'], [0], [:2], [1:], [1:2], [1:2:3]
@@ -354,17 +378,45 @@ export function parse(tokens: Token[]): Program {
354
  }
355
 
356
  function parseMultiplicativeExpression(): Statement {
357
- let left = parseFilterExpression();
 
 
 
358
 
359
  while (is(TOKEN_TYPES.MultiplicativeBinaryOperator)) {
360
  const operator = tokens[current];
361
  ++current;
362
- const right = parseFilterExpression();
363
  left = new BinaryExpression(operator, left, right);
364
  }
365
  return left;
366
  }
367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  function parseFilterExpression(): Statement {
369
  let operand = parseCallMemberExpression();
370
 
 
14
  BooleanLiteral,
15
  BinaryExpression,
16
  FilterExpression,
17
+ TestExpression,
18
  UnaryExpression,
19
  SliceExpression,
20
+ KeywordArgumentExpression,
21
  } from "./ast";
22
 
23
  /**
 
28
  const program = new Program([]);
29
  let current = 0;
30
 
31
+ /**
32
+ * Consume the next token if it matches the expected type, otherwise throw an error.
33
+ * @param type The expected token type
34
+ * @param error The error message to throw if the token does not match the expected type
35
+ * @returns The consumed token
36
+ */
37
  function expect(type: string, error: string): Token {
38
  const prev = tokens[current++];
39
  if (!prev || prev.type !== type) {
 
199
  // Choose parse function with lowest precedence
200
  return parseLogicalOrExpression();
201
  }
202
+
203
  function parseLogicalOrExpression(): Statement {
204
  let left = parseLogicalAndExpression();
205
  while (is(TOKEN_TYPES.Or)) {
 
287
  // add (x + 5, foo())
288
  expect(TOKEN_TYPES.OpenParen, "Expected opening parenthesis for arguments list");
289
 
290
+ const args = parseArgumentsList();
291
 
292
  expect(TOKEN_TYPES.CloseParen, "Expected closing parenthesis for arguments list");
293
  return args;
294
  }
295
  function parseArgumentsList(): Statement[] {
296
  // comma-separated arguments list
 
297
 
298
+ const args = [];
299
+ while (!is(TOKEN_TYPES.CloseParen)) {
300
+ let argument = parseExpression();
301
+
302
+ if (is(TOKEN_TYPES.Equals)) {
303
+ // keyword argument
304
+ // e.g., func(x = 5, y = a or b)
305
+ ++current; // consume equals
306
+ if (!(argument instanceof Identifier)) {
307
+ throw new SyntaxError(`Expected identifier for keyword argument`);
308
+ }
309
+ const value = parseExpression();
310
+ argument = new KeywordArgumentExpression(argument, value);
311
+ }
312
+ args.push(argument);
313
+ if (is(TOKEN_TYPES.Comma)) {
314
+ ++current; // consume comma
315
+ }
316
  }
317
  return args;
318
  }
319
+
320
  function parseMemberExpressionArgumentsList(): Statement {
321
  // NOTE: This also handles slice expressions colon-separated arguments list
322
  // e.g., ['test'], [0], [:2], [1:], [1:2], [1:2:3]
 
378
  }
379
 
380
  function parseMultiplicativeExpression(): Statement {
381
+ let left = parseTestExpression();
382
+
383
+ // Multiplicative operators have higher precedence than test expressions
384
+ // e.g., (4 * 4 is divisibleby(2)) evaluates as (4 * (4 is divisibleby(2)))
385
 
386
  while (is(TOKEN_TYPES.MultiplicativeBinaryOperator)) {
387
  const operator = tokens[current];
388
  ++current;
389
+ const right = parseTestExpression();
390
  left = new BinaryExpression(operator, left, right);
391
  }
392
  return left;
393
  }
394
 
395
+ function parseTestExpression(): Statement {
396
+ let operand = parseFilterExpression();
397
+
398
+ while (is(TOKEN_TYPES.Is)) {
399
+ // Support chaining tests
400
+ ++current; // consume is
401
+ const negate = is(TOKEN_TYPES.Not);
402
+ if (negate) {
403
+ ++current; // consume not
404
+ }
405
+
406
+ let filter = parsePrimaryExpression();
407
+ if (filter instanceof BooleanLiteral) {
408
+ // Special case: treat boolean literals as identifiers
409
+ filter = new Identifier(filter.value.toString());
410
+ }
411
+ if (!(filter instanceof Identifier)) {
412
+ throw new SyntaxError(`Expected identifier for the test`);
413
+ }
414
+ // TODO: Add support for non-identifier tests
415
+ operand = new TestExpression(operand, negate, filter);
416
+ }
417
+ return operand;
418
+ }
419
+
420
  function parseFilterExpression(): Statement {
421
  let operand = parseCallMemberExpression();
422
 
packages/jinja/src/runtime.ts CHANGED
@@ -12,10 +12,12 @@ import type {
12
  Identifier,
13
  BinaryExpression,
14
  FilterExpression,
 
15
  UnaryExpression,
16
  SliceExpression,
 
17
  } from "./ast";
18
- import { slice } from "./utils";
19
 
20
  export type AnyRuntimeValue =
21
  | NumericValue
@@ -89,6 +91,12 @@ export class StringValue extends RuntimeValue<string> {
89
  return new StringValue(this.value.trim());
90
  }),
91
  ],
 
 
 
 
 
 
92
  ["length", new NumericValue(this.value.length)],
93
  ]);
94
  }
@@ -167,7 +175,20 @@ export class Environment {
167
  /**
168
  * The variables declared in this environment.
169
  */
170
- variables: Map<string, AnyRuntimeValue> = new Map();
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
  constructor(public parent?: Environment) {}
173
 
@@ -245,25 +266,27 @@ export class Interpreter {
245
  }
246
 
247
  /**
248
- * Evaulates expressions following the binary operation type.
249
  */
250
  private evaluateBinaryExpression(node: BinaryExpression, environment: Environment): AnyRuntimeValue {
251
  const left = this.evaluate(node.left, environment);
252
- const right = this.evaluate(node.right, environment);
253
 
254
- // Arbitrary operands
 
 
 
 
 
 
 
 
 
 
255
  switch (node.operator.value) {
256
- // Equality operators
257
  case "==":
258
  return new BooleanValue(left.value == right.value);
259
  case "!=":
260
  return new BooleanValue(left.value != right.value);
261
-
262
- // Logical operators
263
- case "and":
264
- return left.__bool__().value ? right : left;
265
- case "or":
266
- return left.__bool__().value ? left : right;
267
  }
268
 
269
  if (left instanceof UndefinedValue || right instanceof UndefinedValue) {
@@ -326,7 +349,7 @@ export class Interpreter {
326
  }
327
 
328
  /**
329
- * Evaulates expressions following the filter operation type.
330
  */
331
  private evaluateFilterExpression(node: FilterExpression, environment: Environment): AnyRuntimeValue {
332
  const operand = this.evaluate(node.operand, environment);
@@ -380,7 +403,7 @@ export class Interpreter {
380
  case "lower":
381
  return new StringValue(operand.value.toLowerCase());
382
  case "title":
383
- return new StringValue(operand.value.replace(/\b\w/g, (c) => c.toUpperCase()));
384
  case "capitalize":
385
  return new StringValue(operand.value.charAt(0).toUpperCase() + operand.value.slice(1));
386
  case "trim":
@@ -401,7 +424,77 @@ export class Interpreter {
401
  }
402
 
403
  /**
404
- * Evaulates expressions following the unary operation type.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  */
406
  private evaluateUnaryExpression(node: UnaryExpression, environment: Environment): AnyRuntimeValue {
407
  const argument = this.evaluate(node.argument, environment);
@@ -414,7 +507,7 @@ export class Interpreter {
414
  }
415
  }
416
 
417
- private evalProgram(program: Program, environment: Environment): AnyRuntimeValue {
418
  return this.evaluateBlock(program.body, environment);
419
  }
420
 
@@ -431,9 +524,6 @@ export class Interpreter {
431
  }
432
  }
433
 
434
- // Since `trim_blocks` is enabled, we remove the first newline after the template tag
435
- result = result.replace(/^\n/, "");
436
-
437
  return new StringValue(result);
438
  }
439
 
@@ -442,7 +532,22 @@ export class Interpreter {
442
  }
443
 
444
  private evaluateCallExpression(expr: CallExpression, environment: Environment): AnyRuntimeValue {
445
- const args = expr.args.map((arg) => this.evaluate(arg, environment) as AnyRuntimeValue);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  const fn = this.evaluate(expr.callee, environment);
447
  if (fn.type !== "FunctionValue") {
448
  throw new Error(`Cannot call something that is not a function: got ${fn.type}`);
@@ -525,12 +630,25 @@ export class Interpreter {
525
  }
526
 
527
  private evaluateSet(node: SetStatement, environment: Environment): NullValue {
528
- if (node.assignee.type !== "Identifier") {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  throw new Error(`Invalid LHS inside assignment expression: ${JSON.stringify(node.assignee)}`);
530
  }
531
 
532
- const variableName = (node.assignee as Identifier).value;
533
- environment.setVariable(variableName, this.evaluate(node.value, environment));
534
  return new NullValue();
535
  }
536
 
@@ -553,22 +671,19 @@ export class Interpreter {
553
  for (let i = 0; i < iterable.value.length; ++i) {
554
  // Update the loop variable
555
  // TODO: Only create object once, then update value?
556
- scope.setVariable(
557
- "loop",
558
- new ObjectValue(
559
- new Map(
560
- (
561
- [
562
- ["index", new NumericValue(i + 1)],
563
- ["index0", new NumericValue(i)],
564
- ["first", new BooleanValue(i === 0)],
565
- ["last", new BooleanValue(i === iterable.value.length - 1)],
566
- ["length", new NumericValue(iterable.value.length)],
567
- ] as [string, AnyRuntimeValue][]
568
- ).map(([key, value]) => [key, value])
569
- )
570
- )
571
- );
572
 
573
  // For this iteration, set the loop variable to the current element
574
  scope.setVariable(node.loopvar.value, iterable.value[i]);
@@ -617,6 +732,8 @@ export class Interpreter {
617
  return this.evaluateBinaryExpression(statement as BinaryExpression, environment);
618
  case "FilterExpression":
619
  return this.evaluateFilterExpression(statement as FilterExpression, environment);
 
 
620
 
621
  default:
622
  throw new SyntaxError(`Unknown node type: ${statement.type}`);
 
12
  Identifier,
13
  BinaryExpression,
14
  FilterExpression,
15
+ TestExpression,
16
  UnaryExpression,
17
  SliceExpression,
18
+ KeywordArgumentExpression,
19
  } from "./ast";
20
+ import { slice, titleCase } from "./utils";
21
 
22
  export type AnyRuntimeValue =
23
  | NumericValue
 
91
  return new StringValue(this.value.trim());
92
  }),
93
  ],
94
+ [
95
+ "title",
96
+ new FunctionValue(() => {
97
+ return new StringValue(titleCase(this.value));
98
+ }),
99
+ ],
100
  ["length", new NumericValue(this.value.length)],
101
  ]);
102
  }
 
175
  /**
176
  * The variables declared in this environment.
177
  */
178
+ variables: Map<string, AnyRuntimeValue> = new Map([
179
+ [
180
+ "namespace",
181
+ new FunctionValue((args) => {
182
+ if (args.length === 0) {
183
+ return new ObjectValue(new Map());
184
+ }
185
+ if (args.length !== 1 || !(args[0] instanceof ObjectValue)) {
186
+ throw new Error("`namespace` expects either zero arguments or a single object argument");
187
+ }
188
+ return args[0];
189
+ }),
190
+ ],
191
+ ]);
192
 
193
  constructor(public parent?: Environment) {}
194
 
 
266
  }
267
 
268
  /**
269
+ * Evaluates expressions following the binary operation type.
270
  */
271
  private evaluateBinaryExpression(node: BinaryExpression, environment: Environment): AnyRuntimeValue {
272
  const left = this.evaluate(node.left, environment);
 
273
 
274
+ // Logical operators
275
+ // NOTE: Short-circuiting is handled by the `evaluate` function
276
+ switch (node.operator.value) {
277
+ case "and":
278
+ return left.__bool__().value ? this.evaluate(node.right, environment) : left;
279
+ case "or":
280
+ return left.__bool__().value ? left : this.evaluate(node.right, environment);
281
+ }
282
+
283
+ // Equality operators
284
+ const right = this.evaluate(node.right, environment);
285
  switch (node.operator.value) {
 
286
  case "==":
287
  return new BooleanValue(left.value == right.value);
288
  case "!=":
289
  return new BooleanValue(left.value != right.value);
 
 
 
 
 
 
290
  }
291
 
292
  if (left instanceof UndefinedValue || right instanceof UndefinedValue) {
 
349
  }
350
 
351
  /**
352
+ * Evaluates expressions following the filter operation type.
353
  */
354
  private evaluateFilterExpression(node: FilterExpression, environment: Environment): AnyRuntimeValue {
355
  const operand = this.evaluate(node.operand, environment);
 
403
  case "lower":
404
  return new StringValue(operand.value.toLowerCase());
405
  case "title":
406
+ return new StringValue(titleCase(operand.value));
407
  case "capitalize":
408
  return new StringValue(operand.value.charAt(0).toUpperCase() + operand.value.slice(1));
409
  case "trim":
 
424
  }
425
 
426
  /**
427
+ * Evaluates expressions following the test operation type.
428
+ */
429
+ private evaluateTestExpression(node: TestExpression, environment: Environment): BooleanValue {
430
+ // For now, we only support the built-in tests
431
+ // https://jinja.palletsprojects.com/en/3.0.x/templates/#list-of-builtin-tests
432
+ //
433
+ // TODO: Add support for non-identifier tests. e.g., divisibleby(number)
434
+
435
+ const result: boolean = (() => {
436
+ try {
437
+ const operand = this.evaluate(node.operand, environment);
438
+
439
+ switch (node.test.value) {
440
+ case "boolean":
441
+ return operand.type === "BooleanValue";
442
+ case "callable":
443
+ return operand instanceof FunctionValue;
444
+ case "odd":
445
+ if (operand.type !== "NumericValue") {
446
+ throw new Error(`Cannot apply test "odd" to type: ${operand.type}`);
447
+ }
448
+ return (operand as NumericValue).value % 2 !== 0;
449
+ case "even":
450
+ if (operand.type !== "NumericValue") {
451
+ throw new Error(`Cannot apply test "even" to type: ${operand.type}`);
452
+ }
453
+ return (operand as NumericValue).value % 2 === 0;
454
+ case "false":
455
+ return operand.type === "BooleanValue" && !(operand as BooleanValue).value;
456
+ case "true":
457
+ return operand.type === "BooleanValue" && (operand as BooleanValue).value;
458
+ case "number":
459
+ return operand.type === "NumericValue";
460
+ case "integer":
461
+ return operand.type === "NumericValue" && Number.isInteger((operand as NumericValue).value);
462
+ case "iterable":
463
+ return operand instanceof ArrayValue || operand instanceof StringValue;
464
+ case "lower": {
465
+ const str = (operand as StringValue).value;
466
+ return operand.type === "StringValue" && str === str.toLowerCase();
467
+ }
468
+ case "upper": {
469
+ const str = (operand as StringValue).value;
470
+ return operand.type === "StringValue" && str === str.toUpperCase();
471
+ }
472
+ case "none":
473
+ return operand.type === "NullValue";
474
+ case "defined":
475
+ return true;
476
+ case "undefined":
477
+ return false;
478
+ }
479
+ throw new Error(`Unknown test: ${node.test.value}`);
480
+ } catch (e) {
481
+ if (node.operand.type === "Identifier") {
482
+ // Special cases where we want to check if a variable is defined
483
+ if (node.test.value === "defined") {
484
+ return false;
485
+ } else if (node.test.value === "undefined") {
486
+ return true;
487
+ }
488
+ }
489
+ throw e;
490
+ }
491
+ })();
492
+
493
+ return new BooleanValue(node.negate ? !result : result);
494
+ }
495
+
496
+ /**
497
+ * Evaluates expressions following the unary operation type.
498
  */
499
  private evaluateUnaryExpression(node: UnaryExpression, environment: Environment): AnyRuntimeValue {
500
  const argument = this.evaluate(node.argument, environment);
 
507
  }
508
  }
509
 
510
+ private evalProgram(program: Program, environment: Environment): StringValue {
511
  return this.evaluateBlock(program.body, environment);
512
  }
513
 
 
524
  }
525
  }
526
 
 
 
 
527
  return new StringValue(result);
528
  }
529
 
 
532
  }
533
 
534
  private evaluateCallExpression(expr: CallExpression, environment: Environment): AnyRuntimeValue {
535
+ // Accumulate all keyword arguments into a single object, which will be
536
+ // used as the final argument in the call function.
537
+ const args: AnyRuntimeValue[] = [];
538
+ const kwargs = new Map();
539
+ for (const argument of expr.args) {
540
+ if (argument.type === "KeywordArgumentExpression") {
541
+ const kwarg = argument as KeywordArgumentExpression;
542
+ kwargs.set(kwarg.key.value, this.evaluate(kwarg.value, environment));
543
+ } else {
544
+ args.push(this.evaluate(argument, environment));
545
+ }
546
+ }
547
+ if (kwargs.size > 0) {
548
+ args.push(new ObjectValue(kwargs));
549
+ }
550
+
551
  const fn = this.evaluate(expr.callee, environment);
552
  if (fn.type !== "FunctionValue") {
553
  throw new Error(`Cannot call something that is not a function: got ${fn.type}`);
 
630
  }
631
 
632
  private evaluateSet(node: SetStatement, environment: Environment): NullValue {
633
+ const rhs = this.evaluate(node.value, environment);
634
+ if (node.assignee.type === "Identifier") {
635
+ const variableName = (node.assignee as Identifier).value;
636
+ environment.setVariable(variableName, rhs);
637
+ } else if (node.assignee.type === "MemberExpression") {
638
+ const member = node.assignee as MemberExpression;
639
+
640
+ const object = this.evaluate(member.object, environment);
641
+ if (!(object instanceof ObjectValue)) {
642
+ throw new Error("Cannot assign to member of non-object");
643
+ }
644
+ if (member.property.type !== "Identifier") {
645
+ throw new Error("Cannot assign to member with non-identifier property");
646
+ }
647
+ object.value.set((member.property as Identifier).value, rhs);
648
+ } else {
649
  throw new Error(`Invalid LHS inside assignment expression: ${JSON.stringify(node.assignee)}`);
650
  }
651
 
 
 
652
  return new NullValue();
653
  }
654
 
 
671
  for (let i = 0; i < iterable.value.length; ++i) {
672
  // Update the loop variable
673
  // TODO: Only create object once, then update value?
674
+ const loop = new Map([
675
+ ["index", new NumericValue(i + 1)],
676
+ ["index0", new NumericValue(i)],
677
+ ["revindex", new NumericValue(iterable.value.length - i)],
678
+ ["revindex0", new NumericValue(iterable.value.length - i - 1)],
679
+ ["first", new BooleanValue(i === 0)],
680
+ ["last", new BooleanValue(i === iterable.value.length - 1)],
681
+ ["length", new NumericValue(iterable.value.length)],
682
+ ["previtem", i > 0 ? iterable.value[i - 1] : new UndefinedValue()],
683
+ ["nextitem", i < iterable.value.length - 1 ? iterable.value[i + 1] : new UndefinedValue()],
684
+ ] as [string, AnyRuntimeValue][]);
685
+
686
+ scope.setVariable("loop", new ObjectValue(loop));
 
 
 
687
 
688
  // For this iteration, set the loop variable to the current element
689
  scope.setVariable(node.loopvar.value, iterable.value[i]);
 
732
  return this.evaluateBinaryExpression(statement as BinaryExpression, environment);
733
  case "FilterExpression":
734
  return this.evaluateFilterExpression(statement as FilterExpression, environment);
735
+ case "TestExpression":
736
+ return this.evaluateTestExpression(statement as TestExpression, environment);
737
 
738
  default:
739
  throw new SyntaxError(`Unknown node type: ${statement.type}`);
packages/jinja/src/utils.ts CHANGED
@@ -1,3 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
  * Function that mimics Python's array slicing.
3
  * @param array The array to slice.
@@ -23,3 +43,12 @@ export function slice<T>(array: T[], start?: number, stop?: number, step = 1): T
23
  }
24
  return result;
25
  }
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Function that mimics Python's range() function.
3
+ * @param start The start value of the range.
4
+ * @param stop The stop value of the range. If not provided, start will be 0 and stop will be the provided start value.
5
+ * @param step The step value of the range. Defaults to 1.
6
+ * @returns The range of numbers.
7
+ */
8
+ export function range(start: number, stop?: number, step = 1): number[] {
9
+ if (stop === undefined) {
10
+ stop = start;
11
+ start = 0;
12
+ }
13
+
14
+ const result: number[] = [];
15
+ for (let i = start; i < stop; i += step) {
16
+ result.push(i);
17
+ }
18
+ return result;
19
+ }
20
+
21
  /**
22
  * Function that mimics Python's array slicing.
23
  * @param array The array to slice.
 
43
  }
44
  return result;
45
  }
46
+
47
+ /**
48
+ * Function that mimics Python's string.title() function.
49
+ * @param value The string to title case.
50
+ * @returns The title cased string.
51
+ */
52
+ export function titleCase(value: string): string {
53
+ return value.replace(/\b\w/g, (c) => c.toUpperCase());
54
+ }
packages/jinja/test/e2e.test.js CHANGED
@@ -10,7 +10,7 @@ const EXAMPLE_CHAT = [
10
  { role: "user", content: "I'd like to show off how chat templating works!" },
11
  ];
12
 
13
- const EXAMPLE_CHAT_WITH_SYTEM = [
14
  {
15
  role: "system",
16
  content: "You are a friendly chatbot who always responds in the style of a pirate",
@@ -80,7 +80,7 @@ const TEST_DEFAULT_TEMPLATES = Object.freeze({
80
  // hf-internal-testing/llama-tokenizer
81
  chat_template: `{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif USE_DEFAULT_PROMPT == true and not '<<SYS>>' in messages[0]['content'] %}{% set loop_messages = messages %}{% set system_message = 'DEFAULT_SYSTEM_MESSAGE' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\\n' + system_message + '\\n<</SYS>>\\n\\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() + ' [/INST]' }}{% elif message['role'] == 'system' %}{{ '<<SYS>>\\n' + content.strip() + '\\n<</SYS>>\\n\\n' }}{% elif message['role'] == 'assistant' %}{{ ' ' + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}`,
82
  data: {
83
- messages: EXAMPLE_CHAT_WITH_SYTEM,
84
  bos_token: "<s>",
85
  eos_token: "</s>",
86
  USE_DEFAULT_PROMPT: true,
@@ -106,7 +106,7 @@ const TEST_CUSTOM_TEMPLATES = Object.freeze({
106
  "HuggingFaceH4/zephyr-7b-beta (add_generation_prompt=false)": {
107
  chat_template: `{% for message in messages %}\n{% if message['role'] == 'user' %}\n{{ '<|user|>\n' + message['content'] + eos_token }}\n{% elif message['role'] == 'system' %}\n{{ '<|system|>\n' + message['content'] + eos_token }}\n{% elif message['role'] == 'assistant' %}\n{{ '<|assistant|>\n' + message['content'] + eos_token }}\n{% endif %}\n{% if loop.last and add_generation_prompt %}\n{{ '<|assistant|>' }}\n{% endif %}\n{% endfor %}`,
108
  data: {
109
- messages: EXAMPLE_CHAT_WITH_SYTEM,
110
  eos_token: "</s>",
111
  add_generation_prompt: false,
112
  },
@@ -124,6 +124,16 @@ const TEST_CUSTOM_TEMPLATES = Object.freeze({
124
  },
125
  target: `<|system|>\nYou are a friendly chatbot who always responds in the style of a pirate</s>\n<|user|>\nHow many helicopters can a human eat in one sitting?</s>\n<|assistant|>\n`,
126
  },
 
 
 
 
 
 
 
 
 
 
127
  "mistralai/Mistral-7B-Instruct-v0.1": {
128
  chat_template: `{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token + ' ' }}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}`,
129
  data: {
@@ -133,6 +143,149 @@ const TEST_CUSTOM_TEMPLATES = Object.freeze({
133
  },
134
  target: `<s>[INST] Hello, how are you? [/INST]I'm doing great. How can I help you today?</s> [INST] I'd like to show off how chat templating works! [/INST]`,
135
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  });
137
 
138
  describe("End-to-end tests", () => {
 
10
  { role: "user", content: "I'd like to show off how chat templating works!" },
11
  ];
12
 
13
+ const EXAMPLE_CHAT_WITH_SYSTEM = [
14
  {
15
  role: "system",
16
  content: "You are a friendly chatbot who always responds in the style of a pirate",
 
80
  // hf-internal-testing/llama-tokenizer
81
  chat_template: `{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif USE_DEFAULT_PROMPT == true and not '<<SYS>>' in messages[0]['content'] %}{% set loop_messages = messages %}{% set system_message = 'DEFAULT_SYSTEM_MESSAGE' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\\n' + system_message + '\\n<</SYS>>\\n\\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() + ' [/INST]' }}{% elif message['role'] == 'system' %}{{ '<<SYS>>\\n' + content.strip() + '\\n<</SYS>>\\n\\n' }}{% elif message['role'] == 'assistant' %}{{ ' ' + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}`,
82
  data: {
83
+ messages: EXAMPLE_CHAT_WITH_SYSTEM,
84
  bos_token: "<s>",
85
  eos_token: "</s>",
86
  USE_DEFAULT_PROMPT: true,
 
106
  "HuggingFaceH4/zephyr-7b-beta (add_generation_prompt=false)": {
107
  chat_template: `{% for message in messages %}\n{% if message['role'] == 'user' %}\n{{ '<|user|>\n' + message['content'] + eos_token }}\n{% elif message['role'] == 'system' %}\n{{ '<|system|>\n' + message['content'] + eos_token }}\n{% elif message['role'] == 'assistant' %}\n{{ '<|assistant|>\n' + message['content'] + eos_token }}\n{% endif %}\n{% if loop.last and add_generation_prompt %}\n{{ '<|assistant|>' }}\n{% endif %}\n{% endfor %}`,
108
  data: {
109
+ messages: EXAMPLE_CHAT_WITH_SYSTEM,
110
  eos_token: "</s>",
111
  add_generation_prompt: false,
112
  },
 
124
  },
125
  target: `<|system|>\nYou are a friendly chatbot who always responds in the style of a pirate</s>\n<|user|>\nHow many helicopters can a human eat in one sitting?</s>\n<|assistant|>\n`,
126
  },
127
+ "HuggingFaceH4/zephyr-7b-gemma-v0.1": {
128
+ chat_template: `{% if messages[0]['role'] == 'user' or messages[0]['role'] == 'system' %}{{ bos_token }}{% endif %}{% for message in messages %}{{ '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n' }}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% elif messages[-1]['role'] == 'assistant' %}{{ eos_token }}{% endif %}`,
129
+ data: {
130
+ messages: EXAMPLE_CHAT,
131
+ bos_token: "<bos>",
132
+ eos_token: "<eos>",
133
+ add_generation_prompt: false,
134
+ },
135
+ target: `<bos><|im_start|>user\nHello, how are you?<|im_end|>\n<|im_start|>assistant\nI'm doing great. How can I help you today?<|im_end|>\n<|im_start|>user\nI'd like to show off how chat templating works!<|im_end|>\n`,
136
+ },
137
  "mistralai/Mistral-7B-Instruct-v0.1": {
138
  chat_template: `{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token + ' ' }}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}`,
139
  data: {
 
143
  },
144
  target: `<s>[INST] Hello, how are you? [/INST]I'm doing great. How can I help you today?</s> [INST] I'd like to show off how chat templating works! [/INST]`,
145
  },
146
+ "mistralai/Mixtral-8x7B-Instruct-v0.1": {
147
+ chat_template: `{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}`,
148
+ data: {
149
+ messages: EXAMPLE_CHAT,
150
+ bos_token: "<s>",
151
+ eos_token: "</s>",
152
+ },
153
+ target: `<s>[INST] Hello, how are you? [/INST]I'm doing great. How can I help you today?</s>[INST] I'd like to show off how chat templating works! [/INST]`,
154
+ },
155
+ "cognitivecomputations/dolphin-2.5-mixtral-8x7b": {
156
+ chat_template: `{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}`,
157
+ data: {
158
+ messages: EXAMPLE_CHAT,
159
+ bos_token: "<s>",
160
+ eos_token: "</s>",
161
+ },
162
+ target: `<|im_start|>user\nHello, how are you?<|im_end|>\n<|im_start|>assistant\nI'm doing great. How can I help you today?<|im_end|>\n<|im_start|>user\nI'd like to show off how chat templating works!<|im_end|>\n`,
163
+ },
164
+ "openchat/openchat-3.5-0106": {
165
+ chat_template: `{{ bos_token }}{% for message in messages %}{{ 'GPT4 Correct ' + message['role'].title() + ': ' + message['content'] + '<|end_of_turn|>'}}{% endfor %}{% if add_generation_prompt %}{{ 'GPT4 Correct Assistant:' }}{% endif %}`,
166
+ data: {
167
+ messages: EXAMPLE_CHAT,
168
+ bos_token: "<s>",
169
+ eos_token: "</s>",
170
+ add_generation_prompt: false,
171
+ },
172
+ target: `<s>GPT4 Correct User: Hello, how are you?<|end_of_turn|>GPT4 Correct Assistant: I'm doing great. How can I help you today?<|end_of_turn|>GPT4 Correct User: I'd like to show off how chat templating works!<|end_of_turn|>`,
173
+ },
174
+ "upstage/SOLAR-10.7B-Instruct-v1.0": {
175
+ chat_template: `{% for message in messages %}{% if message['role'] == 'system' %}{% if message['content']%}{{'### System:\n' + message['content']+'\n\n'}}{% endif %}{% elif message['role'] == 'user' %}{{'### User:\n' + message['content']+'\n\n'}}{% elif message['role'] == 'assistant' %}{{'### Assistant:\n' + message['content']}}{% endif %}{% if loop.last and add_generation_prompt %}{{ '### Assistant:\n' }}{% endif %}{% endfor %}`,
176
+ data: {
177
+ messages: EXAMPLE_CHAT,
178
+ bos_token: "<s>",
179
+ eos_token: "</s>",
180
+ add_generation_prompt: false,
181
+ },
182
+ target: `### User:\nHello, how are you?\n\n### Assistant:\nI'm doing great. How can I help you today?### User:\nI'd like to show off how chat templating works!\n\n`,
183
+ },
184
+ "codellama/CodeLlama-70b-Instruct-hf": {
185
+ chat_template: `{% if messages[0]['role'] == 'system' %}{% set user_index = 1 %}{% else %}{% set user_index = 0 %}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != ((loop.index0 + user_index) % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 %}{{ '<s>' }}{% endif %}{% set content = 'Source: ' + message['role'] + '\n\n ' + message['content'] | trim %}{{ content + ' <step> ' }}{% endfor %}{{'Source: assistant\nDestination: user\n\n '}}`,
186
+ data: {
187
+ messages: EXAMPLE_CHAT,
188
+ bos_token: "<s>",
189
+ eos_token: "</s>",
190
+ },
191
+ target: `<s>Source: user\n\n Hello, how are you? <step> Source: assistant\n\n I'm doing great. How can I help you today? <step> Source: user\n\n I'd like to show off how chat templating works! <step> Source: assistant\nDestination: user\n\n `,
192
+ },
193
+ "Deci/DeciLM-7B-instruct": {
194
+ chat_template: `{% for message in messages %}\n{% if message['role'] == 'user' %}\n{{ '### User:\n' + message['content'] }}\n{% elif message['role'] == 'system' %}\n{{ '### System:\n' + message['content'] }}\n{% elif message['role'] == 'assistant' %}\n{{ '### Assistant:\n' + message['content'] }}\n{% endif %}\n{% if loop.last and add_generation_prompt %}\n{{ '### Assistant:' }}\n{% endif %}\n{% endfor %}`,
195
+ data: {
196
+ messages: EXAMPLE_CHAT,
197
+ bos_token: "<s>",
198
+ eos_token: "</s>",
199
+ add_generation_prompt: false,
200
+ },
201
+ target: `### User:\nHello, how are you?\n### Assistant:\nI'm doing great. How can I help you today?\n### User:\nI'd like to show off how chat templating works!\n`,
202
+ },
203
+ "Qwen/Qwen1.5-72B-Chat": {
204
+ chat_template: `{% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system\nYou are a helpful assistant<|im_end|>\n' }}{% endif %}{{'<|im_start|>' + message['role'] + '\n' + message['content']}}{% if (loop.last and add_generation_prompt) or not loop.last %}{{ '<|im_end|>' + '\n'}}{% endif %}{% endfor %}{% if add_generation_prompt and messages[-1]['role'] != 'assistant' %}{{ '<|im_start|>assistant\n' }}{% endif %}`,
205
+ data: {
206
+ messages: EXAMPLE_CHAT,
207
+ bos_token: "<s>",
208
+ eos_token: "</s>",
209
+ add_generation_prompt: false,
210
+ },
211
+ target: `<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\nHello, how are you?<|im_end|>\n<|im_start|>assistant\nI'm doing great. How can I help you today?<|im_end|>\n<|im_start|>user\nI'd like to show off how chat templating works!`,
212
+ },
213
+ "deepseek-ai/deepseek-llm-7b-chat": {
214
+ chat_template: `{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}{{ 'User: ' + message['content'] + '\n\n' }}{% elif message['role'] == 'assistant' %}{{ 'Assistant: ' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ message['content'] + '\n\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'Assistant:' }}{% endif %}`,
215
+ data: {
216
+ messages: EXAMPLE_CHAT,
217
+ bos_token: "<|begin▁of▁sentence|>",
218
+ eos_token: "<|end▁of▁sentence|>",
219
+ },
220
+ target: `<|begin▁of▁sentence|>User: Hello, how are you?\n\nAssistant: I'm doing great. How can I help you today?<|end▁of▁sentence|>User: I'd like to show off how chat templating works!\n\n`,
221
+ },
222
+ "h2oai/h2o-danube-1.8b-chat": {
223
+ chat_template: `{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|prompt|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ '<|system|>' + message['content'] + eos_token }}{% elif message['role'] == 'assistant' %}{{ '<|answer|>' + message['content'] + eos_token }}{% endif %}{% if loop.last and add_generation_prompt %}{{ '<|answer|>' }}{% endif %}{% endfor %}`,
224
+ data: {
225
+ messages: EXAMPLE_CHAT,
226
+ bos_token: "<s>",
227
+ eos_token: "</s>",
228
+ add_generation_prompt: false,
229
+ },
230
+ target: `<|prompt|>Hello, how are you?</s><|answer|>I'm doing great. How can I help you today?</s><|prompt|>I'd like to show off how chat templating works!</s>`,
231
+ },
232
+ "internlm/internlm2-chat-7b": {
233
+ chat_template: `{% if messages[0]['role'] == 'user' or messages[0]['role'] == 'system' %}{{ bos_token }}{% endif %}{% for message in messages %}{{ '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n' }}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% elif messages[-1]['role'] == 'assistant' %}{{ eos_token }}{% endif %}`,
234
+ data: {
235
+ messages: EXAMPLE_CHAT,
236
+ bos_token: "<s>",
237
+ eos_token: "</s>",
238
+ add_generation_prompt: false,
239
+ },
240
+ target: `<s><|im_start|>user\nHello, how are you?<|im_end|>\n<|im_start|>assistant\nI'm doing great. How can I help you today?<|im_end|>\n<|im_start|>user\nI'd like to show off how chat templating works!<|im_end|>\n`,
241
+ },
242
+ "TheBloke/deepseek-coder-33B-instruct-AWQ": {
243
+ chat_template: `{%- set found_item = false -%}\n{%- for message in messages -%}\n {%- if message['role'] == 'system' -%}\n {%- set found_item = true -%}\n {%- endif -%}\n{%- endfor -%}\n{%- if not found_item -%}\n{{'You are an AI programming assistant, utilizing the Deepseek Coder model, developed by Deepseek Company, and you only answer questions related to computer science. For politically sensitive questions, security and privacy issues, and other non-computer science questions, you will refuse to answer.\\n'}}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'system' %}\n{{ message['content'] }}\n {%- else %}\n {%- if message['role'] == 'user' %}\n{{'### Instruction:\\n' + message['content'] + '\\n'}}\n {%- else %}\n{{'### Response:\\n' + message['content'] + '\\n<|EOT|>\\n'}}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{{'### Response:\\n'}}\n`,
244
+ data: {
245
+ messages: EXAMPLE_CHAT,
246
+ bos_token: "<|begin▁of▁sentence|>",
247
+ eos_token: "<|EOT|>",
248
+ },
249
+ target: `You are an AI programming assistant, utilizing the Deepseek Coder model, developed by Deepseek Company, and you only answer questions related to computer science. For politically sensitive questions, security and privacy issues, and other non-computer science questions, you will refuse to answer.\n### Instruction:\nHello, how are you?\n### Response:\nI'm doing great. How can I help you today?\n<|EOT|>\n### Instruction:\nI'd like to show off how chat templating works!\n### Response:\n`,
250
+ },
251
+ "ericzzz/falcon-rw-1b-chat": {
252
+ chat_template: `{% for message in messages %}{% if loop.index > 1 and loop.previtem['role'] != 'assistant' %}{{ ' ' }}{% endif %}{% if message['role'] == 'system' %}{{ '[SYS] ' + message['content'].strip() }}{% elif message['role'] == 'user' %}{{ '[INST] ' + message['content'].strip() }}{% elif message['role'] == 'assistant' %}{{ '[RESP] ' + message['content'] + eos_token }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ ' [RESP] ' }}{% endif %}`,
253
+ data: {
254
+ messages: EXAMPLE_CHAT,
255
+ bos_token: "<|endoftext|>",
256
+ eos_token: "<|endoftext|>",
257
+ add_generation_prompt: false,
258
+ },
259
+ target: `[INST] Hello, how are you? [RESP] I'm doing great. How can I help you today?<|endoftext|>[INST] I'd like to show off how chat templating works!`,
260
+ },
261
+ "abacusai/Smaug-34B-v0.1": {
262
+ chat_template: `{%- for idx in range(0, messages|length) -%}\n{%- if messages[idx]['role'] == 'user' -%}\n{%- if idx > 1 -%}\n{{- bos_token + '[INST] ' + messages[idx]['content'] + ' [/INST]' -}}\n{%- else -%}\n{{- messages[idx]['content'] + ' [/INST]' -}}\n{%- endif -%}\n{% elif messages[idx]['role'] == 'system' %}\n{{- '[INST] <<SYS>>\\n' + messages[idx]['content'] + '\\n<</SYS>>\\n\\n' -}}\n{%- elif messages[idx]['role'] == 'assistant' -%}\n{{- ' ' + messages[idx]['content'] + ' ' + eos_token -}}\n{% endif %}\n{% endfor %}`,
263
+ data: {
264
+ messages: EXAMPLE_CHAT,
265
+ bos_token: "<s>",
266
+ eos_token: "</s>",
267
+ },
268
+ target: `Hello, how are you? [/INST] I'm doing great. How can I help you today? </s><s>[INST] I'd like to show off how chat templating works! [/INST]`,
269
+ },
270
+ "maywell/Synatra-Mixtral-8x7B": {
271
+ chat_template: `Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n{% for message in messages %}{% if message['role'] == 'user' %}### Instruction:\n{{ message['content']|trim -}}{% if not loop.last %}{% endif %}\n{% elif message['role'] == 'assistant' %}### Response:\n{{ message['content']|trim -}}{% if not loop.last %}{% endif %}\n{% elif message['role'] == 'system' %}{{ message['content']|trim -}}{% if not loop.last %}{% endif %}\n{% endif %}\n{% endfor %}\n{% if add_generation_prompt and messages[-1]['role'] != 'assistant' %}\n### Response:\n{% endif %}`,
272
+ data: {
273
+ messages: EXAMPLE_CHAT,
274
+ bos_token: "<s>",
275
+ eos_token: "</s>",
276
+ add_generation_prompt: false,
277
+ },
278
+ target: `Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nHello, how are you?### Response:\nI'm doing great. How can I help you today?### Instruction:\nI'd like to show off how chat templating works!`,
279
+ },
280
+ "deepseek-ai/deepseek-coder-33b-instruct": {
281
+ chat_template: `{% if not add_generation_prompt is defined %}\n{% set add_generation_prompt = false %}\n{% endif %}\n{%- set ns = namespace(found=false) -%}\n{%- for message in messages -%}\n {%- if message['role'] == 'system' -%}\n {%- set ns.found = true -%}\n {%- endif -%}\n{%- endfor -%}\n{{bos_token}}{%- if not ns.found -%}\n{{'You are an AI programming assistant, utilizing the Deepseek Coder model, developed by Deepseek Company, and you only answer questions related to computer science. For politically sensitive questions, security and privacy issues, and other non-computer science questions, you will refuse to answer\\n'}}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'system' %}\n{{ message['content'] }}\n {%- else %}\n {%- if message['role'] == 'user' %}\n{{'### Instruction:\\n' + message['content'] + '\\n'}}\n {%- else %}\n{{'### Response:\\n' + message['content'] + '\\n<|EOT|>\\n'}}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{% if add_generation_prompt %}\n{{'### Response:'}}\n{% endif %}`,
282
+ data: {
283
+ messages: EXAMPLE_CHAT,
284
+ bos_token: "<|begin▁of▁sentence|>",
285
+ eos_token: "<|EOT|>",
286
+ },
287
+ target: `<|begin▁of▁sentence|>You are an AI programming assistant, utilizing the Deepseek Coder model, developed by Deepseek Company, and you only answer questions related to computer science. For politically sensitive questions, security and privacy issues, and other non-computer science questions, you will refuse to answer\n### Instruction:\nHello, how are you?\n### Response:\nI'm doing great. How can I help you today?\n<|EOT|>\n### Instruction:\nI'd like to show off how chat templating works!\n`,
288
+ },
289
  });
290
 
291
  describe("End-to-end tests", () => {
packages/jinja/test/interpreter.test.js ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { Environment, Interpreter } from "../src/runtime";
4
+ import { tokenize } from "../src/lexer";
5
+ import { parse } from "../src/parser";
6
+
7
+ describe("Test interpreter options", () => {
8
+ // https://jinja.palletsprojects.com/en/3.0.x/templates/#whitespace-control
9
+ it("should handle whitespace control", () => {
10
+ const EXAMPLE_IF_TEMPLATE = `<div>\n {% if True %}\n yay\n {% endif %}\n</div>`;
11
+ const EXAMPLE_FOR_TEMPLATE = `{% for item in seq %}\n {{ item }}\n{% endfor %}`;
12
+ const EXAMPLE_FOR_TEMPLATE_2 = `{% for item in seq -%}\n {{ item }}\n{% endfor %}`;
13
+ const EXAMPLE_FOR_TEMPLATE_3 = `{% for item in seq %}\n {{ item }}\n{%- endfor %}`;
14
+ const EXAMPLE_FOR_TEMPLATE_4 = `{% for item in seq -%}\n {{ item }}\n{%- endfor %}`;
15
+
16
+ const seq = [1, 2, 3, 4, 5, 6, 7, 8, 9];
17
+
18
+ const TESTS = [
19
+ // If tests
20
+ {
21
+ template: EXAMPLE_IF_TEMPLATE,
22
+ data: {},
23
+ options: {},
24
+ target: `<div>\n \n yay\n \n</div>`,
25
+ },
26
+ {
27
+ template: EXAMPLE_IF_TEMPLATE,
28
+ data: {},
29
+ options: {
30
+ lstrip_blocks: true,
31
+ },
32
+ target: `<div>\n\n yay\n\n</div>`,
33
+ },
34
+ {
35
+ template: EXAMPLE_IF_TEMPLATE,
36
+ data: {},
37
+ options: {
38
+ trim_blocks: true,
39
+ },
40
+ target: `<div>\n yay\n </div>`,
41
+ },
42
+ {
43
+ template: EXAMPLE_IF_TEMPLATE,
44
+ data: {},
45
+ options: {
46
+ lstrip_blocks: true,
47
+ trim_blocks: true,
48
+ },
49
+ target: `<div>\n yay\n</div>`,
50
+ },
51
+
52
+ // For tests
53
+ {
54
+ template: EXAMPLE_FOR_TEMPLATE,
55
+ data: { seq },
56
+ options: {},
57
+ target: `\n 1\n\n 2\n\n 3\n\n 4\n\n 5\n\n 6\n\n 7\n\n 8\n\n 9\n`,
58
+ },
59
+ {
60
+ template: EXAMPLE_FOR_TEMPLATE,
61
+ data: { seq },
62
+ options: { lstrip_blocks: true },
63
+ target: `\n 1\n\n 2\n\n 3\n\n 4\n\n 5\n\n 6\n\n 7\n\n 8\n\n 9\n`,
64
+ },
65
+ {
66
+ template: EXAMPLE_FOR_TEMPLATE,
67
+ data: { seq },
68
+ options: { trim_blocks: true },
69
+ target: ` 1\n 2\n 3\n 4\n 5\n 6\n 7\n 8\n 9\n`,
70
+ },
71
+ {
72
+ template: EXAMPLE_FOR_TEMPLATE,
73
+ data: { seq },
74
+ options: { lstrip_blocks: true, trim_blocks: true },
75
+ target: ` 1\n 2\n 3\n 4\n 5\n 6\n 7\n 8\n 9\n`,
76
+ },
77
+ {
78
+ template: EXAMPLE_FOR_TEMPLATE_2,
79
+ data: { seq },
80
+ options: {},
81
+ target: `1\n2\n3\n4\n5\n6\n7\n8\n9\n`,
82
+ },
83
+ {
84
+ template: EXAMPLE_FOR_TEMPLATE_3,
85
+ data: { seq },
86
+ options: {},
87
+ target: `\n 1\n 2\n 3\n 4\n 5\n 6\n 7\n 8\n 9`,
88
+ },
89
+ {
90
+ template: EXAMPLE_FOR_TEMPLATE_3,
91
+ data: { seq },
92
+ options: { trim_blocks: true },
93
+ target: ` 1 2 3 4 5 6 7 8 9`,
94
+ },
95
+ {
96
+ template: EXAMPLE_FOR_TEMPLATE_4,
97
+ data: { seq },
98
+ options: {},
99
+ target: `123456789`,
100
+ },
101
+ ];
102
+
103
+ for (const test of TESTS) {
104
+ const env = new Environment();
105
+ env.set("True", true);
106
+ for (const [key, value] of Object.entries(test.data)) {
107
+ env.set(key, value);
108
+ }
109
+
110
+ const tokens = tokenize(test.template, test.options);
111
+ const parsed = parse(tokens);
112
+
113
+ const interpreter = new Interpreter(env);
114
+ const result = interpreter.run(parsed);
115
+ expect(result.value).toEqual(test.target);
116
+ }
117
+ });
118
+ });
packages/jinja/test/templates.test.js CHANGED
@@ -49,6 +49,7 @@ const TEST_STRINGS = {
49
  // Object methods
50
  OBJ_METHODS: `{{ obj.x(x, y) }}{{ ' ' + obj.x() + ' ' }}{{ obj.z[x](x, y) }}`,
51
  STRING_METHODS: `{{ ' A '.strip() }}{% set x = ' B ' %}{{ x.strip() }}{% set y = ' aBcD ' %}{{ y.upper() }}{{ y.lower() }}`,
 
52
 
53
  // String indexing and slicing
54
  STRING_SLICING: `|{{ x[0] }}|{{ x[:] }}|{{ x[:3] }}|{{ x[1:4] }}|{{ x[1:-1] }}|{{ x[1::2] }}|{{ x[5::-1] }}|`,
@@ -78,6 +79,22 @@ const TEST_STRINGS = {
78
  BOOLEAN_MIXED: `|{{ true and 1 }}|{{ true and 0 }}|{{ false and 1 }}|{{ false and 0 }}|{{ true or 1 }}|{{ true or 0 }}|{{ false or 1 }}|{{ false or 0 }}|`,
79
  BOOLEAN_MIXED_2: `|{{ true and '' }}|{{ true and 'a' }}|{{ false or '' }}|{{ false or 'a' }}|{{ '' and true }}|{{ 'a' and true }}|{{ '' or false }}|{{ 'a' or false }}|`,
80
  BOOLEAN_MIXED_IF: `{% if '' %}{{ 'A' }}{% endif %}{% if 'a' %}{{ 'B' }}{% endif %}{% if true and '' %}{{ 'C' }}{% endif %}{% if true and 'a' %}{{ 'D' }}{% endif %}`,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  };
82
 
83
  const TEST_PARSED = {
@@ -728,6 +745,15 @@ const TEST_PARSED = {
728
  { value: ")", type: "CloseParen" },
729
  { value: "}}", type: "CloseExpression" },
730
  ],
 
 
 
 
 
 
 
 
 
731
 
732
  // String indexing and slicing
733
  STRING_SLICING: [
@@ -1477,6 +1503,280 @@ const TEST_PARSED = {
1477
  { value: "endif", type: "EndIf" },
1478
  { value: "%}", type: "CloseStatement" },
1479
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1480
  };
1481
 
1482
  const TEST_CONTEXT = {
@@ -1552,6 +1852,7 @@ const TEST_CONTEXT = {
1552
 
1553
  // String methods
1554
  STRING_METHODS: {},
 
1555
 
1556
  // String indexing and slicing
1557
  STRING_SLICING: {
@@ -1593,6 +1894,26 @@ const TEST_CONTEXT = {
1593
  BOOLEAN_MIXED: {},
1594
  BOOLEAN_MIXED_2: {},
1595
  BOOLEAN_MIXED_IF: {},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1596
  };
1597
 
1598
  const EXPECTED_OUTPUTS = {
@@ -1640,6 +1961,7 @@ const EXPECTED_OUTPUTS = {
1640
  // Object methods
1641
  OBJ_METHODS: "AB A_B",
1642
  STRING_METHODS: "AB ABCD abcd ",
 
1643
 
1644
  // String indexing and slicing
1645
  STRING_SLICING: "|0|0123456789|012|123|12345678|13579|543210|",
@@ -1653,9 +1975,7 @@ const EXPECTED_OUTPUTS = {
1653
  MEMBERSHIP_NEGATION_2: "|false|true|false|true|false|true|",
1654
 
1655
  // Escaped characters
1656
- // NOTE: Since `trim_blocks` is enabled, we remove the first newline after the template tag,
1657
- // meaning the first newline in the output is not present
1658
- ESCAPED_CHARS: `\t'"\\|\n|\t|'|"|\\|`,
1659
 
1660
  // Substring inclusion
1661
  SUBSTRING_INCLUSION: `|true|true|false|true|false|true|false|`,
@@ -1671,6 +1991,22 @@ const EXPECTED_OUTPUTS = {
1671
  BOOLEAN_MIXED: `|1|0|false|false|true|true|1|0|`,
1672
  BOOLEAN_MIXED_2: `||a||a||true|false|a|`,
1673
  BOOLEAN_MIXED_IF: `BD`,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1674
  };
1675
 
1676
  describe("Templates", () => {
 
49
  // Object methods
50
  OBJ_METHODS: `{{ obj.x(x, y) }}{{ ' ' + obj.x() + ' ' }}{{ obj.z[x](x, y) }}`,
51
  STRING_METHODS: `{{ ' A '.strip() }}{% set x = ' B ' %}{{ x.strip() }}{% set y = ' aBcD ' %}{{ y.upper() }}{{ y.lower() }}`,
52
+ STRING_METHODS_2: `{{ 'test test'.title() }}`,
53
 
54
  // String indexing and slicing
55
  STRING_SLICING: `|{{ x[0] }}|{{ x[:] }}|{{ x[:3] }}|{{ x[1:4] }}|{{ x[1:-1] }}|{{ x[1::2] }}|{{ x[5::-1] }}|`,
 
79
  BOOLEAN_MIXED: `|{{ true and 1 }}|{{ true and 0 }}|{{ false and 1 }}|{{ false and 0 }}|{{ true or 1 }}|{{ true or 0 }}|{{ false or 1 }}|{{ false or 0 }}|`,
80
  BOOLEAN_MIXED_2: `|{{ true and '' }}|{{ true and 'a' }}|{{ false or '' }}|{{ false or 'a' }}|{{ '' and true }}|{{ 'a' and true }}|{{ '' or false }}|{{ 'a' or false }}|`,
81
  BOOLEAN_MIXED_IF: `{% if '' %}{{ 'A' }}{% endif %}{% if 'a' %}{{ 'B' }}{% endif %}{% if true and '' %}{{ 'C' }}{% endif %}{% if true and 'a' %}{{ 'D' }}{% endif %}`,
82
+
83
+ // Tests (is operator)
84
+ IS_OPERATOR: `|{{ unknown_var is defined }}|{{ unknown_var is not defined }}|{{ known_var is defined }}|{{ known_var is not defined }}|`,
85
+ IS_OPERATOR_2: `|{{ true is true }}|{{ true is not true }}|{{ true is false }}|{{ true is not false }}|{{ true is boolean }}|{{ 1 is boolean }}|`,
86
+ IS_OPERATOR_3: `|{{ 1 is odd }}|{{ 2 is odd }}|{{ 1 is even }}|{{ 2 is even }}|{{ 2 is number }}|{{ '2' is number }}|{{ 2 is integer }}|{{ '2' is integer }}|`,
87
+ IS_OPERATOR_4: `|{{ func is callable }}|{{ 2 is callable }}|{{ 1 is iterable }}|{{ 'hello' is iterable }}|`,
88
+ IS_OPERATOR_5: `|{{ 'a' is lower }}|{{ 'A' is lower }}|{{ 'a' is upper }}|{{ 'A' is upper }}|`,
89
+
90
+ // Short-circuit evaluation
91
+ SHORT_CIRCUIT: `{{ false and raise_exception('This should not be printed') }}`,
92
+ SHORT_CIRCUIT_1: `{{ true or raise_exception('This should not be printed') }}`,
93
+
94
+ // Namespaces
95
+ NAMESPACE: `{% set ns = namespace() %}{% set ns.foo = 'bar' %}{{ ns.foo }}`,
96
+ NAMESPACE_1: `{% set ns = namespace(default=false) %}{{ ns.default }}`,
97
+ NAMESPACE_2: `{% set ns = namespace(default=false, number=1+1) %}|{{ ns.default }}|{{ ns.number }}|`,
98
  };
99
 
100
  const TEST_PARSED = {
 
745
  { value: ")", type: "CloseParen" },
746
  { value: "}}", type: "CloseExpression" },
747
  ],
748
+ STRING_METHODS_2: [
749
+ { value: "{{", type: "OpenExpression" },
750
+ { value: "test test", type: "StringLiteral" },
751
+ { value: ".", type: "Dot" },
752
+ { value: "title", type: "Identifier" },
753
+ { value: "(", type: "OpenParen" },
754
+ { value: ")", type: "CloseParen" },
755
+ { value: "}}", type: "CloseExpression" },
756
+ ],
757
 
758
  // String indexing and slicing
759
  STRING_SLICING: [
 
1503
  { value: "endif", type: "EndIf" },
1504
  { value: "%}", type: "CloseStatement" },
1505
  ],
1506
+
1507
+ // Tests (is operator)
1508
+ IS_OPERATOR: [
1509
+ { value: "|", type: "Text" },
1510
+ { value: "{{", type: "OpenExpression" },
1511
+ { value: "unknown_var", type: "Identifier" },
1512
+ { value: "is", type: "Is" },
1513
+ { value: "defined", type: "Identifier" },
1514
+ { value: "}}", type: "CloseExpression" },
1515
+ { value: "|", type: "Text" },
1516
+ { value: "{{", type: "OpenExpression" },
1517
+ { value: "unknown_var", type: "Identifier" },
1518
+ { value: "is", type: "Is" },
1519
+ { value: "not", type: "UnaryOperator" },
1520
+ { value: "defined", type: "Identifier" },
1521
+ { value: "}}", type: "CloseExpression" },
1522
+ { value: "|", type: "Text" },
1523
+ { value: "{{", type: "OpenExpression" },
1524
+ { value: "known_var", type: "Identifier" },
1525
+ { value: "is", type: "Is" },
1526
+ { value: "defined", type: "Identifier" },
1527
+ { value: "}}", type: "CloseExpression" },
1528
+ { value: "|", type: "Text" },
1529
+ { value: "{{", type: "OpenExpression" },
1530
+ { value: "known_var", type: "Identifier" },
1531
+ { value: "is", type: "Is" },
1532
+ { value: "not", type: "UnaryOperator" },
1533
+ { value: "defined", type: "Identifier" },
1534
+ { value: "}}", type: "CloseExpression" },
1535
+ { value: "|", type: "Text" },
1536
+ ],
1537
+ IS_OPERATOR_2: [
1538
+ { value: "|", type: "Text" },
1539
+ { value: "{{", type: "OpenExpression" },
1540
+ { value: "true", type: "BooleanLiteral" },
1541
+ { value: "is", type: "Is" },
1542
+ { value: "true", type: "BooleanLiteral" },
1543
+ { value: "}}", type: "CloseExpression" },
1544
+ { value: "|", type: "Text" },
1545
+ { value: "{{", type: "OpenExpression" },
1546
+ { value: "true", type: "BooleanLiteral" },
1547
+ { value: "is", type: "Is" },
1548
+ { value: "not", type: "UnaryOperator" },
1549
+ { value: "true", type: "BooleanLiteral" },
1550
+ { value: "}}", type: "CloseExpression" },
1551
+ { value: "|", type: "Text" },
1552
+ { value: "{{", type: "OpenExpression" },
1553
+ { value: "true", type: "BooleanLiteral" },
1554
+ { value: "is", type: "Is" },
1555
+ { value: "false", type: "BooleanLiteral" },
1556
+ { value: "}}", type: "CloseExpression" },
1557
+ { value: "|", type: "Text" },
1558
+ { value: "{{", type: "OpenExpression" },
1559
+ { value: "true", type: "BooleanLiteral" },
1560
+ { value: "is", type: "Is" },
1561
+ { value: "not", type: "UnaryOperator" },
1562
+ { value: "false", type: "BooleanLiteral" },
1563
+ { value: "}}", type: "CloseExpression" },
1564
+ { value: "|", type: "Text" },
1565
+ { value: "{{", type: "OpenExpression" },
1566
+ { value: "true", type: "BooleanLiteral" },
1567
+ { value: "is", type: "Is" },
1568
+ { value: "boolean", type: "Identifier" },
1569
+ { value: "}}", type: "CloseExpression" },
1570
+ { value: "|", type: "Text" },
1571
+ { value: "{{", type: "OpenExpression" },
1572
+ { value: "1", type: "NumericLiteral" },
1573
+ { value: "is", type: "Is" },
1574
+ { value: "boolean", type: "Identifier" },
1575
+ { value: "}}", type: "CloseExpression" },
1576
+ { value: "|", type: "Text" },
1577
+ ],
1578
+ IS_OPERATOR_3: [
1579
+ { value: "|", type: "Text" },
1580
+ { value: "{{", type: "OpenExpression" },
1581
+ { value: "1", type: "NumericLiteral" },
1582
+ { value: "is", type: "Is" },
1583
+ { value: "odd", type: "Identifier" },
1584
+ { value: "}}", type: "CloseExpression" },
1585
+ { value: "|", type: "Text" },
1586
+ { value: "{{", type: "OpenExpression" },
1587
+ { value: "2", type: "NumericLiteral" },
1588
+ { value: "is", type: "Is" },
1589
+ { value: "odd", type: "Identifier" },
1590
+ { value: "}}", type: "CloseExpression" },
1591
+ { value: "|", type: "Text" },
1592
+ { value: "{{", type: "OpenExpression" },
1593
+ { value: "1", type: "NumericLiteral" },
1594
+ { value: "is", type: "Is" },
1595
+ { value: "even", type: "Identifier" },
1596
+ { value: "}}", type: "CloseExpression" },
1597
+ { value: "|", type: "Text" },
1598
+ { value: "{{", type: "OpenExpression" },
1599
+ { value: "2", type: "NumericLiteral" },
1600
+ { value: "is", type: "Is" },
1601
+ { value: "even", type: "Identifier" },
1602
+ { value: "}}", type: "CloseExpression" },
1603
+ { value: "|", type: "Text" },
1604
+ { value: "{{", type: "OpenExpression" },
1605
+ { value: "2", type: "NumericLiteral" },
1606
+ { value: "is", type: "Is" },
1607
+ { value: "number", type: "Identifier" },
1608
+ { value: "}}", type: "CloseExpression" },
1609
+ { value: "|", type: "Text" },
1610
+ { value: "{{", type: "OpenExpression" },
1611
+ { value: "2", type: "StringLiteral" },
1612
+ { value: "is", type: "Is" },
1613
+ { value: "number", type: "Identifier" },
1614
+ { value: "}}", type: "CloseExpression" },
1615
+ { value: "|", type: "Text" },
1616
+ { value: "{{", type: "OpenExpression" },
1617
+ { value: "2", type: "NumericLiteral" },
1618
+ { value: "is", type: "Is" },
1619
+ { value: "integer", type: "Identifier" },
1620
+ { value: "}}", type: "CloseExpression" },
1621
+ { value: "|", type: "Text" },
1622
+ { value: "{{", type: "OpenExpression" },
1623
+ { value: "2", type: "StringLiteral" },
1624
+ { value: "is", type: "Is" },
1625
+ { value: "integer", type: "Identifier" },
1626
+ { value: "}}", type: "CloseExpression" },
1627
+ { value: "|", type: "Text" },
1628
+ ],
1629
+ IS_OPERATOR_4: [
1630
+ { value: "|", type: "Text" },
1631
+ { value: "{{", type: "OpenExpression" },
1632
+ { value: "func", type: "Identifier" },
1633
+ { value: "is", type: "Is" },
1634
+ { value: "callable", type: "Identifier" },
1635
+ { value: "}}", type: "CloseExpression" },
1636
+ { value: "|", type: "Text" },
1637
+ { value: "{{", type: "OpenExpression" },
1638
+ { value: "2", type: "NumericLiteral" },
1639
+ { value: "is", type: "Is" },
1640
+ { value: "callable", type: "Identifier" },
1641
+ { value: "}}", type: "CloseExpression" },
1642
+ { value: "|", type: "Text" },
1643
+ { value: "{{", type: "OpenExpression" },
1644
+ { value: "1", type: "NumericLiteral" },
1645
+ { value: "is", type: "Is" },
1646
+ { value: "iterable", type: "Identifier" },
1647
+ { value: "}}", type: "CloseExpression" },
1648
+ { value: "|", type: "Text" },
1649
+ { value: "{{", type: "OpenExpression" },
1650
+ { value: "hello", type: "StringLiteral" },
1651
+ { value: "is", type: "Is" },
1652
+ { value: "iterable", type: "Identifier" },
1653
+ { value: "}}", type: "CloseExpression" },
1654
+ { value: "|", type: "Text" },
1655
+ ],
1656
+ IS_OPERATOR_5: [
1657
+ { value: "|", type: "Text" },
1658
+ { value: "{{", type: "OpenExpression" },
1659
+ { value: "a", type: "StringLiteral" },
1660
+ { value: "is", type: "Is" },
1661
+ { value: "lower", type: "Identifier" },
1662
+ { value: "}}", type: "CloseExpression" },
1663
+ { value: "|", type: "Text" },
1664
+ { value: "{{", type: "OpenExpression" },
1665
+ { value: "A", type: "StringLiteral" },
1666
+ { value: "is", type: "Is" },
1667
+ { value: "lower", type: "Identifier" },
1668
+ { value: "}}", type: "CloseExpression" },
1669
+ { value: "|", type: "Text" },
1670
+ { value: "{{", type: "OpenExpression" },
1671
+ { value: "a", type: "StringLiteral" },
1672
+ { value: "is", type: "Is" },
1673
+ { value: "upper", type: "Identifier" },
1674
+ { value: "}}", type: "CloseExpression" },
1675
+ { value: "|", type: "Text" },
1676
+ { value: "{{", type: "OpenExpression" },
1677
+ { value: "A", type: "StringLiteral" },
1678
+ { value: "is", type: "Is" },
1679
+ { value: "upper", type: "Identifier" },
1680
+ { value: "}}", type: "CloseExpression" },
1681
+ { value: "|", type: "Text" },
1682
+ ],
1683
+
1684
+ // Short-circuit evaluation
1685
+ SHORT_CIRCUIT: [
1686
+ { value: "{{", type: "OpenExpression" },
1687
+ { value: "false", type: "BooleanLiteral" },
1688
+ { value: "and", type: "And" },
1689
+ { value: "raise_exception", type: "Identifier" },
1690
+ { value: "(", type: "OpenParen" },
1691
+ { value: "This should not be printed", type: "StringLiteral" },
1692
+ { value: ")", type: "CloseParen" },
1693
+ { value: "}}", type: "CloseExpression" },
1694
+ ],
1695
+ SHORT_CIRCUIT_1: [
1696
+ { value: "{{", type: "OpenExpression" },
1697
+ { value: "true", type: "BooleanLiteral" },
1698
+ { value: "or", type: "Or" },
1699
+ { value: "raise_exception", type: "Identifier" },
1700
+ { value: "(", type: "OpenParen" },
1701
+ { value: "This should not be printed", type: "StringLiteral" },
1702
+ { value: ")", type: "CloseParen" },
1703
+ { value: "}}", type: "CloseExpression" },
1704
+ ],
1705
+
1706
+ // Namespaces
1707
+ NAMESPACE: [
1708
+ { value: "{%", type: "OpenStatement" },
1709
+ { value: "set", type: "Set" },
1710
+ { value: "ns", type: "Identifier" },
1711
+ { value: "=", type: "Equals" },
1712
+ { value: "namespace", type: "Identifier" },
1713
+ { value: "(", type: "OpenParen" },
1714
+ { value: ")", type: "CloseParen" },
1715
+ { value: "%}", type: "CloseStatement" },
1716
+ { value: "{%", type: "OpenStatement" },
1717
+ { value: "set", type: "Set" },
1718
+ { value: "ns", type: "Identifier" },
1719
+ { value: ".", type: "Dot" },
1720
+ { value: "foo", type: "Identifier" },
1721
+ { value: "=", type: "Equals" },
1722
+ { value: "bar", type: "StringLiteral" },
1723
+ { value: "%}", type: "CloseStatement" },
1724
+ { value: "{{", type: "OpenExpression" },
1725
+ { value: "ns", type: "Identifier" },
1726
+ { value: ".", type: "Dot" },
1727
+ { value: "foo", type: "Identifier" },
1728
+ { value: "}}", type: "CloseExpression" },
1729
+ ],
1730
+ NAMESPACE_1: [
1731
+ { value: "{%", type: "OpenStatement" },
1732
+ { value: "set", type: "Set" },
1733
+ { value: "ns", type: "Identifier" },
1734
+ { value: "=", type: "Equals" },
1735
+ { value: "namespace", type: "Identifier" },
1736
+ { value: "(", type: "OpenParen" },
1737
+ { value: "default", type: "Identifier" },
1738
+ { value: "=", type: "Equals" },
1739
+ { value: "false", type: "BooleanLiteral" },
1740
+ { value: ")", type: "CloseParen" },
1741
+ { value: "%}", type: "CloseStatement" },
1742
+ { value: "{{", type: "OpenExpression" },
1743
+ { value: "ns", type: "Identifier" },
1744
+ { value: ".", type: "Dot" },
1745
+ { value: "default", type: "Identifier" },
1746
+ { value: "}}", type: "CloseExpression" },
1747
+ ],
1748
+ NAMESPACE_2: [
1749
+ { value: "{%", type: "OpenStatement" },
1750
+ { value: "set", type: "Set" },
1751
+ { value: "ns", type: "Identifier" },
1752
+ { value: "=", type: "Equals" },
1753
+ { value: "namespace", type: "Identifier" },
1754
+ { value: "(", type: "OpenParen" },
1755
+ { value: "default", type: "Identifier" },
1756
+ { value: "=", type: "Equals" },
1757
+ { value: "false", type: "BooleanLiteral" },
1758
+ { value: ",", type: "Comma" },
1759
+ { value: "number", type: "Identifier" },
1760
+ { value: "=", type: "Equals" },
1761
+ { value: "1", type: "NumericLiteral" },
1762
+ { value: "+", type: "AdditiveBinaryOperator" },
1763
+ { value: "1", type: "NumericLiteral" },
1764
+ { value: ")", type: "CloseParen" },
1765
+ { value: "%}", type: "CloseStatement" },
1766
+ { value: "|", type: "Text" },
1767
+ { value: "{{", type: "OpenExpression" },
1768
+ { value: "ns", type: "Identifier" },
1769
+ { value: ".", type: "Dot" },
1770
+ { value: "default", type: "Identifier" },
1771
+ { value: "}}", type: "CloseExpression" },
1772
+ { value: "|", type: "Text" },
1773
+ { value: "{{", type: "OpenExpression" },
1774
+ { value: "ns", type: "Identifier" },
1775
+ { value: ".", type: "Dot" },
1776
+ { value: "number", type: "Identifier" },
1777
+ { value: "}}", type: "CloseExpression" },
1778
+ { value: "|", type: "Text" },
1779
+ ],
1780
  };
1781
 
1782
  const TEST_CONTEXT = {
 
1852
 
1853
  // String methods
1854
  STRING_METHODS: {},
1855
+ STRING_METHODS_2: {},
1856
 
1857
  // String indexing and slicing
1858
  STRING_SLICING: {
 
1894
  BOOLEAN_MIXED: {},
1895
  BOOLEAN_MIXED_2: {},
1896
  BOOLEAN_MIXED_IF: {},
1897
+
1898
+ // Tests (is operator)
1899
+ IS_OPERATOR: {
1900
+ known_var: "Hello World",
1901
+ },
1902
+ IS_OPERATOR_2: {},
1903
+ IS_OPERATOR_3: {},
1904
+ IS_OPERATOR_4: {
1905
+ func: () => {},
1906
+ },
1907
+ IS_OPERATOR_5: {},
1908
+
1909
+ // Short-circuit evaluation
1910
+ SHORT_CIRCUIT: {},
1911
+ SHORT_CIRCUIT_1: {},
1912
+
1913
+ // Namespaces
1914
+ NAMESPACE: {},
1915
+ NAMESPACE_1: {},
1916
+ NAMESPACE_2: {},
1917
  };
1918
 
1919
  const EXPECTED_OUTPUTS = {
 
1961
  // Object methods
1962
  OBJ_METHODS: "AB A_B",
1963
  STRING_METHODS: "AB ABCD abcd ",
1964
+ STRING_METHODS_2: "Test Test",
1965
 
1966
  // String indexing and slicing
1967
  STRING_SLICING: "|0|0123456789|012|123|12345678|13579|543210|",
 
1975
  MEMBERSHIP_NEGATION_2: "|false|true|false|true|false|true|",
1976
 
1977
  // Escaped characters
1978
+ ESCAPED_CHARS: `\n\t'"\\|\n|\t|'|"|\\|`,
 
 
1979
 
1980
  // Substring inclusion
1981
  SUBSTRING_INCLUSION: `|true|true|false|true|false|true|false|`,
 
1991
  BOOLEAN_MIXED: `|1|0|false|false|true|true|1|0|`,
1992
  BOOLEAN_MIXED_2: `||a||a||true|false|a|`,
1993
  BOOLEAN_MIXED_IF: `BD`,
1994
+
1995
+ // Tests (is operator)
1996
+ IS_OPERATOR: `|false|true|true|false|`,
1997
+ IS_OPERATOR_2: `|true|false|false|true|true|false|`,
1998
+ IS_OPERATOR_3: `|true|false|false|true|true|false|true|false|`,
1999
+ IS_OPERATOR_4: `|true|false|false|true|`,
2000
+ IS_OPERATOR_5: `|true|false|false|true|`,
2001
+
2002
+ // Short-circuit evaluation
2003
+ SHORT_CIRCUIT: `false`,
2004
+ SHORT_CIRCUIT_1: `true`,
2005
+
2006
+ // Namespaces
2007
+ NAMESPACE: `bar`,
2008
+ NAMESPACE_1: `false`,
2009
+ NAMESPACE_2: `|false|2|`,
2010
  };
2011
 
2012
  describe("Templates", () => {
packages/tasks/package.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "@huggingface/tasks",
3
  "packageManager": "pnpm@8.10.5",
4
- "version": "0.5.1",
5
  "description": "List of ML tasks for huggingface.co/tasks",
6
  "repository": "https://github.com/huggingface/huggingface.js.git",
7
  "publishConfig": {
 
1
  {
2
  "name": "@huggingface/tasks",
3
  "packageManager": "pnpm@8.10.5",
4
+ "version": "0.5.2",
5
  "description": "List of ML tasks for huggingface.co/tasks",
6
  "repository": "https://github.com/huggingface/huggingface.js.git",
7
  "publishConfig": {