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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ if (JINJA2CPP_BUILD_TESTS)
set(JINJA2CPP_TEST_SOURCES
test/autoescape_test.cpp
test/basic_tests.cpp
test/big_int_test.cpp
test/containers_api_test.cpp
test/errors_test.cpp
test/expressions_test.cpp
Expand Down
16 changes: 13 additions & 3 deletions docs/api-2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ the migration notes fix it), **fix** (a defect; behaviour changes, no rename),
| `Value(T&&)` for unsigned, `long long`, `short`, `char`, `size_t` | *probe:* stores `bool` at C++14 (`Value(5u).data().index() == 1`), `int64_t` at C++17, and `Value(size_t(5))` does not compile at C++17 | constructors for every integral type storing `int64_t`, as `Reflect` already does; `Value(char)` deleted (ambiguous between a number and a one-letter string) | fix (task 0067) |
| `Value(nullptr)` | *probe:* picks `Value(const char*)` and throws `logic_error` from `std::string(nullptr)` | `Value(std::nullptr_t)` makes `None` | fix (0067) |
| `Value(ValuesList&&) noexcept`, `Value(ValuesMap&&) noexcept` | allocate, so `bad_alloc` becomes `std::terminate` | drop `noexcept` there | fix |
| `Value(uint64_t)` above `INT64_MAX` | cannot be held: wrapped to a negative number in 1.x, `std::overflow_error` since 0172 | `jinja2::BigInt` (`big_int.h`): an integer of any size as canonical decimal text, `TryParse(std::string_view, BigInt&) noexcept`, `Sign`, `ToInt64`/`ToUInt64`, comparisons by value; the last `Value` alternative (`sizeof(Value)` unchanged), with `IsBigInt()`, `AsBigInt()`, `bool AsBigInt(const BigInt*&)`; the integral constructor, `Reflect` and the JSON bindings store it for a value outside `int64_t`. `IsInt()` stays `int64_t` only. Until 0270 a `BigInt` that enters a template fails the render (a top-level render parameter fails it even unread), and `ToString()` prints one only at top level, not inside a list or map; arithmetic on it needs a backend (0271, plan docs/design/0178-big-integer-plan.md) | add (0269); an exhaustive `std::visit` over `GetData()` must handle the new alternative |
| `IsEqual()` next to `operator==` | redundant | keep both; not worth a break | — |
| `ValuesMap : std::unordered_map` | hash order | ordered map (task 0043) | break, done in 0043 |
| free `AsString`/`AsWString` overloads for `std::string`, `std::wstring` and the views | encoding conversion; the `std::string` one returns a `const` value, defeating moves | `ToString`/`ToWString` returning `std::string`/`std::wstring` | alias |
Expand Down Expand Up @@ -321,13 +322,13 @@ class Value
public:
using ValueData = variant<NoneValue, bool, std::string, std::wstring, string_view, wstring_view,
int64_t, double, RecWrapper<ValuesList>, RecWrapper<ValuesMap>,
GenericList, GenericMap, RecWrapper<UserCallable>, Markup, WMarkup>;
GenericList, GenericMap, RecWrapper<UserCallable>, Markup, WMarkup, BigInt>;

Value();
Value(std::nullptr_t) noexcept; // None
template<class I, std::enable_if_t<detail::IsStoredAsInt<I>::value>* = nullptr>
Value(I val) noexcept(!detail::CanExceedInt64<I>) // above INT64_MAX: std::overflow_error (0172)
: m_data(detail::ToStoredInt(val)) {}
Value(I val) noexcept(!detail::CanExceedInt64<I>) // outside int64_t: a BigInt (0269)
: m_data(detail::ToStoredInt<ValueData>(val)) {}
Value(char) = delete; // number or one-letter string? say which
// ... string, double, list, map, callable constructors as today

Expand All @@ -341,6 +342,7 @@ public:
bool IsList() const noexcept; // ValuesList or GenericList
bool IsMap() const noexcept; // ValuesMap or GenericMap
bool IsCallable() const noexcept;
bool IsBigInt() const noexcept; // an integer outside int64_t

const std::string& AsString() const; // throws bad_variant_access
std::string& AsString(); // a held Markup becomes a plain string first
Expand Down Expand Up @@ -493,6 +495,14 @@ inside an `if` statement or an inline if, where it fails when it runs (docs/task
1.x let a `UserCallable` passed in the render parameters or the globals act as a filter or
test, found when it ran; `Settings::userCallablesAsFilters = true` keeps that behaviour.

### 5.7 C++-only filters render, `toxml` is gone (release note)

In 1.x `camelize`, `underscorize`, `escapecpp`, `toxml` and `toyaml` were registered but
rendered an empty string. In 2.0 the first four render the output that
[cpp-only-filters.md](cpp-only-filters.md) describes, and `toxml` is removed: a template that
uses it fails `Template::Load` with `ErrorCode::UnknownFilter` (docs/tasks/0233,
docs/release-2.0.md D16). Templates that leaned on the empty output drop the filter.

## 6. Release options

| Option | What ships | Users get | Cost |
Expand Down
85 changes: 85 additions & 0 deletions docs/cpp-only-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# C++-only filters

Jinja2C++ adds a few filters that Python Jinja2 does not have. A template that uses them
does not load in Jinja2 (`No filter named ...`). Since 2.0 each has the output described
here (docs/tasks/0233); before, `camelize`, `underscorize`, `escapecpp`, `toxml` and
`toyaml` were registered but rendered an empty string.

| Filter | Output |
|---|---|
| `applymacro(macro=name, ...)` | The value passed to the macro `name`, with the other arguments |
| `camelize(upper_first=true)` | An identifier in CamelCase |
| `underscorize` | An identifier in snake_case |
| `escapecpp` | The text escaped for a C++ string literal |
| `toyaml` | The value as a block-style YAML document |

`toxml` was removed in 2.0: JSON-like data has no single XML mapping, so it would be a format
of our own to define and support (docs/release-2.0.md, D16). A template that uses it fails
to load with `ErrorCode::UnknownFilter`; `xmlattr` covers attribute lists, and a macro
can write any XML shape.

## `camelize` and `underscorize`

Both take the value's `str()` and split it into words: runs of letters and digits (Unicode
`\w` without `_`), split again before an uppercase letter that follows a lowercase letter
or a digit (`fooBar`, `Vector3D`) and before the last of a run of uppercase letters that a
lowercase one follows (`HTTPServer` is `HTTP` and `Server`). Everything between words
(`_`, `-`, spaces, punctuation) is dropped.

- `camelize` gives each word a titlecase first letter and lowercase others and joins them:
`device_type`, `device-type` and `DEVICE_TYPE` are `DeviceType`, `HTTPServer` is
`HttpServer`. With `upper_first=false` the first word is all lowercase: `deviceType`,
`httpServer`.
- `underscorize` joins the words in lowercase with `_`: `DeviceType` is `device_type`,
`getHTTPResponseCode` is `get_http_response_code`, `__init__` is `init`.

Case mapping is Unicode's, as in `upper`/`lower`: `straße_name|camelize` is `StraßeName`.

## `escapecpp`

The value's `str()` as the contents of a C++ string literal, without the quotes: `\` and
`"` get a backslash, `\n`, `\r`, `\t`, `\a`, `\b`, `\f` and `\v` their escapes, and other
control characters (below U+0020, and U+007F) three octal digits (`\001`), which cannot run
into a digit that follows the way `\x` does. Other characters, `'` and non-ASCII ones
included, are kept, so a UTF-8 template gives a literal for a UTF-8 source file.

```
const char* text = "{{ message|escapecpp }}";
```

## `toyaml`

A YAML document that YAML 1.1 and 1.2 loaders (PyYAML, yaml-cpp, ...) read back as the same
data, with no trailing newline:

- `none` is `null`, booleans `true`/`false`, integers in decimal,
floats in Python's shortest form with a `.` always present (`1.0e+20`), `.nan`, `.inf`
and `-.inf`.
- A mapping is `key: value` lines in the mapping's order; a list is `- item` lines; a
`dictsort` or `items` pair is a two-item list. Nested collections are indented by two
spaces, a collection inside a list starts on the item's line, and an empty one is `[]`
or `{}`.
- A string (a key too) is written plain only when it cannot be read as anything else: it
starts with a letter, `_`, `/` or a printable non-ASCII character, holds only those,
digits, spaces and `-.()+`, does not end with a space and is not a null or boolean word
(`y`, `n`, `yes`, `no`, `on`, `off`, `true`, `false`, `null`, in any case). Any other
string is double-quoted with YAML escapes (`\"`, `\\`, `\n`, `\t`, `\xHH`, `\uHHHH`, ...).
- A key longer than 255 bytes is written as an explicit `? key` with its value on a `:`
line below, since YAML limits an implicit key to 1024 characters.
- Undefined values, `namespace()`, `cycler()`, `joiner()` and macros raise an error
("Object of type Undefined is not YAML serializable"), as `tojson` does.

```
{{ {'name': 'demo', 'ports': [80, 443], 'tls': {'enabled': true}}|toyaml }}
```

```yaml
name: demo
ports:
- 80
- 443
tls:
enabled: true
```

The result is an ordinary string: under autoescape it is HTML-escaped like any other.
12 changes: 7 additions & 5 deletions docs/parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ How far Jinja2C++ is from Python [Jinja2](https://jinja.palletsprojects.com/) 3.
area, and which task in `docs/tasks/` closes each gap.

Every statement marked with a case id is backed by the differential corpus in
`test/parity/` (1938 templates rendered by both engines, see
`test/parity/` (1939 templates rendered by both engines, see
[test/parity/README.md](../test/parity/README.md)); `ctest -R parity` re-checks all of
them. Statements in the last section (API level) are read from the headers and are not
corpus-checked yet.
Expand All @@ -21,7 +21,7 @@ Snapshot of `python3 test/parity/generate.py --report` (Jinja2 3.1.6, Oct 2026):
| autoescape | 124 | 122 | 1 | 1 | 0 | 0 | 0 | 0 | 0210, 0216 |
| custom | 28 | 28 | 0 | 0 | 0 | 0 | 0 | 0 | |
| errors | 97 | 93 | 0 | 2 | 2 | 0 | 0 | 0 | 0036, 0213 |
| filters | 308 | 302 | 4 | 2 | 0 | 0 | 0 | 0 | 0101, 0178, 0203, 0215 |
| filters | 309 | 303 | 4 | 2 | 0 | 0 | 0 | 0 | 0101, 0178, 0203, 0215 |
| fuzz | 32 | 32 | 0 | 0 | 0 | 0 | 0 | 0 | |
| globals | 74 | 73 | 1 | 0 | 0 | 0 | 0 | 0 | 0212 |
| i18n | 54 | 54 | 0 | 0 | 0 | 0 | 0 | 0 | |
Expand All @@ -37,7 +37,7 @@ Snapshot of `python3 test/parity/generate.py --report` (Jinja2 3.1.6, Oct 2026):
| tests | 58 | 57 | 0 | 1 | 0 | 0 | 0 | 0 | 0101 |
| undefined | 106 | 106 | 0 | 0 | 0 | 0 | 0 | 0 | |
| whitespace | 54 | 54 | 0 | 0 | 0 | 0 | 0 | 0 | |
| **total** | **1938** | **1894** | **25** | **15** | **4** | **0** | **0** | **0** | |
| **total** | **1939** | **1895** | **25** | **15** | **4** | **0** | **0** | **0** | |

*output*: both render, text differs. *rejects*: C++ errors on a valid template.
*accepts*: C++ renders a template Jinja2 rejects. *unsupported*: needs an Environment
Expand Down Expand Up @@ -255,8 +255,10 @@ storing a container in itself raises instead of printing `[...]` (deliberate, 00
| `xmlattr` | ✅ | `xmlattr*` | |
| Unknown filter is an error; in a branch never taken it is not | ✅ | `unknown_filter*` | |

C++-only filters (`camelize`, `underscorize`, `escapecpp`, `toxml`, `toyaml`,
`applymacro`) and the `startsWith` test are deliberate extensions; Jinja2 rejects them.
C++-only filters (`camelize`, `underscorize`, `escapecpp`, `toyaml`, `applymacro`; see
[cpp-only-filters.md](cpp-only-filters.md)) and the `startsWith` test are deliberate
extensions; Jinja2 rejects them. `toxml` was removed in 2.0 and is an unknown filter, as in
Jinja2 (0233).

## Statements (`statements`)

Expand Down
10 changes: 8 additions & 2 deletions docs/tasks/0233-cpp-only-filter-stubs.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
status: open
status: done
priority: medium
area: api
issues: ["https://github.com/jinja2cpp/Jinja2Cpp/issues/41"]
touches: [src/filter_factories.cpp, src/string_converter_filter.cpp, src/serialize_filters.cpp]
touches: [src/filter_factories.cpp, src/string_converter_filter.cpp, src/serialize_filters.cpp, src/filters.h, test/filters_test.cpp, docs/cpp-only-filters.md]
shares: [docs/parity.md]
---
# C++-only filters `camelize`, `underscorize`, `escapecpp`, `toxml`, `toyaml` do nothing
Expand All @@ -29,3 +29,9 @@ migration notes (0077) say which.

**Release decision (Ruslan 2026-10-18, D16 in docs/release-2.0.md).** Implement `camelize`,
`underscorize`, `escapecpp` and `toyaml` with documented output; remove `toxml`.

**Outcome.** `camelize(upper_first=true)`, `underscorize`,
`escapecpp` and `toyaml` render the output documented in `docs/cpp-only-filters.md`, with
rows in `test/filters_test.cpp`; `toxml` is removed and fails Load with `UnknownFilter`
(corpus case `filters.unknown_filter_toxml`). Release note: `docs/api-2.0.md` 5.7, for
0077's `MIGRATION.md`. PR #581.
4 changes: 3 additions & 1 deletion docs/tasks/0269-public-big-int-value.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
status: open
status: done
priority: high
area: api
depends: [0172, 0178]
Expand All @@ -23,3 +23,5 @@ that enters a template raises OverflowError. Coordinate with 0250's phase 0253,
rows), abidiff shows `sizeof(Value)` unchanged, and docs/api-2.0.md documents `BigInt`.

Plan: [docs/design/0178-big-integer-plan.md](../design/0178-big-integer-plan.md).

Done in #596.
4 changes: 2 additions & 2 deletions docs/tasks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ files go under `touches` (0012 and 0024 each rewrite about a hundred rows).
| [0230](0230-boost-free-core.md) | A Boost-free core | build | medium | open |
| [0231](0231-robin-hood-successor.md) | robin_hood's upstream is archived | perf | high | open |
| [0232](0232-dependency-pin-drift.md) | Dependency pins disagree across modes and some are mutable | build | low | open |
| [0233](0233-cpp-only-filter-stubs.md) | C++-only filters `camelize`, `underscorize`, `escapecpp`, `toxml`, `toyaml` do nothing | api | medium | open |
| [0233](0233-cpp-only-filter-stubs.md) | C++-only filters `camelize`, `underscorize`, `escapecpp`, `toxml`, `toyaml` do nothing | api | medium | done |
| [0234](0234-github-tracker-cleanup.md) | Close GitHub issues that master already fixes, before 2.0 | release | medium | open |
| [0235](0235-parity-feature-rows-drift.md) | `docs/parity.md` feature rows contradict master | parity | medium | open |
| [0236](0236-meta-find-undeclared-variables.md) | Template introspection: `meta.find_undeclared_variables`, `Template.module` | api | low | open |
Expand Down Expand Up @@ -334,7 +334,7 @@ files go under `touches` (0012 and 0024 each rewrite about a hundred rows).
| [0266](0266-confined-filesystem.md) | `ConfinedFileSystem`: a loader that never leaves its root | api | medium | open |
| [0267](0267-untrusted-mode-fuzzing.md) | Fuzz targets for templates written by untrusted users | robustness | high | open |
| [0268](0268-untrusted-mode-followups.md) | Untrusted mode: additive follow-ups after the freeze | api | low | open |
| [0269](0269-public-big-int-value.md) | Public `BigInt` value (0178 phase 1, G1) | api | high | open |
| [0269](0269-public-big-int-value.md) | Public `BigInt` value (0178 phase 1, G1) | api | high | done |
| [0270](0270-big-int-core-text-form.md) | Big integers in the core, text form (0178 phase 2) | parity | medium | open |
| [0271](0271-big-int-backend-interface.md) | Big-integer backend interface and overflow promotion (0178 phase 3) | api | medium | open |
| [0272](0272-big-int-boost-backend.md) | Boost.Multiprecision backend and `JINJA2CPP_WITH_BIGINT` (0178 phase 4) | build | medium | open |
Expand Down
Loading
Loading