Framework

Repository Author Commit message Committed SHA  
 
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

Last synchronization: 2026-07-21 01:02:40