Skip to content

cartographgg/minecraft-text

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.

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§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.

About

Multi-format Minecraft text library for PHP: parse and render JSON, SNBT, legacy §-codes, MiniMessage, and HTML through a shared component AST.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors