| latte | by dg | {include parent} and {include this} work inside dynamically named blocks The name of the enclosing block is resolved at runtime from the stack of blocks being currently rendered, which is shared along the inheritance chain. Previously this was a compile error, or, when the dynamic block was nested inside a static one, the include silently bound to the outer static block. To be released in 3.1. | | | |
| latte | by dg | blocks inside top-level conditions work in {embed} A block declared inside a top-level condition or loop of the {embed} content (single, duplicate or dynamically named) registers according to the flow of execution: {embed} executes the registration code in collecting mode before the embedded template renders. Adds the runtime collecting machinery (collectBlocks/collectBlock); child templates are not affected yet. Complements the implicit default block for {embed} (#419). To be released in 3.1. | | | |
| latte | by dg | added warnings for content silently discarded when template extends another Two runtime warnings, triggered only when the template actually renders as a child: - a top-level block declared inside a condition or loop: the condition is ignored and the block always applies, which is not what the author expects; in Latte 3.2 such blocks will register according to the flow of execution - any other content outside of blocks (text, markup, includes): it is never rendered, which typically means a missing {block} wrapper To be released in 3.1. | | | |
| latte | by daun | implicit default block for {embed} (#419) | | | |
| latte | by dg | added AGENTS.md & DOCS | | | |
| latte | by dg | added |json attribute modifier |json is an attribute-only modifier: as the last modifier of a dynamic HTML attribute it JSON-encodes the value via HtmlHelpers::formatJsonAttribute() with smart-quoting, overriding the default class/aria/data/bool handling. Anywhere else (outside an attribute, or not as the last modifier) |json is an unknown filter. formatJsonAttribute() is extracted from formatDataAttribute() and shared. | | | |
| latte | by dg | ExpressionAttributeNode: no longer mutates the modifier | | | |
| latte | by dg | spaceless: rewritten as streaming WhitespaceMinifier The new minifier understands HTML structure and works in three modes driven by the content type: - HTML: whitespace around whitespace-insensitive elements (div, p, li, br, ...) is removed entirely on both sides, other runs collapse to a single space; whitespace between attributes collapses while quoted attribute values stay verbatim; content of pre, textarea, script and style passes through untouched; comments, doctype, CDATA and PI pass through verbatim - XML: all tags are treated as whitespace-insensitive - text (and js/css/ical): spaces and tabs collapse to a single space, newlines are preserved including their count; */attr subcontexts collapse everything to a single space It runs as an ob_start() handler processing 4kB chunks: only an undecidable tail (split tag, comment or closing raw-text tag) is held back, pending whitespace is tracked as a flag, so output is streamed and byte-identical for any chunking. {spaceless} now uses a process-wide singleton with a nesting counter; nested blocks are ignored (the outer handler minifies everything), which also fixes the state collision of the former static $strip. Using {spaceless} inside raw-text elements or attributes is a compile error; <script n:spaceless> keeps its content verbatim. BC breaks: - Filters::strip(), spacelessHtml(), spacelessHtmlHandler() and spacelessText() have been removed; the |spaceless filter is handled by Filters::spaceless(), |strip remains an obsolete alias - quoted attribute values are no longer collapsed - word separation around inline tags is preserved (`</i> bb` is no longer glued) - text mode preserves newlines and normalizes \r\n to \n - leading/trailing whitespace of the region is trimmed - compiled template cache must be purged after upgrade | | | |
| latte | by dg | added TagParser::consumeCommaBeforeArguments() | | | |
| latte | by dg | latte-lint: added --format, --fail-on-warning and --quiet options - --format=console|json|sarif selects the reporter; machine formats keep STDOUT clean (banner and mode notes are console-only) - --fail-on-warning (also a Linter constructor parameter) treats files with warnings as failed; by default warnings still do not fail the build (BC) - an unknown option is now an error instead of being silently ignored - covered by an end-to-end test (exit codes, STDOUT/STDERR split, JSON purity even with --debug) | | | |
| latte | by dg | Linter: added JSON and SARIF reporters - both buffer the results and write a single document to STDOUT when linting finishes - SARIF 2.1.0 gives inline annotations in GitHub Actions for free - invalid UTF-8 in a message is substituted instead of failing the run, a zero (= unknown) column is omitted, UNC paths map to file:// URIs - issues without a rule ID fall back to 'latte.issue' in SARIF | | | |
| latte | by dg | Linter: separated analysis from presentation (LintResult, Reporter) - new lintFile() returns a LintResult (file + issues) and writes nothing to STDOUT (debug diagnostics go to STDERR); lintLatte() stays as a deprecated wrapper - presentation extracted into the Reporter interface; ConsoleReporter keeps today's output and prints progress only on an interactive terminal, so CI logs are free of \x0D artifacts; summary now says "found problems in" - a missing template is reported as file.missing, an unreadable one as file.unreadable | | | |
| latte | by dg | Linter: added static checking of template paths New TemplateReferenceCheck validates that statically named templates referenced by {include}, {import}, {extends}/{layout}, {embed file}, {sandbox} and {include block from} exist, via the engine's loader. A loader that cannot resolve references (a bare StringLoader) is silently skipped. Registered as a built-in check. | | | |
| latte | by dg | Linter: rewritten as first-class checks driven by the Linter A lint check is now a Latte\Linting\Check that inspects the parsed template and returns Issue objects, run and owned by the Linter instead of riding the compiler pass pipeline via trigger_error. The engine only compiles; the Linter parses each file, runs the checks (each isolated in try/catch so a faulty check cannot abort the file or mask the others) and reports the collected issues. The symbol validation becomes SymbolCheck; the deprecated LinterExtension is now an inert no-op. Third parties add checks via addCheck(). | | | |
| latte | by dg | Linter: moved into Latte\Linting namespace Latte\Tools\Linter and Latte\Tools\LinterExtension are relocated to the Latte\Linting namespace under src/Latte/Linting. The original class names are kept as deprecated subclasses in a single src/compatibility.php for backward compatibility, so existing scripts keep working. No behavior change. | | | |
| latte | by dg | added tests of node source extent integrity: invariants over a corpus and pinned golden extents | | | |
| latte | by dg | FragmentNode: the extent is virtual, derived from the children The position and end of a fragment are unset in the constructor and computed on read as the lowest start and the highest end among the children, so they can never disagree with what the fragment actually contains and stay correct when children are added later. This drops the eager assignment in append(), which took the position even from NopNodes and CommentNodes that are discarded right away: the fragment of '{* c *}foo' claimed to start at the comment, seven bytes before its first child. It also drops the deriving pass at the end of parse(), which read only the first and last child, so a single child without a position at either edge silently wiped out the whole extent. A fragment must not be given an extent from outside, so __set() refuses it; the parser skips the assignment for tags that return a bare fragment, such as {syntax}. | | | |
| latte | by dg | Node: source extent is a start/end pair, Range is standalone Node::$position is again just the starting point and the new Node::$end holds the exclusive end, so every node carries its full source extent. Range no longer extends Position: it is a standalone {start, end} value object with a virtual $length, kept for StatementNode::$tagRanges and for tools that need a composite. Token and Tag carry the pair as well. Node constructors take an $end parameter next to $position, both filled by the parser. A final traverse derives fragment extents from the first and last child, so FragmentNode no longer reports the length of its first child as its own. Two latent bugs surfaced on the way and are fixed: - Scalar\StringNode::parse() and its siblings accept only two arguments, so the end position passed by the generated parser was silently dropped and strings, numbers and interpolated strings ended up with no extent at all. - The end token can be a synthetic one without a position; endPos() now falls back to the start token, which is what Range::span() used to do implicitly. This reverts the Range shape shipped in 3.1.4. | | | |
| latte | by dg | TextNode: emptying shrinks the extent to a zero-width anchor The parser sometimes needs to drop an indentation node that is already sitting in the tree: a line holding only a tag discards it outright, and an n:attribute moves it inside the wrapper by cloning it and emptying the original. Emptying used to touch only the content, so the husk kept claiming a byte range it no longer represented: its content was not the source slice, and in the clone case two different nodes shared an identical extent, so a node-at-offset lookup got two candidates that nothing could tell apart. Emptying now goes through clear(), which also shrinks the extent to a zero-width anchor at the start. The husk then tells the truth (an empty text anchored here, spanning nothing), the invariant content === substr(source, extent) holds for every text node, the byte range stays with the clone alone, and zero-width nodes are naturally transparent to extent-based lookups, while line/column remain for error messages. | | | |
| latte | by dg | NodeTraverser: clearer exception when a child slot rejects replacement or removal | | | |
| schema | by dg | DOCS | | | |
| schema | by dg | added Expect::tuple() | | | |
| schema | by dg | removed support for key PreventMerging, removed Helpers::merge() (BC break) | | | |
| schema | by dg | AnyOf::merge() merges layers according to the matched alternative instead of blindly (BC break) | | | |
| schema | by dg | Structure::merge() driven by MergeMode; numeric-key appending decoupled from otherItems (BC break) | | | |
| schema | by dg | added MergeMode enum, mergeMode() and mergeWith(); Type::merge() merges arrays only according to the schema (BC break) | | | |
| schema | by dg | Type: mergeDefaults() are disabled by default (BC break) [Closes #28, Closes #31] | | | |
| schema | by dg | Schema::merge() receives Context and reports merge errors through it (BC break) | | | |
| tracy | by dg | used attribute Deprecated | | | |
| tracy | by dg | error.log changed to warning.log | | | |
| tracy | by dg | Logger: added typehints WIP | | | |
| tracy | by dg | uses PascalCase constants | | | |