@braccato/highlight

中文文档
0.3.2发布于 2026/10/18MITTypeScriptES Module构建来源已验证近 30 天下载 1709

Lightweight syntax highlighting for TTML, LRC, QRC and SRT lyrics

原始使用文档
文档目录8 节
  1. Install
  2. Usage
  3. Read-only panes
  4. Editors
  5. Tokens only
  6. Tokens
  7. Theming
  8. Detection

@braccato/highlight

Syntax highlighting for TTML, LRC, QRC and SRT lyrics, with zero runtime dependencies beyond @braccato/parsers/format. It ships a pure tokenizer, a DOM renderer for read-only panes, and a textarea overlay for editors. Stamps and markup recede, and the sung words stay bright.

Install

npm i @braccato/highlight

Usage

Read-only panes

import { highlightInto } from "@braccato/highlight";
import "@braccato/highlight/highlight.css";

highlightInto(pre, source, { pretty: true });

highlightInto adds the bh class to the element, which the token colours are scoped under, and returns the format it used. pretty: true breaks minified TTML into lines at head, body, div and p. It only inserts whitespace, but the rendered text no longer matches the source, so use it for display only and never in an editor. Line formats ignore it.

Editors

import { attachEditor } from "@braccato/highlight";
import "@braccato/highlight/highlight.css";

const editor = attachEditor(textarea);

attachEditor wraps the textarea in a div.bh-edit and lays a highlighted pre.bh-layer under it. The textarea becomes transparent and keeps the caret, the selection and all input. The layer copies the textarea's font, padding, border widths and box sizing on every input and on every resize, and follows its scroll position.

The layer holds one span.bh-line block per source line, and each block holds a single text node. The colours come from the CSS Custom Highlight API: one highlight per token type, registered in CSS.highlights as bh-<type> and shared by every editor on the page. Splitting a line into elements would make the browser round each piece's width separately, and a row that only just fits in the textarea would then wrap one word early in the layer and push every later line down a row. A single text node is laid out exactly as the textarea lays out its value. Where the API is missing, the layer shows plain text in the same position.

Every input re-tokenizes the whole document, so TTML state such as background vocals stays correct on later lines, but only the lines that changed are rewritten and recoloured.

Only the text within one editor height above and below the visible part is coloured. The rest has no highlight ranges. A scroll that stays inside that margin restyles once scrolling settles, and a scroll past it restyles before the next paint.

Keystroke cost in Chrome 152 on an Apple M4 Pro, for a keystroke typed mid-document in a 900 by 600 pixel editor. Script and layout is the median script plus forced style and layout time; paint is the paint phase of the frame that follows:

DocumentScript and layoutPaint
21.4k-character one-line TTML2.8 ms17 ms
98k-character one-line TTML7.1 ms26 ms
320-line TTML3.2 ms1.6 ms
2,000-line LRC3.6 ms1.3 ms

A keystroke repaints the whole text node it lands in, with every highlight on it, so one long line is the slow case: the visible text repaints in one piece per token.

Scrolling restyles once per editor height scrolled. On the 98k-character line that restyle costs about one and a half keystrokes, and on the multi-line documents less than one; scroll steps in between cost under 0.2 ms.

The package is 10.9 KB minified and 4.7 KB gzipped (esbuild --minify), plus @braccato/parsers/format.

It returns { wrap, layer, refresh, setFormat, destroy }:

  • Setting textarea.value from code does not fire input, so call editor.refresh() after every programmatic write.
  • editor.destroy() puts the textarea back where it was, removes the wrapper and drops the bh-input class. Calling it twice is safe.
  • Pass { format } to pin a format. Without it the format is detected on every render, so pasting a different format re-colours.
  • The editor copies the options when it attaches and owns its format from then on. Changing the options object later does nothing.
  • editor.setFormat(format) changes the format of the live editor and re-renders at once. The textarea is not re-attached, so its native undo history survives. editor.setFormat(undefined) goes back to detecting the format on every render. Passing the current fixed format (or undefined while detecting) does nothing. In detect mode, passing the format that was detected pins it. setFormat does not replace refresh() after a programmatic write to textarea.value: call refresh() for the new value whether or not the format changes.
  • Attaching a textarea that already has a live editor returns that editor's handle and ignores the new options. Use setFormat on the handle to change the format.

The overlay needs the textarea and the layer to share one box, so .bh-input forces margin: 0, box-sizing: border-box, width: 100% and resize: none on the textarea. The layer copies that box sizing, so a textarea's padding and borders stay inside the wrapper and both wrap lines at the same width. Put any margin or width the host wants on the wrapper (.bh-edit, or editor.wrap) instead.

Highlights can only change colours, so background vocals and comments are not italic in the editor, and --bh-bgText-style only applies to read-only panes. Background vocals are dimmer in the editor instead, through --bh-bgText-editor. Any font works: the layer never changes the font of any part of the text.

Tokens only

import { tokenize } from "@braccato/highlight";

const tokens = tokenize(source); // [{ type: "punct", text: "[" }, { type: "timestamp", text: "00:14.21" }, ...]

The tokens' concatenated text always equals the input, and no two adjacent tokens share a type. They are exactly what highlightInto draws, so a renderer of your own (a React component that renders on the server, for example) reproduces it by drawing each token as below, with no merging step:

tokens.map(({ type, text }, i) => (type === "text" ? text : <span key={i} className={`bh-${type}`}>{text}</span>));

Wrap the result in an element with the bh class so the token colours apply. highlightInto with pretty: true tokenizes prettyTtml(source) instead of the source.

Tokens

In read-only panes each token renders as one <span class="bh-<type>">, except text, which renders as a plain text node. In the editor each token is a range in the bh-<type> highlight. tokenize already joins adjacent text of the same type into one token.

TokenMeaning
textSung lyric text. It is also used for whitespace and newlines.
bgTextSung text inside a TTML ttm:role="x-bg" span.
timestampA line time: [00:14.21], a TTML begin, end or dur value, [14210,4350], or an SRT cue time.
wordTimeA word time: <00:18.90> or (14210,300).
agentA voice: v1: in LRC, or TTML ttm:agent, ttm:role or xml:id values.
metaMetadata: an LRC or QRC [ti:...] key, text inside the TTML <head>, or an SRT cue number.
tagA TTML element name.
attrA TTML attribute name.
valueAny other attribute value, or an LRC meta value.
punctBrackets, quotes, < > / =, and the SRT -->.
comment<!-- ... --> and <?xml ... ?>.

Theming

Every colour is a CSS custom property with a built-in fallback. The stylesheet defines no global custom properties, so set any of them on any ancestor of the pane:

.my-pane { --bh-timestamp: #86efac; }
PropertyDefault
--bh-textrgba(255, 255, 255, 0.95)
--bh-bgTextrgba(255, 255, 255, 0.82)
--bh-bgText-styleitalic
--bh-bgText-editorrgba(255, 255, 255, 0.65)
--bh-timestamprgba(165, 180, 252, 0.7)
--bh-wordTimergba(165, 180, 252, 0.52)
--bh-agentrgba(252, 211, 77, 0.45)
--bh-metargba(249, 168, 212, 0.38)
--bh-tagrgba(255, 255, 255, 0.26)
--bh-attrrgba(255, 255, 255, 0.22)
--bh-valuergba(134, 239, 172, 0.3)
--bh-punctrgba(255, 255, 255, 0.16)
--bh-commentrgba(255, 255, 255, 0.2)
--bh-caret#fff
--bh-selectionrgba(165, 180, 252, 0.28)
--bh-placeholderrgba(255, 255, 255, 0.35)

Detection

When no format is given, it is detected with detectFormat from @braccato/parsers/format. Those are the same rules detectParser uses, in the same priority: TTML, LRC, SRT, QRC, then plain. The subpath is dependency-free, so highlighting never pulls in the XML parser.

版本与发布记录

@braccato/highlight 共发布 9 个版本,首次发布于 2026/10/01;过去 12 个月发布了 9 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.3.2latest2026/10/1823 天前
0.3.12026/10/071 个月前
0.3.02026/10/031 个月前
0.2.22026/10/031 个月前
0.2.12026/10/011 个月前
0.2.02026/10/011 个月前

关于 @braccato/highlight 的常见问题

@braccato/highlight 是什么?

Lightweight syntax highlighting for TTML, LRC, QRC and SRT lyrics

如何安装 @braccato/highlight?

在项目目录运行 npm install @braccato/highlight,或使用 pnpm add / yarn add / bun add。

@braccato/highlight 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@braccato/highlight 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@braccato/highlight 可以商用吗?

@braccato/highlight 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@braccato/highlight 的下载量有多少?

根据 npm 官方下载统计,@braccato/highlight 在 2026/10/09 – 2026/11/07 这 30 天内被下载 1,709 次,日均约 57 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@braccato/highlight 还在维护吗?

最新版本 0.3.2 发布于 2026/10/18(23 天前);至今共发布 9 个版本,其中过去 12 个月发布了 9 个;首次发布于 2026/10/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。