Search by

cartograph / minecraft-text

ollieread

Multi-format Minecraft text library: parse JSON / SNBT / legacy / MiniMessage components into a shared AST and render to JSON / SNBT / legacy / MiniMessage / HTML.

Package info

github.com/cartographgg/minecraft-text

pkg:composer/cartograph/minecraft-text

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.2.0 2026-05-19 09:16 UTC

This package is auto-updated.

Last update: 2026-08-29 09:47:51 UTC


README

Cartograph Logo

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.

Packagist Version Total Downloads PHP Version License

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_color support, and show_item payload 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 Diagnostic against 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 OpenUrl are validated against a configurable allowlist (default http/https/mailto), and obfuscated text is exposed via a data-mc-text attribute 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.

Interpret legacy codes inside text fields

JSON, SNBT, and MiniMessage parsers leave §/& codes inside a text field as raw characters — that matches vanilla Minecraft's own behaviour but doesn't help with the very common ecosystem case of a JSON MOTD or SNBT block carrying legacy-coded text:

{"text": "§6§lMythical§e§lMC\n§r§a§l Original Minigames 1.12.2+"}

Text::interpretLegacyInText() walks a tree and expands every legacy-coded TextComponent in place via LegacyReader. The wrapping node's own style and trailing children are preserved; the legacy-parsed styled runs are spliced in as the first children of a rewrapped TextComponent carrying the original style:

use Cartograph\Text\Text;

$tree = Text::interpretLegacyInText(Text::parseJson($motd));
$html = Text::renderHtml($tree);

The transform is opt-in (the parsers themselves never run it automatically) and lossy in one direction: once codes have been expanded into styled runs, the original raw text is gone, so a tree transformed this way will no longer re-serialise byte-identically back to its source wire format. Pass a LegacyOptions to enable & interpretation, Essentials &#RRGGBB hex, or BungeeCord §x§R§R§G§G§B§B hex (the last is on by default):

use Cartograph\Text\Legacy\LegacyOptions;
use Cartograph\Text\Legacy\Marker;
use Cartograph\Text\Text;

$tree = Text::interpretLegacyInText(
    Text::parseJson($motd),
    new LegacyOptions(parseMarkers: Marker::SECTION | Marker::AMPERSAND),
);

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']
    omitUnstyledSpans: true,                       // default true; set false for an always-emit-span DOM
);

echo Text::renderHtml($component, $opts);

By default, any component that would render as a <span> carrying no class, inline style, click/hover data, insertion, or obfuscation payload is emitted as bare body text — so plain text renders as hi, not <span>hi</span>, and the empty-text root container that comes out of LegacyReader and LegacyInterpreter no longer prepends a stray <span></span>. An <a href> envelope from an OpenUrl click is kept and wraps the body directly. Pass omitUnstyledSpans: false if your consumer needs a stable one-span-per-leaf DOM for CSS hooks or position-indexed JS.

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 interpretLegacyInText Component (legacy codes in text fields expanded into styled runs)
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§B hex (opt-in default-on) and Essentials &#RRGGBB hex (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 via TagHandler.
  • 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.