A pure-PHP library for parsing and rendering Minecraft chat text in every
format the live ecosystem uses: JSON text components, SNBT text components,
legacy §/&-coded strings, MiniMessage, and HTML.
- Five input formats: JSON text components, SNBT text components,
legacy
§/&-coded strings (with the BungeeCord and Essentials hex extensions), and MiniMessage; readers are permissive by default with an opt-in strict mode - Five output formats: JSON, SNBT, legacy, MiniMessage, and HTML; the HTML renderer is configurable for inline styles, CSS classes, or a hybrid, with a bundled reference stylesheet
- Dialect-targeted rendering: pick the Minecraft schema generation
(1.20, 1.20.5, 1.21.4, 1.21.5, 26.1) and the JSON/SNBT renderers emit
the canonical wire shape — correct event-key casing, allowed click and
hover actions,
shadow_colorsupport, andshow_itempayload form - Round-trip integrity: unknown click actions survive as
CustomClickEvent; MiniMessage gradient/rainbow/transition tags round-trip verbatim through the MiniMessage renderer (other renderers lower them to plain text spans) - Diagnostics, not exceptions: lossy and non-canonical inputs record
a
Diagnosticagainst the parse or render result rather than throwing, with stable dotted codes (parse.minimessage.unknown-tag,render.legacy.translate-dropped,render.html.url-scheme-blocked, ...) suitable for programmatic filtering - Six native component types (Text, Translate, Keybind, Score, Selector, NBT) plus three MiniMessage-native transformations (Gradient, Rainbow, Transition)
- Style inheritance matching vanilla Minecraft semantics, with a
tri-state Decorations record so children can suppress an inherited
decoration (
<!bold>) - Fluent builder (
TextBuilder) and a static facade (Text) for programmatic construction without manual AST assembly - Translation resolution via a caller-supplied locale source, with a
printf-style placeholder helper (
%s/%1$s/%%) - HTML safety: every text leaf is HTML-escaped, URL schemes in
OpenUrlare validated against a configurable allowlist (defaulthttp/https/mailto), and obfuscated text is exposed via adata-mc-textattribute for client-side animation - PHPStan level max with full generic annotations; 100% mutation score under Infection (all surviving mutants documented as observationally equivalent)
- PHP 8.5 or newer
cartograph/minecraft-nbt^1.0 (resolved automatically by Composer)
No PHP extensions are required.
Install via Composer:
composer require cartograph/minecraft-textParse a legacy MOTD and render it as themed HTML:
use Cartograph\Text\Text;
$html = Text::renderHtml(Text::parseLegacy("§6Welcome §eto §athe server"));
// <span class="mc-color-gold">Welcome </span>
// <span class="mc-color-yellow">to </span>
// <span class="mc-color-green">the server</span>That's a complete, runnable example. The bundled reference stylesheet
(Html::referenceStylesheet()) defines the default mc-color-* and
mc-bold rules so the output drops straight into any page.
Build a tree programmatically and serialise it as JSON for Minecraft:
use Cartograph\Text\Component\NamedColor;
use Cartograph\Text\Text;
$component = Text::text(
'Hello, ',
children: [
Text::text('world', style: Text::build()->color(NamedColor::Red)->build()->style()),
Text::text('!'),
],
);
echo Text::renderJson($component);
// {"text":"Hello, ","extra":[{"text":"world","color":"red"},{"text":"!"}]}Minecraft chat text is a tree of typed components. Each component carries some content (literal text, a translation key, a keybind, a score reference, a selector, or an NBT path) plus an optional style (colour, decorations, font, shadow colour, click/hover events, insertion text) and an "extra" list of child components that inherit the parent's effective style.
Vanilla Minecraft serialises this tree in three main ways:
- JSON text components, the wire format used by chat, signs, written books, advancement descriptions, and the SLP status response
- SNBT text components, the same shape encoded as stringified NBT for use in commands and data packs
- Legacy
§-codes, the pre-1.7 wire format still emitted by plugins, written into pre-1.13 worlds, and used by SLP MOTDs
The community uses two more:
- MiniMessage, a tag-language popularised by the Adventure library (Paper, Velocity), with native syntax for gradients, rainbows, and click/hover events
- HTML, for displaying chat on websites
This library reads, writes, and converts between all five. A shared,
immutable Component AST sits between the readers and the renderers,
so any input format can be re-rendered to any output format.
Output that targets Minecraft's JSON/SNBT wire format is dialect-
driven: a Profile for the target Minecraft generation decides which
fields are valid, which click/hover actions are allowed, what casing
event keys use (clickEvent vs click_event), and whether
shadow_color and the legacy NBT-string show_item payload are
supported. Fields the dialect cannot represent record a Diagnostic
and drop, or throw ProfileMismatchException under strict mode.
Lossy conversions (rendering a KeybindComponent to legacy, an
unresolved TranslateComponent to HTML, an unknown <wibble> tag in
MiniMessage) record dotted diagnostics on the result rather than
silently failing.
Every reader has two facade entry points: a one-shot version returning
just the parsed Component, and a *Result version returning a
ParseResult bundling the component with any diagnostics raised.
use Cartograph\Text\Text;
$c1 = Text::parseJson('{"text":"hi","color":"red"}');
$c2 = Text::parseSnbt('{text:"hi",color:"red"}');
$c3 = Text::parseLegacy('§chi');
$c4 = Text::parseMiniMessage('<red>hi</red>');
// Or with diagnostics:
$r = Text::parseJsonResult($input);
$component = $r->component;
$diagnostics = $r->diagnostics; // list<Diagnostic>Parsers are permissive by default — bare strings, list-as-component,
both clickEvent and click_event casings, both legacy and modern
show_item payloads, and unknown click actions all parse without
throwing. Pass new JsonOptions(strict: true) (or the per-format
equivalent) to reject non-canonical shapes.
use Cartograph\Text\Text;
echo Text::renderJson($c);
echo Text::renderSnbt($c);
echo Text::renderLegacy($c);
echo Text::renderMiniMessage($c);
echo Text::renderHtml($c);
// `*Result` variants return diagnostics alongside the text:
$r = Text::renderHtmlResult($c);
$html = $r->text;
$diagnostics = $r->diagnostics;The JSON and SNBT renderers take a JsonOptions / SnbtOptions with a
dialect field — pick the Minecraft schema generation you're targeting:
use Cartograph\Text\Json\JsonOptions;
use Cartograph\Text\Profile\JsonDialect;
use Cartograph\Text\Text;
echo Text::renderJson($c, new JsonOptions(JsonDialect::Mc1_20));
// Pre-1.21.5 wire shape: clickEvent (camelCase), legacy show_item, no shadow_color
echo Text::renderJson($c, new JsonOptions(JsonDialect::Mc26_1));
// Latest wire shape: click_event (snake_case), components-form show_item, shadow_colorTwo equivalent APIs:
The Text facade has a static factory for every component type:
use Cartograph\Text\Component\NamedColor;
use Cartograph\Text\Component\Style;
use Cartograph\Text\Component\Decorations;
use Cartograph\Text\Text;
$c = Text::text(
'Hello',
style: new Style(color: NamedColor::Gold, decorations: new Decorations(bold: true)),
);TextBuilder is a fluent, immutable helper for chained construction:
use Cartograph\Text\Build\TextBuilder;
use Cartograph\Text\Component\NamedColor;
use Cartograph\Text\Component\Events\OpenUrl;
use Cartograph\Text\Text;
$c = Text::build()
->append(TextBuilder::text('Click here')
->color(NamedColor::Aqua)
->underlined()
->onClick(new OpenUrl('https://example.com')))
->append(TextBuilder::text(' to continue.'))
->build();TranslationResolver walks a tree replacing every TranslateComponent
with its resolved text. The package ships no locale data — implement
the Translator interface against your own locale source:
use Cartograph\Text\Translation\TranslationResolver;
use Cartograph\Text\Translation\Translator;
$translator = new class implements Translator {
public function translate(string $key, ?string $locale = null): ?string
{
return match ($key) {
'chat.type.text' => '<%s> %s',
default => null,
};
}
};
$resolved = new TranslationResolver($translator)->resolve($component);The HtmlRenderer accepts an optional Translator via HtmlOptions and
runs the resolution pass automatically before walking the tree.
use Cartograph\Text\Html\EventStrategy;
use Cartograph\Text\Html\HtmlOptions;
use Cartograph\Text\Html\StaticPalette;
use Cartograph\Text\Html\StylingMode;
use Cartograph\Text\Text;
$opts = new HtmlOptions(
stylingMode: StylingMode::Inline, // Inline / Classes / Hybrid (default)
classPrefix: 'mc-',
events: EventStrategy::AnchorPlusData,
palette: StaticPalette::sign(), // chat (default) or sign
allowedUrlSchemes: ['https'], // default ['http','https','mailto']
);
echo Text::renderHtml($component, $opts);The bundled reference stylesheet (Html::referenceStylesheet()) defines
the default mc-color-*, mc-bold, mc-italic, etc. classes plus a
CSS-only animation for mc-obfuscated.
The public surface splits into the AST (Component namespace), the
five readers, the five renderers, the dialect/profile system, and a
small set of supporting types.
| Class | Method group | Returns |
|---|---|---|
Text |
parseJson/parseSnbt/parseLegacy/parseMiniMessage (+*Result) |
Component (or ParseResult) |
Text |
renderJson/renderSnbt/renderLegacy/renderMiniMessage/renderHtml (+*Result) |
string (or RenderResult) |
Text |
text/translate/keybind/score/selector/nbt/gradient/rainbow/transition |
the matching *Component |
Text |
color(string|int|NamedColor) |
Color |
Text |
build() |
TextBuilder seeded with empty text |
| Class | Role |
|---|---|
Component |
sealed interface implemented by every node |
TextComponent |
literal text (JSON {"text": "..."}) |
TranslateComponent |
locale key + with arguments (JSON {"translate": ...}) |
KeybindComponent |
keybind reference, e.g. key.jump |
ScoreComponent |
scoreboard score reference |
SelectorComponent |
target selector, e.g. @p, with optional separator |
NbtComponent |
NBT path read from a block/entity/storage |
GradientComponent |
MiniMessage-native colour gradient |
RainbowComponent |
MiniMessage-native HSL hue cycle |
TransitionComponent |
MiniMessage-native single sampled colour |
Style |
immutable container for colour, decorations, font, events, etc. |
Decorations |
tri-state record of bold/italic/underlined/strikethrough/obfuscated |
Color / NamedColor / HexColor |
colour interface and its two implementations |
NbtSource |
enum: Block, Entity, Storage |
Events\ClickEvent |
sealed: OpenUrl, RunCommand, SuggestCommand, CopyToClipboard, ChangePage, CustomClickEvent |
Events\HoverEvent |
sealed: ShowText, ShowItem, ShowEntity |
| Format | Reader | Renderer | Options class |
|---|---|---|---|
| JSON | JsonReader |
JsonRenderer |
JsonOptions |
| SNBT | SnbtReader |
SnbtRenderer |
SnbtOptions |
| Legacy | LegacyReader |
LegacyRenderer |
LegacyOptions |
| MiniMessage | MiniMessageReader |
MiniMessageRenderer |
MiniMessageOptions |
| HTML | — | HtmlRenderer |
HtmlOptions |
| Class | Role |
|---|---|
Profile\JsonDialect |
enum: Mc1_20, Mc1_20_5, Mc1_21_4, Mc1_21_5, Mc26_1 |
Profile\Profile |
per-dialect field naming and action allowlists |
Render\Diagnostic |
severity + dotted code + message + optional path/offset |
Render\Severity |
enum: Info, Warning, Error |
Read\ParseResult |
parsed component + diagnostics |
Render\RenderResult |
rendered text + diagnostics |
| Class / interface | Purpose |
|---|---|
MiniMessage\TagHandler |
Register custom MiniMessage tags via TagRegistry::register |
Html\Palette / Html\StaticPalette |
Map NamedColor to RGB; ship chat() and sign() palettes |
Translation\Translator |
Caller-supplied locale lookup |
- Minecraft Java versions: parsers accept every JSON/SNBT shape
emitted between 1.20 and 26.1 (Minecraft Java's current major).
Renderers target the same range via
JsonDialect; pick the dialect matching the server/client you're talking to. - Legacy codes: classic
§plus&, with BungeeCord§x§R§R§G§G§B§Bhex (opt-in default-on) and Essentials&#RRGGBBhex (opt-in) - MiniMessage: the vanilla tag vocabulary — colours and aliases,
decorations with
<!negation>form,click/hover/insertion/font, content tags (<lang>,<key>,<score>,<selector>,<nbt>), transformations (<gradient>,<rainbow>,<transition>), plus<reset/>and<newline/>. Custom tags pluggable viaTagHandler. - HTML: emits semantic markup with configurable styling mode,
scheme-allowlisted click handlers, and a
data-mc-*attribute convention for consumer-side JavaScript
Bedrock Edition's text format variants are not supported.
Bug reports, feature requests, and pull requests are welcome at
github.com/cartographgg/minecraft-text.
See CONTRIBUTING.md for development setup and the
required checks (tests, static analysis, code style, and mutation
testing). Each check has a Composer script: composer test,
composer static, composer style, and composer mutation.
Released under the MIT License. © Cartograph contributors.
Maintained as part of Cartograph, a Minecraft server directory and monitoring platform. The library is self-contained and has no Cartograph-specific behaviour; use it anywhere you need to work with Minecraft chat text in PHP.
