🦖
[■] Өөрчлөлтийн түүх
codesaur/template GitHub дээр үзэх

Changelog - codesaur/template

This file documents all notable changes to the codesaur/template package.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.


[5.0.1] - 2026-10-01

[5.0.1]: https://github.com/codesaur-php/Template/compare/v5.0.0...v5.0.1

Fixed

|e('js') did not escape template-literal characters

  • Bug: The js strategy escaped quotes, backslashes, control characters and < > & /, but not the backtick ` or $. A value printed inside a JS template literal (`Hello {{ name|e('js') }}`) could close the literal or inject a ${...} expression. U+2028 / U+2029 (line terminators inside string literals for pre-ES2019 engines) were not escaped either.
  • Fix: ` -> \x60, $ -> \x24, U+2028 -> 
, U+2029 -> 
. The replacement is now a single-pass strtr(). Output for all previously escaped characters is unchanged.

Added

  • testJsEscapeCoversTemplateLiterals in tests/AutoEscapeTest.php.

[5.0.0] - 2026-09-09

[5.0.0]: https://github.com/codesaur-php/Template/compare/v4.1.1...v5.0.0

Changed (BREAKING)

{{ }} now HTML-escapes by default (autoescape)

  • Before: print nodes output values as-is. Every user-supplied value had to be escaped manually with |e, and a single forgotten |e was an XSS hole.
  • Now: like Twig, {{ }} passes string results through htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'). Values that are not escaped ("safe"):
    • the result of the raw, e and escape filters (|e is never double-escaped)
    • Markup objects (new class, see below)
    • MemoryTemplate / FileTemplate objects ({{ content }} layout injection - the nested template renders with its own autoescape setting)
    • macro return values (the macro body is already escaped)
    • int, float, bool, null
  • The safe mark travels with the value (runtime Markup wrapper, not a compile-time filter check). Any operation that turns a safe value into a plain string drops the mark and the result is escaped again - the same semantics as Twig: {{ v|upper|e }} escapes once, {{ v|e|upper }} and {{ v|raw ~ '!' }} escape the string result, {{ ok ? v|raw : v }} is decided per branch, {% set x = v|raw %}{{ x }} stays raw.
  • Filters that produce HTML or HTML-significant characters (nl2br, json_encode, striptags with allowed tags) return plain strings and therefore need |raw.
  • setAutoEscape(false) restores the previous behaviour for a template instance (plain-text / e-mail templates). When disabled, raw is a no-op, e returns a plain string and macros return plain strings, exactly as in 4.x.

Added

  • MemoryTemplate::setAutoEscape(bool $enabled): void / isAutoEscape(): bool
  • MemoryTemplate::escape(string $value): string (protected) - the single HTML escaping strategy used by both autoescape and the html strategy of |e; subclasses may override it.
  • codesaur\Template\Markup - final, \Stringable wrapper that marks an HTML string as safe. Custom filters/functions that return HTML must wrap their result: fn($v) => new Markup('<b>' . htmlspecialchars((string) $v) . '</b>').
  • tests/AutoEscapeTest.php (40 tests) - default state, every safe rule, mark loss through filters/concat/ternary, set, macros (by name, via _self, with a raw argument), nested templates, Markup, custom filters, disabled mode, FileTemplate layouts.
  • "Autoescape" section in docs/{mn,en}/README.md and docs/{mn,en}/api.md with the safe-value table, the mark-loss table and the list of filters that need |raw.

Upgrading from 4.x

  1. Every {{ }} that intentionally outputs HTML needs |raw. Typical places: CMS/record content fields, HTML stored in settings, |nl2br, |json_encode inside <script>, |striptags('<b>'), static HTML fragments passed as strings. Existing |e usages can stay - they are not double-escaped.
  2. Layouts that receive the inner page as a template object ($layout->set('content', $page)) need no change: {{ content }} is safe. Passing $page->output() (a string) instead would be escaped - keep passing the object.
  3. Custom filters/functions that return HTML must return new Markup(...).
  4. Non-HTML templates (plain text, e-mail bodies, generated code) call setAutoEscape(false).
  5. To find candidates for |raw, grep the templates for prints of fields or filters that are known to carry HTML, e.g.:
    grep -rnE "\{\{[^}]*(content|html|body|description)[^}]*\}\}" --include=*.html
    grep -rnE "\|(nl2br|json_encode|striptags)" --include=*.html

    and review each hit: add |raw only where the value is trusted.

Fixed

  • tests/EngineTest.php: testBuiltInJsonDecodeFilter uses |raw (the JSON output is otherwise escaped); testAutoescapeDisabled now disables autoescape explicitly and a new testAutoescapeEnabledByDefault asserts the default.

[4.1.1] - 2026-07-04

[4.1.1]: https://github.com/codesaur-php/Template/compare/v4.1.0...v4.1.1

Fixed

Expression parser left tokens unconsumed on short-circuited branches

  • Bug: When the ternary (? :), elvis (?:), null coalescing (??), and, and or operators short-circuited, the parser pointer was not advanced past the skipped operand. Standalone expressions appeared to work (trailing text is ignored), but inside parentheses, function arguments, or array literals the leftover tokens corrupted parsing:
    • {{ (a ? 'x' : 'y') ~ '!' }} rendered x instead of x!
    • {% if (a and b) or c %} evaluated incorrectly (c was never reached)
    • {{ max(a ? 1 : 2, 5) }} truncated the argument list and could raise a TypeError
  • Fix: Branching operators now always consume both operands and select the result afterwards (pTernary, pNullCoalesce, pOr, pAnd). Note: operands are therefore always evaluated as well - this is safe because all accessors in the engine are null-safe and division is zero-guarded.

Unary minus

  • Bug: Negative literals only worked by accident ({{ -5 }} parsed as null - 5), and failed at higher precedence: {{ 3 * -2 }} returned -2 instead of -6.
  • Fix: pPrimary() now supports a proper unary minus for numeric operands ({{ -5 }}, {{ 3 * -2 }}, {{ -price }}); non-numeric operands yield 0.

Added

Tests

  • testTernaryInsideParentheses, testElvisInsideParentheses, testNullCoalescingInsideParentheses, testLogicalOperatorsInsideParentheses, testOrFollowedByTernary, testTernaryInsideFunctionArguments - parser pointer regression tests
  • testUnaryMinus - negative literals, unary minus with multiplication and variables

Changed

  • Removed the author's phone number from composer.json.

[4.1.0] - 2026-06-19

[4.1.0]: https://github.com/codesaur-php/Template/compare/v4.0.2...v4.1.0

Added

  • More Twig-compatible operators in the expression parser (MemoryTemplate::pCompare()):
    • in / not in membership - {% if type in ['image', 'video'] %}. Works on arrays (in_array, loose), strings (str_contains substring), and Traversable.
    • ends with string operator - complements the existing starts with.
    • matches regex operator - {% if email matches '/^[^@]+@[^@]+$/' %} (uses preg_match, errors suppressed).
    • is even / is odd number tests (also via is not) - e.g. alternating row styles in loops.
    • Note: the for ... in loop is parsed at the tag level (buildFor), so the new in comparison operator does not conflict with it.

[4.0.2] - 2026-06-09

[4.0.2]: https://github.com/codesaur-php/Template/compare/v4.0.1...v4.0.2

Changed

  • Property names no longer use a leading underscore (PSR-12 compliance)
    • MemoryTemplate: $_html -> $html, $_vars -> $vars
    • FileTemplate: $_file -> $filepath
    • Internal only - all properties are protected, so the public API is unchanged.

[4.0.1] - 2026-04-27

[4.0.1]: https://github.com/codesaur-php/Template/compare/v4.0.0...v4.0.1

Fixed

{% for %}{% else %}{% endfor %} empty-iterable branch

  • Bug: Templates using the Twig-style {% for %}{% else %}{% endfor %} construct silently dropped everything after the {% endfor %} from the rendered output. The parser hit {% else %}, returned the loop body it had collected so far, but never consumed the else/endfor tokens - so the outer buildTree saw the orphan else as an end-marker and returned immediately. Both the else branch and any content following the loop were lost.
  • Root cause: buildFor() only looked for {% endfor %} and had no knowledge of the {% else %} separator. The for-node had no else slot, and renderFor() returned an empty string for non-iterable / empty collections instead of falling back to the alternate branch.
  • Fix: buildFor() now optionally consumes an {% else %} block and attaches its body as a new else key on the for-node. renderFor() renders that else body when the iterable is missing, non-iterable, or empty - matching Twig's documented behavior.

Object method calls in expressions

  • Bug: Expressions like {{ user.can('edit') }} and {% if auth.is('admin') %} silently returned null, causing permission-gated UI to be hidden even from authorized users.
  • Root cause: The pPostfix() parser only handled the _self.macroName(...) (macro invocation) case when it encountered a method call with arguments, and silently set the result to null for all other cases - completely skipping object and array-of-callables method dispatch.
  • Fix: pPostfix() now handles three distinct cases when a .name(args) postfix is parsed:
    • _self.macro(...) -> callMacro (unchanged)
    • object.method(...) -> $val->$method(...$args) (guarded by method_exists)
    • array['callable'](...) -> invokes the callable array element directly
    • Otherwise -> null (unchanged)

Added

Documentation

  • Added "Method calls" line to the MemoryTemplate class docblock describing the newly supported object.method(args) and array_of_callables.name(args) forms.

Tests

  • testForElseWithItems - {% else %} is skipped when the loop iterates
  • testForElseWithEmptyItems - {% else %} renders for empty arrays
  • testForElseWithNonIterable - {% else %} renders for null / non-iterable
  • testObjectMethodCall - calling public methods on an object from expressions
  • testObjectMethodCallInIfBlock - using a method call as an {% if %} condition
  • testArrayOfCallablesMethodCall - calling closures stored as array values
  • testMissingMethodReturnsNull - non-existent methods safely return null

[4.0.0] - 2026-03-30

[4.0.0]: https://github.com/codesaur-php/Template/compare/v3.0.1...v4.0.0

Changed

Completely removed Twig dependency

  • Removed twig/twig package entirely from dependencies
  • During development, the necessary capabilities inspired by Twig's syntax and design patterns were reimplemented as our own standalone engine
  • Added ext-mbstring to requirements (for capitalize, upper, lower, length, etc.)

Migrated full engine into MemoryTemplate

  • The complete template engine (tokenizer, parser, renderer, expression evaluator) has been moved from FileTemplate into MemoryTemplate. MemoryTemplate now supports if/for/set/macro, filter chains, expression parser, ternary/null coalescing, and loop variables
  • FileTemplate is now a thin wrapper that only reads files and passes them to MemoryTemplate's engine

New methods added to MemoryTemplate

  • addFilter(string, callable), addFunction(string, callable) - register custom filters/functions
  • 33 built-in filters (e, date, length, keys, slice, json_encode, json_decode, abs, trim, striptags, title, join, reverse, sort, unique, column, batch, values, replace, wordwrap, etc.)
  • Built-in functions: attribute, range, max, min

Removed

  • Removed the TwigTemplate class (merged into FileTemplate)
  • twig/twig dependency (15+ files, ~50,000 lines of code removed)
  • addGlobal(), getEnvironment() methods (Twig-specific)
  • {# comment #} template comment syntax
  • Removed the stringify() protected method - replaced with (string) cast

[3.0.1] - 2026-03-05

[3.0.1]: https://github.com/codesaur-php/Template/compare/v3.0.0...v3.0.1

Changed

Documentation Cleanup

  • Removed all Unicode emoji characters from documentation files
  • Replaced Unicode arrow with ASCII arrow

[3.0.0] - 2026-01-08

[3.0.0]: https://github.com/codesaur-php/Template/compare/v2.0.0...v3.0.0

Stable Release

This version is the stable release of the codesaur/template package with complete features, full test coverage, and comprehensive documentation.

Added

Core Functionality

  • MemoryTemplate - Lightweight template engine with simple {{key}} placeholders
  • FileTemplate - File-based template loader (extends MemoryTemplate)
  • TwigTemplate - Advanced renderer fully integrated with Twig engine (extends FileTemplate)

Testing

  • Unit, Integration, Performance, Memory tests (70+ tests, 1200+ assertions)
  • 100% line coverage, 100% method coverage

CI/CD

  • GitHub Actions CI/CD pipeline (PHP 8.2)

Documentation

  • README.md, API.md, REVIEW.md (Mongolian and English)

[2.0.0] - 2025-11-28

[2.0.0]: https://github.com/codesaur-php/Template/compare/v1.0...v2.0.0

Added

Core Improvements

  • Enhanced template processing capabilities
  • Improved error handling
  • Better file template support

[1.0] - 2021-03-09

[1.0]: https://github.com/codesaur-php/Template/releases/tag/v1.0

Initial Release

  • MemoryTemplate - Basic template engine with {{key}} placeholders
  • FileTemplate - File-based template loader
  • TwigTemplate - Twig engine integration