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:

TagMeaningHTML platformMarkdown platformPlain-text platform
<b>…</b>Boldas is**…**the words
<i>…</i>Italicas is*…*the words
<u>…</u>Underlineas is__…__ or the wordsthe words
<s>…</s>Strikethroughas is~~…~~the words
<a href="…">…</a>A linkas is[…](…)words (url)
<code>…</code>Inline code (an access code, a handle)as is`…`the words
<pre>…</pre>A block of preformatted textas isfenced blockthe words
<blockquote>…</blockquote>A quoted message (a support reply)as is> …the words, indented

Two rules hold for every platform:

  1. 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.
  2. 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 (&amp;, &lt;); 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::$meta for its own Messenger to read.
  • Truncate. Length is the manifest's messaging.max_length and 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?

On this page

Subscriby is a product designed by you — for you.

No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request