A substantial correctness and security release born from a deep audit of the engine. It hardens context-sensitive escaping in two spots, fixes a long list of filter and compiler edge cases, adds the handy |map filter and an afterRender() extension hook, and brings back a performance regression fix. A recommended upgrade for everyone.
💥 Behavior Changes & Deprecations
{$interval|date}without an explicit format now throws instead of silently printing the literal format stringj. n. Y. Pass a format forDateInterval, e.g.{$interval|date: '%d days'}.{first},{last},{sep}used outside a{foreach}now emit a deprecation warning – they rely on an ambient$iteratorthat only a loop provides. This becomes a compile-time error in Latte 3.2.
✨ New Features
- New
|mapfilter – transform every item with a callback, the natural partner to|filter:{foreach ($items|map: fn($x) => $x->name) as $name}. - New
Extension::afterRender()hook – called in afinally, so it runs even when rendering ends early via{exitIf}or is interrupted by an exception. Perfect for cleanup or measurement. - The Tracy panel now shows render time per template, both total and self (excluding nested templates), so you can finally see which template actually eats the time.
|last,|random,|implode/|join,|commasnow accept anyiterable, not just arrays – hand them a generator or iterator directly, just like|first,|sliceand|filteralready allowed.
🐛 Bug Fixes
- Security:
<script type=" text/javascript ">with surrounding whitespace was classified as raw text while browsers execute it as JavaScript. The type is now trimmed the way browsers do, so context-sensitive escaping kicks in correctly. - Security: a template compiled under a permissive sandbox policy could be silently reused from cache under a stricter one, because part of the sandbox is enforced at compile time. Policy rules are now part of the cache key.
- Security:
xlink:hrefin inline SVG is now treated as a URL attribute and sanitized likehref. {capture $x|filter}no longer double-escapes its content. Adding an innocent filter like|trimused to strip the "HTML-ness" of the captured markup, escaping it on output; captured HTML now stays intact (while a filter that changes the content type still yields a plain string).{switch}with no expression used to compile to invalid PHP; it now works as a switch-true idiom ({case $cond}).<script type="a{$dyn}">with a partially dynamic type attribute no longer fails compilation with a cryptic "Unhandled match case" – it's treated as JavaScript.- The empty comment
{**}no longer breaks compilation. |reverseand|columnno longer drop elements from generators or iterators that yield duplicate keys.|indentno longer interprets$or\in the indentation string as regular-expression backreferences.|explode/|split(and|random) now raise a clearRuntimeExceptionon malformed UTF-8 instead of a confusingTypeError.|padLeft/|padRightwith an empty pad string, and thedivisibleBy()function with a zero divisor, now throw a helpful message instead of a rawDivisionByZeroError.- Compiled template class names now use a 64-bit hash (up from 40-bit), removing a small but real risk of two templates colliding so that one silently renders the other.
- Custom n:attributes whose name contains
tag-orinner-(e.g.n:my-tag-attr) are no longer mangled by the prefix stripper. - Extensions that subclass core nodes no longer silently break
{iterateWhile},{rollback}and{include parent}–Tag::closestTag()now matches subclasses, as its documentation always promised. - A compile-time failure without a cache directory now surfaces as a proper
CompileExceptionpointing at the generated source, instead of a bareParseError.
⚡ Performance
- The engine no longer recomputes its entire configuration signature on every
{include},{extends}or{embed}– a memoization lost in the 3.1 cache refactor is restored (and properly invalidated whenever you reconfigure the engine). This also fixes a crash whencreateTemplate(clearCache: false)was the very first call. - Passing a parameters object to
render()is roughly 2.6× faster – the reflection scan of the parameter class is memoized per class instead of being repeated on every render.