book-and-quill
v1.1.0
Published
A multi-version JSON text component library for Minecraft. Parse, stringify, resolve, and wrap raw JSON text components, with full support for Minecraft 1.20.4 through 26.2.
Readme
book-and-quill
A multi-version JSON text component library for Minecraft. Parse, stringify, resolve, and wrap raw JSON text components, with full support for Minecraft 1.20.4 through 26.2.
Features
- Multi-version support — Automatically adapts syntax rules based on the target Minecraft version
- Flexible parsing — Accepts both strict JSON and lenient SNBT (Stringified NBT) input
- Full text styling — Bold, italic, underlined, strikethrough, obfuscated, custom fonts, shadow colors
- All content types — Text, translatable, score, selector, keybind, NBT, atlas sprites, player heads
- Interactive elements — Click events and hover events with modern and legacy format support
- Color handling — Named colors, hex (
#RRGGBB/#AARRGGBB), integer, and RGBA array formats - Unicode escapes —
\x,\u,\U, and\N{name}escape sequences - Component resolution — Flatten a component to styled plain-text segments, resolving
translate,keybind, andwitharguments through your own callbacks - Text wrapping — Break text into lines exactly the way Minecraft's
StringSplitterdoes, with your own glyph metrics
Installation
npm install book-and-quillUsage
Static API
import { TextComponent } from 'book-and-quill'
// Parse a text component string
const element = TextComponent.parse('{text:"Hello",color:red}')
// → { text: 'Hello', color: 'red' }
// Parse with a specific Minecraft version
const legacy = TextComponent.parse('{"text":"Hello"}', { minecraftVersion: '1.20.4' })
// Stringify a text component
const modern = TextComponent.stringify(
{ text: 'Hello', color: 'red' },
{ minecraftVersion: '1.21.5' }
)
// → '{text:Hello,color:red}'
const legacyStr = TextComponent.stringify(
{ text: 'Hello', color: 'red' },
{ minecraftVersion: '1.20.4' }
)
// → '{"text":"Hello","color":"red"}'Instance API
const component = TextComponent.fromString('{text:"Hello",bold:true}')
component.toString() // stringify with defaults
component.toString(true, '1.21.5') // minified, modern format
component.toJSON() // returns the raw TextElementParser & Stringifier Classes
import { TextComponentParser, TextComponentStringifier } from 'book-and-quill'
const parser = new TextComponentParser({ minecraftVersion: '1.21.5' })
const element = parser.parse('{text:"Hello"}')
const stringifier = new TextComponentStringifier({ minecraftVersion: '1.21.6', minify: false })
const output = stringifier.stringify(element)Parser Feature Flags
By default the parser enables all features appropriate for the given minecraftVersion. You can override this entirely by passing a custom enabledFeatures bitmask:
import { TextComponentParser } from 'book-and-quill'
const { FEATURES } = TextComponentParser
const parser = new TextComponentParser({
enabledFeatures:
FEATURES.LITERAL_KEYS | // unquoted object keys: {text:"hello"}
FEATURES.LITERAL_STRINGS | // unquoted string values: {text:hello}
FEATURES.SINGLE_QUOTES | // single-quoted keys/values: {text:'hello'}
FEATURES.TRAILING_COMMAS | // trailing commas: {text:"hello",}
FEATURES.OPTIONAL_COMMAS | // omit commas entirely: {text:"hello" color:red} ⚠️ non-standard
FEATURES.MODERN_EVENT_FORMAT | // click_event/hover_event (1.21.5+)
FEATURES.CLICK_EVENT_ACTION_SHOW_DIALOG | // show_dialog action (1.21.6+)
FEATURES.TEXT_OBJECT_TYPE_OBJECT | // sprite/player types (1.21.9+)
FEATURES.SHADOW_COLOR | // shadow_color field (1.21.4+)
FEATURES.SHADOW_COLOR_ACCEPTS_STRING | // shadow_color as named color ⚠️ non-standard
FEATURES.SPACE_ESCAPE_SEQUENCE | // \s → space (1.21.5+)
FEATURES.HEX_ESCAPE_SEQUENCE | // \x41 → A (1.21.5+)
FEATURES.EIGHT_DIGIT_UNICODE_ESCAPE_SEQUENCE | // \U0001F600 → 😀 (1.21.5+)
FEATURES.NAMED_UNICODE_ESCAPE_SEQUENCE | // \N{Snowman} → ☃ (1.21.5+)
FEATURES.IMPLICIT_TEXT_KEY | // {color:red} → {text:'',color:red} ⚠️ non-standard
FEATURES.TEXT_OBJECT_INFERRED_KEYS | // keyless values infer text/color ⚠️ non-standard
FEATURES.CLICK_EVENTS |
FEATURES.HOVER_EVENTS,
})Features marked ⚠️ non-standard are syntax sugar that Minecraft itself does not support. Components using them must be processed by this parser before being used in-game.
Colors
TextComponent.getColor('red') // Named Minecraft color
TextComponent.getColor('#00aced') // Hex color
TextComponent.getColor(-16732947) // Integer color
TextComponent.getColor([0, 1, 0, 0.5]) // RGBA array (0–1 range)
TextComponent.intToHex8(-16732947) // '#FF00ACED'
TextComponent.hexToInt('#FF00ACED') // -16732947Style Utilities
// Compare styles across two components
TextComponent.hasSameStyle({ color: 'red', bold: true }, { color: 'red', bold: true }) // true
// Resolve inherited style from a component, with an optional parent style
TextComponent.getComponentStyle([{ color: 'red' }, 'text'], { bold: true })
// → { bold: true, color: 'red' }Interactive Elements
import type { TextElement } from 'book-and-quill'
const element: TextElement = {
text: 'Click me',
color: 'gold',
click_event: { action: 'run_command', command: '/say hello' },
hover_event: { action: 'show_text', value: { text: 'Tooltip' } },
extra: ['!'],
}Resolving a Component to Text
resolveComponent flattens a component tree into styled plain-text segments — the shape you need for measuring, wrapping, or drawing text yourself. Anything that depends on a language file or a running game (translate, keybind, score, ...) is resolved through callbacks you pass in, so the function is fully synchronous:
import { resolveComponent } from 'book-and-quill'
const lang = { 'chat.type.text': '<%s> %s' } // however you load your language file
const segments = resolveComponent(
{ translate: 'chat.type.text', with: ['Steve', { text: 'hi', color: 'yellow' }] },
{ translate: key => lang[key] }
)
// → a '<Steve> ' segment (style {}) followed by a 'hi' segment (style { color: 'yellow' })
// each segment's `text` is a UnicodeString; call .toString() for a plain string%s,%1$s, and%%in a translation are filled fromwith; each argument keeps its own style.- A missing translation falls back to the component's
fallback, then to the key itself. key.jumpand friends resolve throughVANILLA_KEYBINDS, then yourtranslatecallback. Passkeybindto override a binding.score,selector,nbt,sprite, andplayerbecome{...}placeholders unless you pass aplaceholdercallback.- Adjacent segments that share a style are merged.
Wrapping Text
wrapText breaks segments into lines the way Minecraft's StringSplitter does: break at the last space, hard-break a word with no spaces, and always break on \n. It doesn't know about fonts — you pass a function that returns each glyph's width — so it wraps for the vanilla font, a resource-pack font, or anything else:
import { resolveComponent, wrapText } from 'book-and-quill'
const segments = resolveComponent('the quick brown fox jumps over the lazy dog')
// Advance of one code point, in pixels. This fakes a 6px monospace font;
// a real one would read the font's glyph metrics.
const widthOf = (codePoint: string) => (codePoint === ' ' ? 4 : 6)
const { lines, width } = wrapText(segments, 120, widthOf)
// lines: [{ segments: [{ text, style }], width }, ...]
// width: the widest line, in whole pixels (rounded up, like Font.width)Pass { resolveLegacyCodes: true } to also strip inline § formatting codes, matching Minecraft's StringDecomposer.
Version Feature Highlights
| Version | Features |
| ------- | ----------------------------------------------------------------------- |
| 1.20.4 | Legacy format — quoted keys/values, camelCase clickEvent/hoverEvent |
| 1.21.4 | shadow_color support |
| 1.21.5 | Modern click_event/hover_event format, SNBT escape sequences |
| 1.21.6 | show_dialog click event action |
| 1.21.9 | sprite (atlas) and player (head texture) content types |
API Reference
TextComponent (static)
| Method | Description |
| -------------------------------------------- | ---------------------------------------------------------- |
| parse(text, options?) | Parse a JSON/SNBT string into a TextElement |
| stringify(element, options?) | Stringify a TextElement to JSON/SNBT |
| fromString(str, options?) | Parse and wrap in a TextComponent instance |
| fromJSON(json) | Wrap an existing JSON object in a TextComponent instance |
| getComponentStyle(component, parentStyle?) | Resolve the computed style of a component |
| getColor(color) | Resolve any color format to a tinycolor instance |
| hasSameStyle(a, b) | Check if two components share the same style keys |
resolveComponent(component, options?)
Flattens a component into { text: UnicodeString, style }[], resolving dynamic content through options:
| Option | Type | Default |
| ------------- | ------------------------------------------ | ---------------------------------------------- |
| translate | (key: string) => string \| undefined | translation key is left as-is |
| keybind | (keybind: string) => string \| undefined | VANILLA_KEYBINDS bound key, then translate |
| placeholder | (element: TextObject) => string | {...} placeholder text |
VANILLA_KEYBINDS (a Record<string, string> of the default bindings) is exported alongside it.
wrapText(segments, maxLineWidth, widthOf, options?)
Breaks resolveComponent's segments into lines, matching Minecraft's StringSplitter. Returns { lines: { segments, width }[], width }, with per-line and overall pixel widths rounded up like Font.width. widthOf(codePoint, style) returns a glyph's advance in pixels — including the 1px inter-glyph gap and the extra bold pixel. options.resolveLegacyCodes (default false) strips inline § codes first.
TextElement Type
type TextElement = string | TextElement[] | TextObjectAll inputs are fully typed — TextObject, event types, color values, NBT sources, and version-specific fields all have complete TypeScript definitions with autocomplete support.
License
See LICENSE.
