История коммитов

.
refactor(album): move the album pages to Twig
Thirteen templates become twelve plus four components: the photo card and the
vote buttons were copied into three pages each, and the grid around them into
two.

The description of a photo is escaped and given its smiley images by the
presenter, so it arrives as Twig\Markup.

The comments page still hands back a string — the legacy Comments class prints a
whole page of its own — while the error pages it may answer with are views.
.
refactor(collections): move the public pages to Twig
ItemContentFormatter returns Twig\Markup: it purifies the item text on output,
which is exactly the contract that lets a template print it as it is. It already
answered null for empty content, so nothing had to be invented for the "no text"
case.

The ten admin templates stay on Plates.
.
refactor(help): move the help pages to Twig
Eight templates become six: the smiley catalogue and the avatar catalogue were
the same file twice, and the footer of a list is a component now.

Escaping moves to the output here too — the catalogues ran htmlspecialchars() and
htmlentities() over the names they passed to the view, which under autoescape
would print the entities themselves.

The picture of a personal smiley is the rendered image tag, so the use case hands
it over as Twig\Markup. The interface of the renderer keeps returning a string:
its other callers are still on Plates and will each decide when they move.
.
refactor(guestbook): move the guestbook pages to Twig
The text of an entry and the reply to it come out of the formatter as
Twig\Markup — they have been through the purifier, the media embedder and the
smilies. A missing reply is null rather than empty markup, so the template can
still tell there is none.

The name of an entry was printed unescaped, including into the argument of an
inline onclick handler; it is escaped now, for JavaScript in that one place.

The editor partial received an already escaped value and printed it raw into an
attribute. It takes the raw value now and escapes once, on output.

The edit and reply pages rendered the editor a second time inside their scripts
block, which emitted a second component with the same id; only the one in the
form is left.
.
refactor(community): move the community pages to Twig
The public result page comes with them: `@theme/pages/result.twig`, named
explicitly by the callers that answer "for registered users only". The condition
that pulled the admin sidebar into it is gone — an admin copy of the page will be
its own file.

The list, the row and the links under a list become components, and the
byte-identical administration template is dropped: users, birthdays and the staff
list are one page with different data.

Two things the old markup hid:

- the row printed a delete link from an attribute nobody sets, so the block never
rendered; it is removed, and with it the "Delete" string of this domain;
- the karma of a row came from an alias the karma query adds, and reading it off
a row loaded any other way would call it as a method; it is computed from the
two real columns instead.

Links between the pages were relative (./search/, ../), which resolved to a
different page depending on the trailing slash of the current URL; they are
absolute now.
.
refactor(contacts): move the contact page to Twig
The first form to move, so two components of the theme come with it: the
validation messages of a field and the consent checkbox. Both print each message
as text and put the line breaks in the template, instead of joining messages with
a tag into one unescaped string.

The sanitized title of a consent is Twig\Markup now, which is what lets the
checkbox print it as it is under autoescape.

The phone of the tel: link is normalized by the DTO rather than by a preg_replace
in the middle of the markup.

The admin pages of the module stay on Plates: templates are dispatched one by
one, so the two halves of a module move independently.
.
docs(agents): add the Twig migration recipe
What the five migrated modules settled: the layout of a migrated module, the
review every output goes through, and the four traps that cost a page each —
the __call() of an Eloquent model, the truthiness of Markup, a layout variable
shadowing the data of the page, and string literals the escaper leaves alone.
.
refactor(online): move the visitor lists to Twig
The four pages share one template each way, so the visitor row and the tabs
become components of the module.

Where a visitor is is a link assembled from the places configuration, so both
formatters behind it return Twig\Markup and the templates print it as it is. The
forum one returns null instead of an empty string: markup is an object, and an
object is truthy however empty it is — a caller could no longer tell "no place"
from "nothing to show".

The IP and browser of a visitor were guarded by rights >= 3 with a second, always
true check for any rights inside; only the outer one survives.
.
fix(docker): send the charset with stylesheets
text/css is not among the default charset_types of nginx, so the stylesheets went
out as "text/css" with no encoding while the pages carried UTF-8. A browser is
then free to read them as latin1, and the breadcrumb divider showed up as ».
.
fix(view): keep layout variables from shadowing page data
A {% set %} in a layout lives in the context its blocks are rendered in, so
"notifications" set for the sidebar counters replaced the list of the
notifications page, and the page died on the first item.

Every variable a layout keeps for itself is now prefixed with layout_, and a test
holds the convention for the layouts still to be written.