cartograph / minecraft-text
Multi-format Minecraft text library: parse JSON / SNBT / legacy / MiniMessage components into a shared AST and render to JSON / SNBT / legacy / MiniMessage / HTML.
Requires
- php: ^8.5
- cartograph/minecraft-nbt: ^1.0
Requires (Dev)
- brianium/paratest: ^7.22
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.32
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^13
README
Minecraft Text
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.
Features
- 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)
Requirements
- PHP 8.5 or newer
cartograph/minecraft-nbt^1.0 (resolved automatically by Composer)
No PHP extensions are required.
Installation
Install via Composer:
composer require cartograph/minecraft-text
Quick start
Parse 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":"!"}]}
Concepts
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.
Usage
Parse JSON, SNBT, legacy, or MiniMessage
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.
Render to JSON, SNBT, legacy, MiniMessage, or HTML
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_color
Build trees programmatically
Two 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();
Resolve translations
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.
Customise HTML rendering
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.
Class overview
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.
Entry point
| 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 |
Component AST (Cartograph\Text\Component)
| 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 |
Readers and renderers
| Format | Reader | Renderer | Options class |
|---|---|---|---|
| JSON | JsonReader |
JsonRenderer |
JsonOptions |
| SNBT | SnbtReader |
SnbtRenderer |
SnbtOptions |
| Legacy | LegacyReader |
LegacyRenderer |
LegacyOptions |
| MiniMessage | MiniMessageReader |
MiniMessageRenderer |
MiniMessageOptions |
| HTML | — | HtmlRenderer |
HtmlOptions |
Profiles and diagnostics
| 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 |
Extension points
| 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 |
Compatibility
- 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.
Contributing
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.
License
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.
