Download the PHP package rlnks/php-mail-tree without Composer
On this page you can find all versions of the php package rlnks/php-mail-tree. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download rlnks/php-mail-tree
More information about rlnks/php-mail-tree
Files in rlnks/php-mail-tree
Package php-mail-tree
Short Description A PHP HTML email builder using an intuitive object-tree nesting approach. Structure your email exactly like you think it — each node is a named property on its parent.
License MIT
Homepage https://github.com/rlnks/php-mail-tree
Informations about the package php-mail-tree
rlnks/php-mail-tree
A PHP library for building HTML emails using an intuitive object-tree nesting approach. Structure your email exactly the way you think it — each node is a named property on its parent, and the hierarchy of your PHP code mirrors the hierarchy of the rendered HTML.
Installation
Requires PHP 8.2+.
The concept
Most email builders make you concatenate strings or call sequential methods. This library lets you build an object tree instead. Assign child nodes as named properties — the name you choose becomes documentation.
Each node knows its place in the tree. Call $email->build() once and the entire tree renders itself recursively, cascading styles top-down.
Structure-first pattern
Because nodes are just PHP object properties, you can declare the email skeleton first, then fill in the content separately. This separates layout decisions from copy decisions — exactly like drag-and-drop in a visual builder, but in code.
Why this matters:
- You can comment out an entire section (
// $email->body->features = …) without touching content code. - Content writers and layout developers can work in different blocks of the same file.
- The skeleton reads like a wireframe —
banner → gap → intro → gap → features → divider → footer— matching the mental model of a designer. - Nodes assigned later (step 2) are still accessible via
->property access becauseHasChildrenstores them in an array under the hood. Order of assignment doesn't matter; render order is insertion order.
Node management
Every node supports a full set of tree-management methods. All methods return $this (or the affected node) so they can be chained.
Visibility — hide() / show()
A hidden node is completely skipped during rendering. It stays in the tree, so it can be revealed again or toggled conditionally.
Reordering — moveUp() / moveDown() / moveToIndex()
Nodes track their parent automatically when assigned via ->. Move methods operate on the parent's child list immediately.
moveUp/moveDown accept an optional $steps argument (default 1). Positions clamp at boundaries — no wrap-around.
Relative positioning — insertBefore() / insertAfter()
Position a node relative to a named sibling instead of an absolute index.
The sibling is identified by the property name it was assigned under.
Lifecycle — detach() / replaceWith() / duplicate()
Inspection — getChildren()
Returns a snapshot of all direct children, keyed by their property name.
Note:
Imagesupportshide()/show()but not the tree-manipulation methods (move*,insertBefore/After,detach,replaceWith,duplicate,getChildren), since it cannot have children.
Quick start with StyleSheet + Presets
Complete example
A production-ready multi-file structure that demonstrates every major pattern at once: separated styles and translations, skeleton-first construction, images and links defined as named variables, a single build() call, and multiple output variants from that one render.
styles.php — named section styles
translations.php — translatable strings
email.php — the template
What this demonstrates:
styles.phpandtranslations.phpare the single sources of truth for their respective concerns.- The skeleton (step 3) reads like a wireframe:
header → gap → hero → gap → intro → gap → feature → gap → divider → footer. - Images and links are defined as named variables — easy to update centrally and reuse.
- Workflow A (
build()per locale) gives full highlighting support. Workflow B (resolve()post-build) is lightweight and suits ESP/PHP export formats. $t->resolve($base, 'tags')is what you send to Mailchimp, Klaviyo, or any ESP that has its own merge-tag system — the{{key}}placeholders act as the handoff format.$t->resolve($base, 'php')produces a self-contained PHP file suitable for inclusion in a dynamic mailer that reads client data at send time.
StyleSheet
StyleSheet is the central style brain of the email. It does two independent things:
- Theme → style arrays — you store brand tokens once; generator methods translate them into the CSS property arrays that each node class expects.
- Named-style registry — a
define/getsystem that replaces repeating raw style arrays across your template.
Vocabulary 1 — Theme variables (design tokens)
The StyleSheet constructor takes design tokens, not CSS properties. Tokens are PHP camelCase names that represent a brand concept rather than a specific CSS rule:
| Token | Default | What it represents |
|---|---|---|
primaryColor |
#333333 |
Brand accent (headings, links, buttons) |
textColor |
#444444 |
Body copy color |
bgColor |
#f0f0f0 |
Outer page/body background |
containerBg |
#ffffff |
Inner email content background |
borderColor |
#dddddd |
Dividers, borders |
fontFamily |
Arial, … |
Font stack |
baseFontSize |
14px |
Default paragraph size |
lineHeight |
150% |
Default line height |
containerWidth |
600 |
Email width in px (integer) |
marginWidth |
30 |
Left/right gutter width in px |
spacerHeight |
20px |
Default vertical spacer height |
spacerBg |
'' |
Spacer background — empty falls back to containerBg |
buttonRadius |
4px |
Button corner radius |
buttonHeight |
50 |
Button height in px (integer) |
buttonWidth |
200 |
Button width in px (integer) |
buttonFontSize |
16px |
Button label size |
darkBgColor |
#1a1a1a |
Dark mode outer background |
darkContainerBg |
#2d2d2d |
Dark mode container background |
darkTextColor |
#dddddd |
Dark mode body copy color |
darkPrimaryColor |
'' |
Dark mode accent — empty falls back to primaryColor |
darkBorderColor |
#444444 |
Dark mode divider/border color |
Why camelCase tokens instead of CSS properties directly?
A single token like primaryColor maps to CSS in many different places — the color of an h1, the background-color of a button, the border-color of a divider. If you stored CSS properties directly you'd have to repeat and keep them in sync. The token is defined once; each generator method puts it in the right property for the right context.
Vocabulary 2 — Style keys (semantic node keys)
Style arrays throughout the library are always nested under a semantic key that identifies which node type or HTML element receives the styles. The value is a flat array of standard CSS properties (background-color, font-size, etc.).
Why container and column instead of table and td?
Email HTML wraps a logical "container" in multiple elements (<table><tbody><tr>) and Outlook sometimes adds MSO conditional wrappers around it. Using the key container means you're styling the concept rather than a specific tag. It also avoids confusion with the CSS table and td display values. The library owns the mapping; you use the semantic vocabulary.
Generator methods — theme → style arrays
These methods read your tokens and produce a ready-to-use style array with the correct semantic keys and standard CSS properties. Pass the return value directly to the matching constructor.
emailStyle() — used internally by new EmailDocument($sheet)
Pass $sheet directly to EmailDocument — it calls emailStyle() internally and also registers itself as the global default so all preset make() calls in the same script auto-resolve it without needing sheet: $sheet.
Produces (for the theme above):
This style array becomes the root of the cascade — every child node in the tree inherits from it.
| Generator method | Pass to | What it produces |
|---|---|---|
emailStyle() |
called internally by new EmailDocument($sheet) |
body bg, global font, heading + link + highlight colors |
containerStyle() |
new Container(…) |
container key with width, bg, table resets |
outerContainerStyle() |
new Container(…, mso: true) |
same + no bg (transparent outer wrapper) |
marginColumnStyle() |
new Column(…) |
column key with gutter width |
bodyColumnStyle() |
new Column(…) |
column key with inner content width |
twoColStyle() |
new Column(…) |
column key with half inner width |
threeColStyle() |
new Column(…) |
column key with one-third inner width |
spacerStyle($height) |
new Column(…) |
font-size/line-height/height triple for Outlook |
buttonContainerStyle() |
new Container(…) |
bg-color, border-radius, center alignment |
All generators accept an optional $overrides array so you can tweak one property without redefining the whole block:
Named-style registry
Store reusable style combinations under a name and apply them via get(). Keys inside the stored style follow the same semantic vocabulary (container, column, h1, div, etc.) and values are standard CSS properties.
| Method | Description |
|---|---|
define(string $name, array $style) |
Store a named style; returns $this for chaining |
get(string $name, array $overrides = []) |
Retrieve — optionally merged with one-off overrides |
extend(string $name, array $extra) |
Merge additional keys into an existing definition |
has(string $name) |
Check whether a name is registered |
addWebFont(string $url, string $fontName = ''): static |
Register a web font URL; when $fontName is provided it is prepended to fontFamily in the theme automatically. EmailDocument injects the matching <link> tags in <head>. |
webFontLinks(): string |
Return the <link> HTML tags (including Google Fonts preconnect) for all registered web fonts. Called automatically by EmailDocument. |
addResponsiveRule(string $selector, array $properties): static |
Add a custom rule that will be emitted inside the @media block of responsiveCss(). |
responsiveCss(int $breakpoint = 620, bool $darkMode = false): string |
Return the full <style> string for Body::setCSS(). |
darkModeCss(): string |
Return only the dark-mode CSS block (@media (prefers-color-scheme: dark) + Outlook.com overrides). Called automatically by responsiveCss(darkMode: true). |
useAsDefault(): static |
Register this instance as the process-wide default — all presets resolve it automatically when no $sheet is passed. Called automatically by new EmailDocument($sheet). Returns $this for chaining. |
static getDefault(): ?static |
Return the current default, or null if none set |
static clearDefault(): void |
Clear the default (useful in tests) |
Shared styles file
The named-style registry is designed to live in a dedicated file you require at the top of each template — the PHP equivalent of a CSS stylesheet. This keeps all brand-specific colours, backgrounds, and typography rules out of your template logic.
my_styles.php
email.php
applyStyle() — targeting specific children
When a named style needs to address a specific named child of a section rather than the section itself, use applyStyle() instead of setStyle(). The optional 'children' key maps child property names to their own style arrays — other children are unaffected.
All keys except 'children' are applied to the node itself via setStyle(). The 'children' map recurses — if a child also has HasChildren, its entry can contain another 'children' key.
responsiveCss(int $breakpoint = 620)
Returns a complete <style> string to inject via $email->body->setCSS(…). It is the only place in the package that outputs a <style> block — everything else is inline CSS.
What it includes:
- Client resets:
bodymargin/padding, Outlook.com.ExternalClass, globaltablecollapse +mso-table-lspace - Image resets:
display:block,border:0, bicubic interpolation - Apple Mail: kills auto-detected blue links (
a[x-apple-data-detectors]) - Gmail: kills blue link rewrite (
u + .body a) - Samsung Mail: kills link rewrite (
#MessageViewBody a) @mediaresponsive:devicewidthtables → 100%,section-body→ 100%,col-2/col-3/col-4/col-5→display:block, fluid images, base font bump- Utilities:
hidden-sm(hide on mobile),show-sm(show only on mobile) - Dark mode:
responsiveCss(darkMode: true)emits a@media (prefers-color-scheme: dark)block + Outlook.com overrides driven by thedark*theme tokens
Translator
Translator handles all text placeholder resolution — multi-language versions, template tag output, PHP snippet generation, and runtime variable injection.
There are two workflows. Choose based on whether you need per-section **...** highlight overrides.
Workflow A — attached translator (recommended)
Attach $t to EmailDocument. Each build() call resolves placeholders and processes **...** highlighting in the same pass, so per-section highlight CSS overrides work correctly.
Since $t is an object, setLocale() propagates to the attached reference — no re-attachment needed between renders.
Workflow B — post-processor (build once, resolve many)
Build the tree once with {{placeholders}} intact, then resolve into as many variants as needed. **...** in translation values won't be highlighted (only static **...** in Text nodes is), but this workflow lets you produce tags, php, and custom ESP formats cheaply.
You can mix both: use workflow A for the rendered locales, and workflow B for tags/php exports from the same base.
Translations file
A plain PHP file that returns a keyed array. Each key maps to an array of locale → value pairs. Only include the locales you actually have — a missing locale keeps the {{key}} placeholder intact in the output.
Load it:
Using placeholders in nodes
Write {{key}} anywhere in a text string. Keys support dots (customer.first_name) and optional spaces inside braces ({{ key }}).
With an attached translator (workflow A) placeholders are resolved inside build(). With workflow B they survive build() and are resolved by resolve() afterward.
Runtime values — bind()
bind() injects a value that applies to every locale (same across languages — client name, order ID, URL, etc.). Bindings take priority over translations for all non-php locales.
Built-in modes
| Mode | Behaviour |
|---|---|
'fr', 'en', any locale |
Replace {{key}} with the stored translation; keep tag if absent |
'tags' |
Identity — HTML returned unchanged, all {{key}} intact |
'php' |
Replace {{key}} with php entry from data, or generated snippet |
php mode is designed for generating PHP template files (.php mail templates for other systems). It is protected: php entries never appear in toXml() output and php is excluded from locales().
The default PHP snippet is configurable:
XML export
toXml() produces an XML file of all translations — useful for handing off to a translation service or storing in a CMS. The php locale is always excluded.
Output format:
Complete workflow example
API reference — Translator
| Method | Description |
|---|---|
__construct(array $data = [], string $open = '{{', string $close = '}}', string $phpSnippet = …) |
Initialize with optional data and delimiters |
load(array $data): static |
Merge a full translations array (from require 'translations.php') |
define(string $key, array $values): static |
Define or extend a single key |
bind(string $key, string $value): static |
Runtime value — same across all locales, overrides translations |
bindMany(array $values): static |
Batch bind |
setLocale(string $locale): static |
Set the active locale; propagates immediately if $t is attached to EmailDocument |
getLocale(): string |
Return the current locale |
get(string $key, ?string $locale = null): string |
Resolve a single key |
resolve(string $html, ?string $locale = null): string |
Replace all {{key}} placeholders in an HTML string (workflow B) |
locales(): array |
All registered locales excluding protected ones (php) |
toXml(array $locales = []): string |
Export to XML; php always excluded |
useAsDefault(): static |
Register as process-wide default — called automatically by EmailDocument::build() |
static getDefault(): ?static |
Return the current default, or null |
static clearDefault(): void |
Clear the default (useful in tests) |
Placeholder syntax: Keys support word characters, dots, and optional surrounding spaces — {{key}}, {{ key }}, {{customer.first_name}} are all valid.
Preset components
All presets are static factories that return fully configured node trees. Use deepclone() to reuse a preset multiple times.
Spacer
Every spacer sets font-size, line-height, and height to the same value + mso-line-height-rule: exactly — the only reliable way to enforce pixel-perfect height in Outlook.
Background color: when a $sheet is provided, the spacer inherits spacerBg from the theme. If spacerBg is empty (the default), it falls back to containerBg — so spacers visually blend with the surrounding sections without any extra configuration. Pass bg: explicitly to override per-instance. Without a sheet, the default is transparent.
Divider
Renders a border-bottom on a collapsed <td> (font-size:0; line-height:0; height:0) — the only approach that works reliably across Outlook, Gmail, and Apple Mail. The container is bounded by max-width: containerWidth so it stays within the email column on wide viewports.
Button — VML hybrid
What it generates:
- Outlook 2007–2019 / 365:
<v:roundrect>VML shape witharcsizederived fromborder-radius ÷ shorter-dimension. The entire colored shape is clickable (hrefon the roundrect). - All other clients:
<a>withdisplay:inline-block,background-color,border-radius, and explicitline-heightfor height control.
VML arcsize formula: round(borderRadiusPx / min(width, height) * 100), capped at 50.
FullWidthImage
All parameters are optional — $src, $alt, and $href all default to ''. The tree structure is always col → link (Anchor) → img (Image) regardless of whether a link is used, so ->col->link and ->col->link->img are always accessible for deferred assignment.
When href is empty the Anchor renders transparently — its children output without any <a> wrapper. Set a link later via ->col->link->setLink($url).
No margin columns — image runs edge-to-edge. Uses width="600" HTML attribute alongside CSS width:100% so Outlook (which ignores max-width) still constrains the image. display:block eliminates the 4px baseline gap beneath inline images.
Section — 1 column with margins
Structure: Container(600px) > Column(30px) + Column(540px, section-body) + Column(30px).
The section-body class is targeted in responsiveCss() for full-width on mobile.
TwoColumn — responsive
Columns tagged col-2 → display:block; width:100% on mobile via responsiveCss().
ThreeColumn — responsive
Columns tagged col-3 → stack to full-width on mobile. If (containerWidth − margins) is not divisible by 3, the remainder pixels go to col1 to avoid sub-pixel Outlook overflow.
NColumn — N-column responsive grid
Columns tagged col-N (e.g. col-4) → display:block; width:100% on mobile. Remainder pixels after integer division are added to the first column.
ProductCard
E-commerce product card: image + title + description + price + optional CTA button. All parameters are optional — omit any you don't need. Designed to sit inside a TwoColumn, ThreeColumn, or NColumn cell (pass ->col to plug into the column slot), or standalone inside a Section for a full-width featured product.
AlertBar
Full-width colored banner with an optional icon prefix. Color and icon default to the type. Pass bgColor: / textColor: / icon: to override any part.
BulletList
Table-based bullet list compatible with all email clients. Each item renders as a two-cell row: bullet character + text. Place inside a Section->body — it is a content component, not a standalone layout block.
Coupon
Centered promo code block with a dashed border and a copyable code. Designed to stand alone at the email body level.
DataTable
Structured data table with header row. Carries the datatable CSS class, which responsiveCss() targets to reduce padding and font size on narrow viewports. Place inside a Section->body — it is a content component, not a standalone layout block.
Quote
Blockquote-style callout with a left border accent and optional author attribution. Uses primaryColor for the border.
PricingTable
Side-by-side pricing tier cards. The highlighted column ('highlight' => true) renders with a filled primaryColor background and no border; other columns use a 1px border. Column widths are computed to exactly fill the container without overflow.
StepIndicator
Horizontal step-progress tracker with numbered circles and connector lines. Steps before $current are "completed" (filled + checkmark), $current is "active" (filled), steps after are "upcoming" (hollow). Circle border-radius is applied to the <td> (not the <table>) for consistent 32×32px rendering across clients. Outlook renders the circles as squares — acceptable fallback.
SocialBar
Row of social icon links. Built-in platforms: facebook, twitter, instagram, linkedin, youtube, pinterest, tiktok, github. Each icon is an <a>-wrapped <img> with border="0" and moz-do-not-send="true". Replace the placeholder icon URLs with CDN-hosted images for production.
VideoBlock
Clickable video thumbnail with a centered play-button overlay. Since video cannot play inline in email, this renders a linked image that opens the video in a browser. Uses FullWidthImage internally.
Advanced nodes
ConditionalBlock
Wraps children in an MSO/IE conditional comment block. Use it to show or hide content specifically in Outlook 2007–2019, or to show content in all clients except Outlook.
Common condition strings: 'mso', '!mso', 'gte mso 9', '(gte mso 9)|(IE)'. When the condition starts with !, the inverted syntax (<!--[if !mso]><!-->…<!--<![endif]-->) is used automatically.
ForLoopBlock
Renders a list of items using a builder callback. The builder receives each item and its zero-based index and must return a Renderable node or a raw HTML string. No wrapper element is added.
Use ForLoopBlock when the node structure itself varies per item. For uniform repetition (same structure, different data), prefer DataTable, BulletList, or PricingTable.
RawHtml
Escape hatch that emits a pre-built HTML string verbatim as a Renderable node. The style cascade is intentionally bypassed.
Use sparingly — only when the tree primitives cannot express a particular email-safe structure.
HtmlImporter
Converts an existing HTML email into ready-to-run MailTree PHP code. The converter parses the HTML bottom-up, extracts theme tokens, groups columns into section rows, propagates common inline styles upward, and generates a full PHP template.
What the generated file includes:
- Auto-detected
StyleSheetwithbgColor,containerBg,primaryColor,fontFamily,baseFontSize, andcontainerWidthextracted from the source HTML. - Named
$imgNand$linkNvariables for all images and links found. - Email skeleton:
EmailDocument,Body, and one section per detected row —Spacer,Divider,Section,TwoColumn,ThreeColumn, orNColumnas appropriate. - Style setters generated from propagated and per-column inline styles.
- Content assignments:
Text,Image,Anchor,Button,BulletList,Divider. - A
Translatorstub and abuild()+resolve()call at the end.
The output is a starting point — review and adjust structure, styles, and placeholder keys before using in production.
Style cascade
Styles flow top-down through the tree at render time. Each node receives its parent's full style array and merges its own overrides on top.
Style arrays are always keyed by element type:
deepclone
The deepclone() helper (auto-loaded globally) deep-copies any node tree. Use it to safely reuse pre-built blocks:
API reference
EmailDocument
| Method | Description |
|---|---|
__construct(?StyleSheet $sheet = null, ?Translator $t = null) |
$sheet applies the theme and registers itself as the global default. $t attaches a translator — placeholders and **...** are resolved on every build() call. |
setSubject(string $title) |
<title> tag and email subject |
addLink(string $href, string $rel) |
<link> in <head> (Google Fonts, etc.) |
setStyle(array $style) |
Merge additional styles |
getStyle(): array |
Full resolved style array |
build(array $style = [], int $indent = 0, ?Translator $t = null, ?string $locale = null): string |
Render the document. With an attached translator the locale set on $t is used. Pass $t/$locale explicitly to override for a single call. |
The methods below marked † are available on all nodes that can have children (Body, Container, Column, Text, Anchor). Image only supports hide()/show().
Body
| Method | Description |
|---|---|
__construct(array $style = []) |
Style overrides for <body> |
setCSS(string $css) |
Inject <style> block — use $sheet->responsiveCss() |
setStyle(array $style) |
Merge additional styles |
getStyle(): array |
Full resolved style array |
hide() / show() |
Toggle visibility in the rendered output |
moveUp(int $steps = 1) † |
Move earlier in parent's child list |
moveDown(int $steps = 1) † |
Move later in parent's child list |
moveToIndex(int $index) † |
Jump to absolute position (0 = first) |
insertBefore(string $key) † |
Reposition just before the named sibling |
insertAfter(string $key) † |
Reposition just after the named sibling |
detach() † |
Remove from parent; returns self for re-attachment |
replaceWith(Renderable $node) † |
Swap out in-place; returns the replaced node |
duplicate() † |
Clone + insert after self; returns the duplicate |
getChildren(): array † |
Snapshot of direct children keyed by property name |
applyStyle(array $style) † |
Apply semantic keys to self + optionally target named children via 'children' key; returns $this |
Container
Renders as <table><tbody><tr>. Children should be Column instances.
| Method | Description |
|---|---|
__construct(array $style = [], bool $mso = false) |
mso: true wraps with Outlook conditional comments |
setClass(string $class) |
CSS class on <table> |
setID(string $id) |
id on <table> |
setStyle(array $style) |
Merge style overrides |
getStyle(): array |
Full resolved style array |
hide() / show() |
Toggle visibility in the rendered output |
moveUp / moveDown / moveToIndex † |
See Body above |
insertBefore / insertAfter † |
See Body above |
detach / replaceWith / duplicate / getChildren † |
See Body above |
applyStyle(array $style) † |
See Body above |
Column
Renders as <td>.
| Method | Description |
|---|---|
__construct(array $style = []) |
['column' => [...]] |
setClass(string $class) |
CSS class on <td> (used for responsive: col-2, col-3, section-body) |
setStyle(array $style) |
Merge style overrides |
getStyle(): array |
Full resolved style array |
hide() / show() |
Toggle visibility in the rendered output |
moveUp / moveDown / moveToIndex † |
See Body above |
insertBefore / insertAfter † |
See Body above |
detach / replaceWith / duplicate / getChildren † |
See Body above |
applyStyle(array $style) † |
See Body above |
Text
Wraps content in any inline or block tag. With $tag = null acts as a transparent wrapper. Supports inline **...** markup — see Text highlighting.
| Method | Description |
|---|---|
__construct(string $text, ?string $tag = null, array $style = []) |
$tag: h1, h2, h3, div, p, span, etc. **...** in $text is highlighted at render time |
setStyle(array $style) |
Merge style overrides |
getStyle(): array |
Full resolved style array |
hide() / show() |
Toggle visibility in the rendered output |
moveUp / moveDown / moveToIndex † |
See Body above |
insertBefore / insertAfter † |
See Body above |
detach / replaceWith / duplicate / getChildren † |
See Body above |
applyStyle(array $style) † |
See Body above |
Image
| Method | Description |
|---|---|
__construct(string $src = '', string $alt = '', array $style = []) |
|
setSrc(string $src, ?string $alt = null) |
Update src (and optionally alt) after construction |
setStyle(array $style) |
Merge style overrides |
getStyle(): array |
Full resolved style array |
hide() / show() |
Toggle visibility in the rendered output |
Emits width/height HTML attributes only when the CSS value is a plain integer (not %, auto).
Anchor
| Method | Description |
|---|---|
__construct(string $href = '', array $style = []) |
href = '' → renders transparently (no <a> tag, children output directly) |
setLink(string $href) |
Update href after construction; set to '' to remove the link wrapper |
setStyle(array $style) |
Merge style overrides |
getStyle(): array |
Full resolved style array |
hide() / show() |
Toggle visibility in the rendered output |
moveUp / moveDown / moveToIndex † |
See Body above |
insertBefore / insertAfter † |
See Body above |
detach / replaceWith / duplicate / getChildren † |
See Body above |
applyStyle(array $style) † |
See Body above |
Text highlighting
Any Text node supports inline **...** markup. At render time, content wrapped in double asterisks is output as a <span> with the highlight style from the cascade — by default font-weight:bold + primaryColor.
With an attached translator (workflow A), {{customer.first_name}} is resolved first, then **...** is applied with the section's CSS — so per-section highlight overrides work correctly:
`...` in translation values — also works with workflow A, because placeholders and highlighting are resolved in the same build() pass:
With workflow B ($t->resolve() post-build) the highlight style from the cascade is no longer available, so **...** that arrives inside a translation value won't be processed. Use workflow A, or write HTML directly in the translation value.
Customizing the highlight style — override the highlight key in your StyleSheet. It follows the same base-style pattern as h1, container, etc.:
Notes:
- Requires a StyleSheet attached to
EmailDocument— without one,**...**is left as-is. - A
str_containscheck short-circuits the regex when**is absent — no cost on nodes that don't use the feature.
Email HTML best practices applied
Every class and preset follows current best practices:
border-collapse: collapse+border-spacing: 0+mso-table-lspace/rspace: 0pton all tablescellpadding="0" cellspacing="0"on all<table>elementstable-layout: fixedprevents cell width negotiation in Outlookdisplay: blockon<img>eliminates the 4px baseline gapborder="0"HTML attribute on<img>prevents blue link-borders in Outlook and old clients that ignore inline CSSmoz-do-not-send="true"on<img>prevents Thunderbird from attaching images as file attachments- Numeric
width/heightHTML attributes on images alongside CSS (Outlook ignoresmax-width) htmlspecialchars($href, ENT_QUOTES)on all link hrefs — prevents XSS and invalid HTML from URLs with&query params- Spacers:
font-size,line-height,heightall equal +mso-line-height-rule: exactly; bounded bymax-width: containerWidth; background defaults tocontainerBgviaspacerBgtheme token - Dividers:
border-bottomon<td>with collapsed font/line/height; bounded bymax-width: containerWidth - Buttons: VML
<v:roundrect>for Outlook +inline-block <a>for all others;arcsizederived fromborderRadius ÷ min(width, height) - Rounded circles (StepIndicator):
border-radius: 50%on the<td>— not the<table>— for consistent content-box dimension rendering in all clients. Outlook renders them as squares (acceptable fallback) - DataTable: carries a
datatableCSS class soresponsiveCss()can reduce padding and font size on narrow viewports without breaking the inline-CSS-first model - Responsive:
@mediaquery +display: blockcolumn stacking;datatableclass for table cell padding reduction on mobile - Client resets: Outlook.com ExternalClass, Apple data detectors, Gmail
u + .body a, Samsung Mail - Dark mode:
responsiveCss(darkMode: true)emits a@media (prefers-color-scheme: dark)block driven by thedark*theme tokens
Using as a back-end for a visual builder
Because the tree is plain PHP objects, it maps naturally to a JSON payload from a drag-and-drop front-end:
License
MIT