TdRichText

A semantic parser for engineering prose. Pass a single plain string; the parser annotates HTTP endpoints, durations, storage sizes, percentiles, percentages, multipliers, complexity notation, comparison operators, ALL-CAPS acronyms, and prose numbers as typographically-polished inline spans against cb-* tokens.

Distinct from its rich-text siblings — see the comparison table below.

Preview
The GET /api/feed handler returns 95% of responses in 200ms with p99 at 450ms. Each payload is at most 36 KB. Throughput is 10× the previous build, with O(n log n) sort. Use ctx.auth.getUserIdentity() and check for null before the SDK call.
Customize
Root element
span
Motion
static
Enabled matchers

Installation

npx shadcn@latest add https://craftbits.dev/r/td-rich-text.json

Usage

import { TdRichText } from "@craft-bits/core";
 
<TdRichText>
  The GET /api/feed handler returns 95% of responses in 200ms with p99 at 450ms.
</TdRichText>

The barrel exports the component as TdRichText to keep its name distinct from the markdown-style RichText (@craft-bits/core/edu/rich-text) and the token-array RichTextNodes (@craft-bits/core/text/rich-text). Import via the subpath if you only want this primitive:

import { TdRichText } from "@craft-bits/core/text/td-rich-text";

How it differs

ComponentInput shapeRecognises
RichTextNodesRichTextToken[]nothing — declarative
RichTextProsesemantic JSXnothing — descendant CSS
RichElementsper-element componentsnothing — explicit composition
RichParagraphparagraph childrentypographic recipe
RichText (edu)stringmarkdown marks
TdRichTextstringengineering semantics — durations, sizes, percentiles, endpoints, complexity

The other parsers tokenise markup. This one tokenises meaning: it reads engineering prose the way a careful editor would and lifts the right atoms into polished inline spans.

Recognised atoms

AtomPattern (default)Renders as
codebacktick spansmono chip, bordered
endpointGET /api/...mono chip, data-cb-td-rich-atom="endpoint"
duration200ms, 1.5μs, 40nsmono chip, data-cb-td-rich-atom="duration"
size36 KB, 1.5 MBmono chip, data-cb-td-rich-atom="size"
percentilep95, p99, p99.9mono chip, data-cb-td-rich-atom="percentile"
percentage95%, 0.5%mono chip, data-cb-td-rich-atom="percentage"
multiplier10×, 5xmono chip, data-cb-td-rich-atom="multiplier"
complexityO(n), O(n log n)mono italic, accent
comparison>= <= !=typographic upgrade
arrow accent weighted
booleantrue, false, null, undefinedserif italic small-caps
prose-numberstandalone digitsmono tabular-nums
emphasisALL-CAPS, 3+ charspencil-emphasis squiggle
keywordquoted strings, 2+ charsserif italic with curly quotes
texteverything elseplain

Variants

The variant prop controls motion. static (the default) ships zero motion. reveal springs each atom in on mount with a small stagger. interactive adds a scale-on-hover to code chips.

<TdRichText variant="reveal"></TdRichText>

Swap the root element when the surrounding layout expects a block:

<TdRichText as="p"></TdRichText>

Extending the vocabulary

Matchers are exposed via the matchers prop so consumers can layer or replace them. Earlier rules win when ranges overlap.

import {
  TdRichText,
  TD_RICH_TEXT_DEFAULT_MATCHERS,
  type TdRichTextMatcher,
} from "@craft-bits/core";
 
const EXTRA: TdRichTextMatcher = {
  re: /\bRFC \d+\b/g,
  type: "emphasis",
};
 
const MATCHERS = [EXTRA, ...TD_RICH_TEXT_DEFAULT_MATCHERS];
 
<TdRichText matchers={MATCHERS}>RFC 9110 defines GET semantics.</TdRichText>

Pass an empty list to disable parsing entirely (useful for fixture tests).

Props

PropTypeDefaultDescription
childrenstringrequiredPlain-text source. The parser annotates it.
as'span' | 'div' | 'p''span'Root element.
variant'static' | 'reveal' | 'interactive''static'Motion mode.
matchersreadonly TdRichTextMatcher[]TD_RICH_TEXT_DEFAULT_MATCHERSCustom matcher list. Earlier rules win.
classNamestringMerged onto the root via cn().

Accessibility

  • Pure inline typography — no interactive controls of its own.
  • Decorative arrows render with aria-hidden="true" so screen readers skip them.
  • variant="reveal" and variant="interactive" use motion/react. Reduced-motion is the parent's responsibility — set variant="static" (the default) when prefers-reduced-motion: reduce.
  • ALL-CAPS emphasis runs through PencilEmphasis, which honours prefers-reduced-motion itself.
  • Body text targets WCAG AA contrast against --cb-bg in both themes.

Credits

  • Extracted from: terminal-dreams (src/components/ui/RichText.tsx). The source was a parser-first engine with 15 engineering-prose matchers (endpoints, durations, percentiles, sizes, multipliers, complexity, …) bound to bespoke ApiEndpoint / DurationBadge / SizeBadge / PercentileBadge / PercentageBadge / MultiplierBadge / ComplexityBadge / CustomArrow widgets, an in-module STATIC_CACHE, and three motion modes. The library version keeps the parser and the three motion modes, generalises the per-atom widgets into a single data-cb-td-rich-atom styled chip so consumers can re-skin via CSS, swaps framer-motion for motion/react, replaces the unbounded module-level cache with WeakMap-keyed caches that release with the matcher list, and exposes matchers so the vocabulary is extensible without forking the component.