Skip to main content
xriptxr

BBCode

Info:Content
20

Renders BBCode as themed HTML over a named, extensible tag vocabulary — as a document or an inline label.

forum phpbb vbulletin rich text markup render tags vocabulary

Live demo

live · @xtyle/astro

Bbcode

A document

phpBB's core roster, themed from the register: quotes ride the border and surface ramps, [color] reaches theme tokens by name.

Derivation

An algorithm maps a few anchors and knobs into a full, internally consistent
token set. See the derivation model for the long version.
  • the anchors you pin
  • the knobs you turn
  • everything else, derived
A theme is a print. The algorithm is the press.

Quotes, spoilers, and code

[spoiler] renders an Accordion and [code] a Code, rather than either growing a second opinion about something the library already ships. Both own their own fragment, so a mod reshapes them through a surface it knows.

Quotes, spoilers, and code
The Analytical Engine weaves algebraic patterns, just as the
Jacquard loom weaves flowers and leaves.
The ending
She was the engine all along.
const theme = derive({ bg: "#0f1115", accent: "#5b8cff" });

The rest of the roster

The declared extensions, where the major dialects agree: strike, marks, sub/sup, fonts, backgrounds, sizes, rules and tables.

bold · italic · underline · struck ·
marked · x2 · H2O · mono ·
accent · background · bigger



tagrenders
[code]a real Code component
[spoiler]an Accordion

An inline label

inline flows with the text around it and inherits its type, so it drops into a chip or a tab title. Note the last one: a block tag in a label never erupts a heading into the row.

label:

Fix tooltip placement

label:

Refactor AnchorTracker

label:

Drop keep the swatch fix

label:

Review PR #12

label:

[h1]a heading in a label[/h1]

Two vocabularies, one registry

The app registered its own [choice] tag and listed it in the story vocabulary but not the bio's. Same registry, same source syntax — the bio simply has no such tag, so it renders as the text it was. Refusal by construction. The handler returns an <xtyle-button> rather than a bare one, so a custom tag inherits real components instead of re-deriving its own hover state.

vocabulary="demo-story"

The path forks at the standing stones.

Go north
Follow the river

vocabulary="demo-bio"

Writes fiction about engines. Three novels.
[choice]this tag is not in the bio vocabulary[/choice]

Both languages at once

The Markdown component's process-bbcode composes the two. Neither renderer reads the other's output: BBCode constructs are lifted out before markdown parses and dropped back in after, so a tag inside a fence stays literal.

Both languages, one body

It weaves algebraic patterns — markdown emphasis, inside a BBCode quote.

Markdown owns the prose: bold, code, and links. BBCode owns the tags, like colour.

A spoiler, mid-document
Block tags stand on their own rather than nesting inside a paragraph.
[b]inside a fence, BBCode stays literal[/b]

Untrusted input

No sanitizer, and no allowHtml to get wrong — BBCode has no raw-HTML passthrough at all. The markup below is escaped to text, the javascript: URL loses its link, and the colour carrying a second CSS declaration is dropped rather than escaped.

This came from a forum post and is rendered as-is:

<script>alert("this never runs")</script>
<img src=x onerror=alert("nor this")>

a link that looks fine
and one that is fine.

a colour with a second declaration

With a source view

editable adds a toggle the fragment fill draws, so a mod can reword it, make it an icon, or move it. input fires as the source is typed.

Edit me

Switch to source with the toggle, change something, and switch back.
  • the textarea is the source of truth while editing
  • input fires with the BBCode as you type
Edit

BBCode renders forum-style markup into HTML that themes entirely from the token register: quotes and rules ride the border and surface ramps, sizes ride the type scale, and [code] renders a real <xtyle-code> so a code block inside a post and an <xtyle-code> beside it agree in any theme. The roster is phpBB's core set[b], [i], [u], [url], [img], [email], [quote], [code], [list]/[*], [color], [size] — plus declared extensions where the major dialects agree: [s], [sub]/[sup], [mark], [font], [bg], [spoiler], [noparse], [hr], [br], [h1][h6], [table]/[tr]/[td]/[th], and the four alignments.

inline switches to a label render for a chip or a tab title. It ships no sanitizer, and unlike Markdown there is no allowHtml either — BBCode has no raw-HTML passthrough to lift, so everything that is not a registered tag is escaped to text and every tag emits markup the renderer wrote itself, from a bounded set. The two places an author's value still reaches an attribute are handled by name: URLs go through the same scheme allowlist Markdown uses, and style values ([color], [size], [font], [bg]) are matched against narrow patterns rather than escaped, because red;background:url(…) survives escaping intact and is still a second declaration. vocabulary is the interesting attribute. One registry backs every instance and a vocabulary is a named subset of it, so a story body can admit [choice] while an author bio beside it cannot — refusal by construction rather than by filtering afterward. registerBbcodeTags adds tags, defineBbcodeVocabulary composes them into a named set, and an instance picks one by name; a tag outside the set renders as its literal text. The same seam is reachable from Markdown through processBbcode, which composes both languages in one body.

When to use

How this component composes with the rest of the set.

Reach for BBCode wherever the authors are untrusted and the markup should be bounded: forum posts, comments, user bios, in-game text. The format has no HTML passthrough at all, so there is no equivalent of allowHtml to get wrong.
vocabulary is the feature to design around, not an afterthought. Declare one vocabulary per surface — a story body, an author bio, a comment — and an instance can only ever render what that surface admits. It is cheaper and far more legible than filtering rendered output, and it fails closed by construction.
Import registerBbcodeTags / defineBbcodeVocabulary from @xtyle/core/elements — not from @xtyle/core/markup. The registry is module state, and the elements entry is the one that shares an instance with the components reading it. Where the same declaration has to run at build time too (an Astro frontmatter beside its client script), import the leaf @xtyle/core/elements/bbcode-registry.js, which pulls in the registry without evaluating any custom element and so works in Node as well as the browser. Registering after an element has already painted is fine either way: live components repaint when the roster changes, so script order is not a race you have to win.
Adding a tag is registerBbcodeTags([{ name, render }]). The handler returns markup, and whatever it returns still passes the fragment format declared in component-host.json — so a third-party tag cannot emit <script> however it is written. That floor is what makes an open tag registry safe rather than reckless. It also cuts the other way: the format's vocabulary admits xtyle's own components, so a handler should return an <xtyle-button> or an <xtyle-alert> rather than a bare element, and inherit the real hover, focus ring, and disabled treatment instead of re-deriving them.
Registering a tag does not expose it. It is reachable only where a vocabulary lists it, so installing a pack of tags is not the same as turning them on everywhere.
Use the Markdown component's processBbcode when a body should carry both languages. The two renderers never read each other's output: BBCode constructs are lifted out before markdown parses and dropped back in afterward, and prose inside a construct still gets markdown's inline render.
Most of the roster renders xtyle's own components rather than markup with a private stylesheet: [code] is a Code, [spoiler] an Accordion, [url] a Link, [img] an Image, [hr] a Separator, [h1][h6] a Heading, and [table] a Table around the author's own <table>. Each of those owns its own fragment, so none of it is furniture this component invented, and a mod reshapes it through a surface it already knows. [list] is the deliberate exception: the List component is the reference skin over the collection substrate, with a roving tab stop and a selection model, and a run of bullets in a paragraph is none of those.
An image or link that renders blank is usually a scheme rather than a bug: xtyle allows http, https, mailto, tel and data, and nothing else until an app says so. allowUriSchemes() from @xtyle/core/elements widens the renderer and the fragment format together.
Pair editable with the input event to keep your own state in sync; the event's detail.source carries the BBCode as it's typed.

Props

6 props, straight from the manifest.

PropTypeDefaultBindingsDescription
source string
html svelte astro
The BBCode to render. Optional for html/svelte, where the element's text content is used instead.
inline boolean false
html svelte astro
Render as a label rather than a document. It flows with the surrounding text and inherits its type, so it fits a tab title, a chip, or a table cell.
vocabulary string
html svelte astro
Which named tag vocabulary this instance may reach. Absent, the whole registry is in play. A tag outside the set is inert and renders as its literal text, so one surface can accept a tag another refuses without running a second renderer or filtering afterward. A name nobody declared falls back to the whole registry rather than rendering nothing — a vocabulary that refuses everything looks exactly like a bug, so a typo degrades to permissive-and-visible instead of silently blanking a body.
markdown boolean false
html svelte astro
Also run markdown's inline renderer over the prose between tags, so a body can carry `**bold**` and `[color=accent]` at once. For the composed *block* render, use the Markdown component's `processBbcode` instead — this is the label-shaped half of the same seam.
editable boolean false
html svelte astro
Offer a source view the reader can switch to, via a toggle the fragment fill draws.
editing boolean false
html svelte astro
Whether the source view is showing. Only meaningful alongside `editable`; setting it alone would strand the reader in a box with no way out, so it is ignored.

Appearance

States

editing

.xtyle-bbcode__toggle[aria-pressed='true']

The source view is showing: the body is swapped for the textarea and the toggle reads as pressed.

revealed

xtyle-accordion [data-open]

An open [spoiler], its body shown beneath the summary.

Anatomy

The named parts that make up the component, with their selectors.

body

.xtyle-bbcode__body

Where the rendered BBCode lands. Unlike the Markdown body, every rule here is keyed to the renderer's own class names rather than to element types — which is what makes a single tag reskinnable without touching the rest. Carries data-vocabulary naming the vocabulary the body rendered under, so the choice is visible where the markup landed.

--fg-0 --font-sans --text-body --leading-normal

quote

.xtyle-bbcode__quote

A [quote], marked by a leading accent edge on a raised surface, with its attribution above it.

--fg-1 --fg-2 --bg-1 --accent --space-4 --radius-sm

spoiler

xtyle-accordion

A [spoiler], rendered as a one-section Accordion — so it reveals with no JavaScript, and a mod reshapes it through the Accordion's own fill rather than through a shape this tag invented.

list

.xtyle-bbcode__list

A [list], ordered or not. Items are split on [*] rather than closed individually, as BBCode writes them.

--space-1 --space-3 --space-6

table

xtyle-table

A [table] with a filled header row and ruled cells.

link

xtyle-link

A [url] or [email], on the accent, warming to the body color on hover.

editor

.xtyle-bbcode__editor

The source textarea shown while editing, in the mono face on the field surface.

toggle

.xtyle-bbcode__toggle

The edit/view switch. Chrome the component invents, so it is a real node in the fragment fill: a mod can reword it, make it an icon, or move it, and the element keeps working.

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 20/20 consumed tokens produced default register: 310 tokens

Live coverage check against the xtyle-default register (derive(xtyleDefault, { anchors })coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent --bg-1 --fg-0 --fg-1 --fg-2 --font-sans --leading-normal --radius-sm --space-1 --space-2 --space-3 --space-4 --space-6 --text-body --text-sm --text-xs --warn-bg --warn-text --weight-bold --weight-semibold

Slots

default
html svelte

The BBCode as text content (html/svelte). For Astro, pass it via the source prop instead.

Accessibility

Renders semantic structure — real <blockquote> with <cite>, <ul>/<ol>, <table> with <th>, <h1><h6> — so assistive tech announces the content's shape rather than a wall of text.
[spoiler] renders an Accordion, which is <details>-backed — focusable, operable from the keyboard, and announced as a disclosure, without this component implementing any of it.
inline emits no headings or landmarks, which is what makes it safe to drop into a tab title or a chip.
The edit/view toggle is a real <button> carrying aria-pressed, so its state is announced; the source textarea has an accessible name.
A [url] whose scheme was refused renders as plain text rather than a link that does nothing, so it is skipped by link navigation.
[color], [bg] and [size] are author-controlled, so a body can be authored into poor contrast. Where authors are untrusted and contrast matters, leave those three out of the vocabulary — which is exactly the kind of decision the vocabulary layer exists to make once, structurally.

Code

A document

The block render: emphasis, links, lists and color, themed from the register.

<xtyle-bbcode source="[b]Patch notes[/b]

The [i]tooltip[/i] now tracks its trigger. See [url=/changelog]the changelog[/url].

[list]
[*]fixed placement under scroll
[*][color=accent]Escape[/color] dismisses a hover-raised hint
[/list]"></xtyle-bbcode>

An inline label

The label render, flowing with the text around it and inheriting its type.

<!-- a label, not a document: emphasis renders, blocks stay out of the way -->
<xtyle-bbcode inline source="Fix [b]tooltip[/b] in [color=accent]AnchorTracker[/color]"></xtyle-bbcode>

Quotes, spoilers, and code

[quote] attributes, [spoiler] is a native disclosure, and [code] hands off to the Code component.

<xtyle-bbcode source="[quote=ada]The Analytical Engine weaves algebraic patterns.[/quote]

[spoiler=Ending]She was the engine all along.[/spoiler]

[code=ts]const theme = derive({ bg: '#0f1115' });[/code]"></xtyle-bbcode>

Two vocabularies, one registry

The same custom [choice] tag is reachable in the story body and inert in the bio, because the bio's vocabulary doesn't list it.

<!-- the story body admits the app's own [choice]; the bio beside it cannot -->
<xtyle-bbcode vocabulary="story" source="You reach the fork. [choice]Go north[/choice]"></xtyle-bbcode>
<xtyle-bbcode vocabulary="bio" source="Writes fiction. [choice]nope[/choice]"></xtyle-bbcode>

<script type="module">
	import { registerBbcodeTags, defineBbcodeVocabulary } from "@xtyle/core/elements";

	registerBbcodeTags([
		{ name: "choice", block: true, render: (c) => `<xtyle-button variant="outline" block>${c.content}</xtyle-button>` },
	]);
	defineBbcodeVocabulary("story", ["b", "i", "url", "quote", "choice"]);
	defineBbcodeVocabulary("bio", ["b", "i", "url"]);
</script>

Both languages at once

The Markdown component's processBbcode composes the two: the quote comes from BBCode, the emphasis inside it from markdown.

<!-- both languages in one body: the quote is BBCode, the emphasis is markdown -->
<xtyle-markdown process-bbcode source="## Release notes

[quote=ada]It **weaves** algebraic patterns.[/quote]

Written in \`markdown\` with [color=accent]BBCode[/color] alongside."></xtyle-markdown>

With a source view

editable adds the toggle the fill draws, emitting input as the source is typed.

<xtyle-bbcode editable source="[b]Draft[/b]

Switch to the source and edit it."></xtyle-bbcode>