Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.3.2

- Add `strftimeNow(DateTime)` for deterministic per-render template clocks, preserving the built-in function's validation and input escaping.

## 1.3.1

- Fixed `\xHH` hexadecimal escapes in template strings, including Flint's chat template.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,7 @@ dinja implements the Jinja that chat templates use, not all of Jinja2:
- Some Jinja2 features llama.cpp lacks are missing here too; for example, `'%s'|format(x)` returns `%s`.
- String literals support `\xHH` with exactly two hexadecimal digits, as in `{{ '\x41\xE9' }}` rendering `Aé`. Values above `0x7f` are Unicode characters, as in llama.cpp; `\\x41` keeps the literal text `\x41`.
- `strftime_now` formats the system clock's current time in the local time zone, as C `strftime` does in llama.cpp. `%Z` is Dart's `DateTime.timeZoneName`, whose format depends on the platform: on the web it is the browser's name for the zone, such as `Eastern Daylight Time` where llama.cpp gives `EDT`.
To supply a deterministic clock for one render, pass
`{'strftime_now': strftimeNow(DateTime.utc(2026, 1, 2))}` to `render`.
The callable keeps the built-in argument/output checks and input escaping;
other renders retain their own clock.
1 change: 1 addition & 0 deletions lib/dinja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
library;

export 'src/parser.dart' show ParserException;
export 'src/runtime/builtins.dart' show strftimeNow;
export 'src/template.dart';
export 'src/types/jinja_string.dart';
export 'src/types/value.dart';
20 changes: 18 additions & 2 deletions lib/src/runtime/builtins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2081,18 +2081,34 @@ JinjaValue _string(List<JinjaValue> args, Map<String, JinjaValue> kwargs) {
return _asString(args);
}

/// Creates a `strftime_now` callable that formats [now] instead of wall time.
///
/// Pass it as the `strftime_now` value to `Template.render` for a deterministic
/// render. Other renders keep the default clock. Argument validation, output
/// limits and input escaping match the built-in function.
JinjaFunction strftimeNow(DateTime now) => JinjaFunction('strftime_now', (
args,
kwargs,
) {
// A context callable is caller-supplied. Preserve rendered input before
// its result is registered as supplied, as the built-in call does.
return deriveRendered(_strftimeAt(args, now), [...args, ...kwargs.values]);
});

JinjaValue _strftime_now(
List<JinjaValue> args,
Map<String, JinjaValue> kwargs,
) {
) => _strftimeAt(args, DateTime.now());

JinjaValue _strftimeAt(List<JinjaValue> args, DateTime now) {
if (args.isEmpty) {
throw Exception('strftime_now expects a format string');
}
final format = args[0];
if (format is! JinjaStringValue) {
throw Exception('strftime_now expects a string, got ${format.typeName}');
}
final result = strftime(format.toString(), DateTime.now());
final result = strftime(format.toString(), now);
// llama.cpp formats into a 100-byte buffer and fails on an empty result.
if (result.isEmpty || utf8.encode(result).length >= 100) {
throw Exception('strftime_now: failed to format time');
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: dinja
description: "Jinja engine for Hugging Face and GGUF LLM chat templates: messages, tools, generation prompts. Parity-tested against llama.cpp and Jinja2. All platforms, including web and Wasm."
version: 1.3.1
version: 1.3.2
repository: https://github.com/leehack/dinja
issue_tracker: https://github.com/leehack/dinja/issues
topics:
Expand Down
88 changes: 88 additions & 0 deletions test/src/runtime/strftime_clock_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'package:dinja/dinja.dart';
import 'package:test/test.dart';

void main() {
final now = DateTime.utc(2026, 1, 2, 3, 4, 5);

test(
'the public callable supplies a fixed clock to repeated template calls',
() {
final template = Template(
"{{ strftime_now('%F %T') }}|{{ strftime_now('%Y %j %z') }}",
);
expect(
template.render({'strftime_now': strftimeNow(now)}),
'2026-01-02 03:04:05|2026 002 +0000',
);
},
);

test('independent callables and default renders keep their own clocks', () {
final template = Template("{{ strftime_now('%F') }}");
final fixed = strftimeNow(now);
expect(template.render({'strftime_now': fixed}), '2026-01-02');
expect(
template.render({'strftime_now': strftimeNow(DateTime.utc(2001))}),
'2001-01-01',
);
final before = DateTime.now();
final actual = template.render();
final after = DateTime.now();
String date(DateTime t) => t.toIso8601String().substring(0, 10);
expect(actual, anyOf(date(before), date(after)));
expect(template.render({'strftime_now': fixed}), '2026-01-02');
});

test('a caller-provided format retains input escaping', () {
final template = Template('{{ strftime_now(format) }}');
expect(
template.render({
'strftime_now': strftimeNow(now),
'format': JinjaString.user('<b>%Y</b>'),
}),
'&lt;b&gt;2026&lt;/b&gt;',
);
expect(
template.render({
'strftime_now': strftimeNow(now),
'format': '<b>%Y</b>',
}),
'<b>2026</b>',
);
});

test(
'argument and formatted-output failures match the built-in contract',
() {
for (final source in [
'{{ strftime_now() }}',
'{{ strftime_now(1) }}',
"{{ strftime_now('') }}",
"{{ strftime_now('${'x' * 100}') }}",
]) {
expect(
() => Template(source).render({'strftime_now': strftimeNow(now)}),
throwsException,
);
}
},
);

test('rendered input stays escaped through safe and derived filters', () {
for (final source in [
'{% set f %}{{ format }}{% endset %}{{ strftime_now(f)|safe }}',
'{% macro f() %}{{ format }}{% endmacro %}{{ strftime_now(f())|safe }}',
'{% set f %}{{ format }}{% endset %}'
'{{ strftime_now(f)|upper|safe }}',
]) {
final template = Template(source);
final input = {'format': JinjaString.user('<injected>')};
final expected = template.render(input);
expect(expected.toLowerCase(), '&lt;injected&gt;');
expect(
template.render({...input, 'strftime_now': strftimeNow(now)}),
expected,
);
}
});
}
Loading