@yaebal/fmt

html and md tagged templates that parse Telegram's markup subset into real entities — with interpolations auto-escaped so user input can never break your formatting.

install

terminal
pnpm add @yaebal/fmt

why

core ships format (entity builders: bold(), link()). @yaebal/fmt adds the parser angle — write familiar markdown or HTML, get the same { text, entities } back. Both avoid parse_mode entirely, so there is nothing to escape.

basic.ts
import { html, md } from "@yaebal/fmt";

// parses into MessageEntity[] — no parse_mode, nothing to escape
ctx.send(html`<b>hello</b> <a href="https://yaebal.dev">docs</a>`);
ctx.send(md`**hello** and \`code\` and ||spoiler||`);

auto-escaped interpolation

this is the headline. a $${string} interpolation is inserted as literal text — its *, <, ` are never re-parsed as markup. user input cannot inject entities or break the message.

safe.ts
const name = "<script>**hax**";

// the interpolation is inserted as LITERAL text — never re-parsed
ctx.send(html`hi <b>${name}</b>`);
// → text: "hi <script>**hax**", one bold entity. no injection possible.
composes with core. if an interpolation is itself a FormatResult (e.g. from bold() / link()), it's merged in with its offsets shifted — so dynamic links don't need attribute parsing, just drop in a link().
compose.ts
import { html } from "@yaebal/fmt";
import { bold, link } from "@yaebal/core";

// a FormatResult sub (from core's builders) is MERGED, offsets shifted
ctx.send(html`welcome ${bold(user.name)} — ${link("open", url)}`);

html tags

supported tags
b / strong          → bold
i / em              → italic
u / ins             → underline
s / strike / del    → strikethrough
code                → code
pre                 → pre
a href="…"          → text_link
span.tg-spoiler     → spoiler
tg-spoiler          → spoiler
blockquote          → blockquote

markdown syntax

supported syntax
**bold**       __italic__      ~~strike~~
||spoiler||    `code`          [text](url)
\`\`\`lang
multi-line pre
\`\`\`
this is a Telegram-oriented dialect (not full CommonMark): same delimiter can't nest in itself, and dynamic links compose via core's link() rather than [x]($${url}) (the url interpolation would be escaped as text).

api

exportsignaturereturns
htmltagged templateFormatResult
mdtagged templateFormatResult
htmlToEntities(s: string)FormatResult
mdToEntities(s: string)FormatResult