Markup Syntax
ETA markup uses two tag forms: angle-bracket span tags that wrap text ranges, and square-bracket message tags that attach whole-message behavior and are stripped before display.
Span tags
Section titled “Span tags”Span tags use angle brackets and follow one of three forms:
<tagname>content</tagname> opening + closing<tagname attr=value>...</tagname><tagname/> self-closing (no content)The parser walks a style stack. When it encounters an opening tag it pushes a new style onto the stack; every text fragment between the open and close inherits that style. When it encounters a closing tag it pops the stack.
Unmatched closers. If a </tagname> appears with nothing on the style stack, the pop call on an empty Stack is still reached. The closing tag is consumed and the stack remains empty. No error is logged and the surrounding text is not discarded. The behavior is the same regardless of whether the tag name matches whatever was pushed earlier, since the pop is unconditional once the "/" closing prefix is detected.
Case. Tag names are lowercased by the parser (matcher.group(2).toLowerCase()), so <Bold>, <BOLD>, and <bold> are all equivalent.
Allowed characters. The tag-name position accepts [a-zA-Z0-9_] for the first character and the same set for subsequent characters ([a-zA-Z0-9_][a-zA-Z0-9_]*). Hyphens are not permitted, so a name like eta-rainbow does not match the span-tag regex at all. See The eta- prefix below for what that naming convention actually means.
A minimal example:
<bold>Hello</bold>, <italic>world</italic>!Self-closing form (used for inline items and entities):
<item value=minecraft:diamond/>Message tags
Section titled “Message tags”Message tags use square brackets:
[tagname attr=value]They are extracted from the string before span parsing and do not appear in the rendered text. They carry whole-message properties (display duration, positioning, etc.) or message-level effects that apply to the entire popup rather than a character range.
[dur 60][pos center] <rainbow>Quest complete!</rainbow>Message tags may not appear inside a span tag’s <...> delimiters. If one does, the parser detects the overlap (overlapsAngleRegion), logs a warning, and treats the bracket tag as literal text.
The full set of recognized message tag names is determined by MessageEffectRegistry and MessageAttributeRegistry. An unrecognized bracket tag name is silently ignored: it is neither extracted nor flagged.
Attributes
Section titled “Attributes”Attributes appear inside the tag after the tag name, separated by whitespace. Each attribute takes one of these forms:
| Form | Example |
|---|---|
key=value | speed=2.5 |
key:value | speed:2.5 |
key="value" | text="hello world" |
key='value' | text='hello world' |
bare name (no = or :) | bold |
The attribute regex is:
([a-zA-Z][a-zA-Z0-9]*)(?:[=:](?:([\"'])([^\"']*)\\2|([^\s>/]+)))?Both = and : are accepted as the key-value separator. Quoted values (single or double) allow spaces and > inside the value. Unquoted values run to the next whitespace, >, or /.
Positional first attribute. When the first token in the attribute string has no = or : separator, its text is stored under the key "value" instead of as a boolean flag. Subsequent bare tokens are stored as key -> "true". This is how shorthand forms like <color #ff4400> work: #ff4400 becomes attrs.get("value").
Boolean flags. A bare attribute name that is not the first token is stored as "true". There is no "false" shorthand; omitting the attribute is the only way to leave it unset.
Numeric coercion. Attribute values are stored as strings. Tags that need a number call Integer.parseInt or Float.parseFloat at the point of use. A parse failure logs at DEBUG level and falls back to the tag’s default value.
Key case. Attribute keys are lowercased when stored (key.toLowerCase()), so Speed=2 and speed=2 are equivalent.
Nesting and inheritance
Section titled “Nesting and inheritance”Tags nest and each layer inherits from the one below it. The inheritStyles method copies every non-null property from the parent span to the child, so a color set by an outer tag carries through unless the inner tag overrides it.
<bold><color #ff4400>Warning:</color> this is still bold.</bold>In that string:
- “Warning:” is bold and red.
- ” this is still bold.” is bold only; the
</color>close popped the color tag but the<bold>tag is still on the stack.
Inherited properties: color, bold, italic, underline, strikethrough, obfuscated, font, effects, obfuscate mode, background color, background gradient, item id, click action, and hover action.
Escaping
Section titled “Escaping”ETA markup has no escape syntax in v3.0.0-beta.1. The parser has no mechanism to output a literal <, >, [, or ] by escaping it inside a markup string. If the string <bold> appears in your input, it will always be parsed as a tag. Avoid raw angle brackets or square brackets inside markup strings where you need them rendered as literal characters; use an alternative representation (such as a translation key that resolves to the desired text) instead.
Malformed input
Section titled “Malformed input”Unclosed tags. If an opening tag has no matching closer, the text that follows it is still rendered with that tag’s styles applied. The style stack is not validated at end-of-input.
Unknown tag names. A tag name that does not match any built-in case in applyTagToSpan falls through to the default branch, which does a PresetRegistry.get(tagName) lookup. If the tag is not a registered preset either, no style is applied and no warning is logged. The text is rendered unstyled relative to that tag.
Unknown attribute names. Unknown attributes are stored in the map but never read by the tag handler. They are silently ignored.
Attribute type mismatches. If an attribute value cannot be parsed as the expected type (e.g. size=abc on <item>), the failure is logged at DEBUG level and the tag uses its default value. The overall parse does not fail and the text is still rendered.
Unknown message tag names. If a [bracket] tag name is not found in MessageEffectRegistry or MessageAttributeRegistry, it is silently skipped. It is not extracted from the string, so it appears as literal text in the rendered output.
Since v3.0.0-beta.1, unknown params warn but do not cause parse failure. If a message effect or attribute constructor throws IllegalArgumentException, the error is logged at WARN level and that effect or attribute is skipped; the rest of the message is unaffected.
The eta- prefix
Section titled “The eta- prefix”The tag-name regex ([a-zA-Z0-9_][a-zA-Z0-9_]*) does not allow hyphens, so <eta-rainbow> does not match as a span tag. Instead, the eta- convention is a Patchouli compatibility strategy described in the README: inside Patchouli book entries, prefer <eta-rainbow> over <rainbow>. If a Patchouli book defines a macro for an unprefixed name (e.g. "<b>": "$(l)"), the macro takes precedence and ETA never sees that tag. A prefixed name like eta-rainbow is unlikely to collide with any macro, so the ETA tag survives the Patchouli pipeline.
Note that because the hyphenated name does not match the span-tag regex, it passes through unparsed by ETA’s own parser as well. The recommended approach is to use prefixed names only in Patchouli contexts where collision is a real concern. Outside Patchouli, use unprefixed names.
The <dur:N> special case
Section titled “The <dur:N> special case”extractDuration is a separate static method, not part of the main span parse. It handles the shorthand <dur:N> pattern used by some callers to embed a message duration directly in the markup string.
<dur:100> <rainbow>Quest complete!</rainbow>The method uses its own regex <dur:(\d+(?:\.\d+)?)> to find and remove the first match, returning {float duration, strippedMarkup}. If no match is found, duration is -1 and the string is returned unchanged. This tag is not processed by TAG_PATTERN at all; it exists only for callers that use extractDuration before calling parse.
A worked example
Section titled “A worked example”Start with plain text and add markup one step at a time.
Step 1: plain
Nice sword!No markup. Renders in the client’s default color.
Step 2: add bold
<bold>Nice sword!</bold>The entire string is bold. The parser pushes a bold style, emits “Nice sword!” with that style, then pops.
Step 3: color the noun
<bold>Nice <color #6af>sword</color>!</bold>“Nice ” and ”!” are bold only. “sword” is bold and light blue. After </color> pops the color layer, the bold layer is still active, so the final ! remains bold.
Step 4: add a glow to the noun
<bold>Nice <neon><color #6af>sword</color></neon>!</bold>“sword” is now bold, light blue, and neon-glowing. The <neon> tag pushes an effect that renders a soft glow pass behind the characters. <color> is nested inside it, so both properties apply to “sword”.
Step 5: add shake to the whole string
<shake><bold>Nice <neon><color #6af>sword</color></neon>!</bold></shake>Every character oscillates horizontally. The shake effect is inherited through the style stack by every span the parser emits.