TextRenderer
Turning the core's canonical HTML into what one platform accepts — the eight tags, what a renderer must keep, and how HTML, Markdown and plain-text platforms each do it.
Subscriby\Connector\Contracts\Ports\TextRenderer. Required of every connector.
Everything the core says to a person is written once, in ten languages, in a canonical HTML subset, and rendered down here per platform. That is what lets years of existing copy move to a new platform without a migration: Telegram keeps the HTML, Discord turns it into Markdown, a platform with no formatting strips it to the words.
The method
public function render(string $canonicalHtml): string;The canonical subset
The core's composer emits these eight tags and nothing else:
| Tag | Meaning | HTML platform | Markdown platform | Plain-text platform |
|---|---|---|---|---|
<b>…</b> | Bold | as is | **…** | the words |
<i>…</i> | Italic | as is | *…* | the words |
<u>…</u> | Underline | as is | __…__ or the words | the words |
<s>…</s> | Strikethrough | as is | ~~…~~ | the words |
<a href="…">…</a> | A link | as is | […](…) | words (url) |
<code>…</code> | Inline code (an access code, a handle) | as is | `…` | the words |
<pre>…</pre> | A block of preformatted text | as is | fenced block | the words |
<blockquote>…</blockquote> | A quoted message (a support reply) | as is | > … | the words, indented |
Two rules hold for every platform:
- Words survive. Whatever the platform cannot show, the text inside the tag stays. A renderer that drops a
<u>element's content loses meaning in ten languages at once. - Plain text is the identity. A body with no tags renders as itself, byte for byte. Do not escape, trim or re-wrap it.
Entities arrive HTML-encoded (&, <); a Markdown or plain-text renderer decodes them, an HTML renderer passes them through.
What a renderer must not do
- Compose. The renderer receives a finished message. It never adds a signature, a greeting or a footer; a connector that wants those fills a UI slot or sets
Message::$metafor its ownMessengerto read. - Truncate. Length is the manifest's
messaging.max_lengthand the core splits a longer body into parts before rendering, so a renderer never sees a body over the limit. - Escape user content twice. The core already encoded what it put inside the tags.
How Telegram does it
The canonical subset is Telegram's own HTML parse mode, so rendering is the identity: every tag the core may emit is one Telegram accepts as written. The allow-list that used to strip unknown tags now belongs to the core's composer, which never emits them.
What the kit checks
text.plain_text_survives: render('Hello, world') returns exactly Hello, world. text.canonical_sample_renders: the sample <b>Bold</b> <i>italic</i> <u>underline</u> <s>struck</s> <a href="https://example.test">link</a> <code>code</code> <pre>pre</pre> <blockquote>quote</blockquote> renders to a non-empty string that still contains every one of the eight words.
Test with real copy
Pull a few of the core's longest messages (a sale notice, a pass reminder, a support reply with a quote) through your renderer in your own tests and read the result on the platform. The kit proves the words survive; only your eyes prove the message still reads well.
How is this guide?
FailureClassifier
Reading why the platform refused a call — the seven kinds the core acts on, what each makes the core do, and how to map a platform's error vocabulary onto them.
SettingsSchema
The install and settings forms as data — when the manifest binds the port for you, when to write your own, and how the Field objects reach the dashboard, the API and the apps.