Skip to content

Entities: deep‐dive

Alexander Cerutti edited this page Jun 16, 2026 · 1 revision

CueNodes: a tale of a shared structure introduced entities as a property. In this page we are going to deep dive into them to understand them better.

What is an entity?

An entity is a piece of what we may call meta-content. Something that belongs to a cue but is not the cue's text itself. If a cue says "Hello, <b>world</b>", the bold tag is an entity. If a TTML <span> carries an inline style, that style is an entity. If a TTML <animate> element transitions a color over time, the animation description is an entity.

entities

Entities form an ordered list on the CueNode. Their order reflects the order in which they appear in the source document. The renderer walks that list to reconstruct the styled, potentially animated visual output.

They are a tool for adapters authors to express rendering variations of any kind. Their idea is to be the middle layer that contains CSS-related matter.

The shape of an entity

All entities implement EntityProtocol, which is a single-property interface:

interface EntityProtocol {
	type: Type;
}

where Type can be of these kinds:

  • LINE_STYLE (0)
  • LOCAL_STYLE (1)
  • TAG (2)
  • ANIMATION (3)

The discriminant type field lets the renderer (or any consumer) use exhaustive narrowing without runtime string comparisons. @sub37/adapter-utils exports a type alias that covers all four concrete types:

export type AllEntities = LineStyleEntity | LocalStyleEntity | TagEntity | AnimationEntity;

Each entity type also comes with a factory function and a type guard, both exported from @sub37/adapter-utils.

You can import the whole namespace or pull in individual names:

// whole namespace — factories and guards are accessed as Entities.createTagEntity(), etc.
import * as Entities from "@sub37/adapter-utils/Entities";

// individually
import {
	TagType,
	createTagEntity,
	isTagEntity,
	createLocalStyleEntity,
	isLocalStyleEntity,
	createLineStyleEntity,
	isLineStyleEntity,
	createAnimationEntity,
	isAnimationEntity,
} from "@sub37/adapter-utils/Entities";

// types only
import type { TagEntity, LocalStyleEntity, LineStyleEntity, AnimationEntity, AllEntities } from "@sub37/adapter-utils/Entities";

TagEntity

import { TagType, createTagEntity, isTagEntity } from "@sub37/adapter-utils/Entities";
import type { TagEntity } from "@sub37/adapter-utils/Entities";

Represents an inline structural element: a bold span, an italic span, a ruby container, and so on. The recognized tag types are:

  • SPAN
  • RUBY
  • RT
  • BOLD
  • ITALIC
  • UNDERLINE

If a format has a tag concept that does not map to any of these, SPAN is the fallback. It carries the tag into the render tree without implying any specific styling.

Along with tagType, a TagEntity carries the element's attributes as a Map<string, string | undefined> and its CSS class list. Adapters that convert formats with rich inline markup (WebVTT's <v>, <b>, <ruby>, TTML's <span>) will produce TagEntity instances most often.

const entity = createTagEntity(
	TagType.BOLD,
	new Map([["data-speaker", "Alice"]])
);

LocalStyleEntity

import { createLocalStyleEntity, isLocalStyleEntity } from "@sub37/adapter-utils/Entities";
import type { LocalStyleEntity } from "@sub37/adapter-utils/Entities";

Carries CSS declarations that apply to the inline element the entity is attached to. The factory accepts either a raw CSS declaration string or a pre-parsed Record<string, string>:

const entity = createLocalStyleEntity("color: red; font-size: 1em");
// or
const entity = createLocalStyleEntity({ color: "red", "font-size": "1em" });

The resulting styles property is always the parsed Record<string, string> form.


LineStyleEntity

import { createLineStyleEntity, isLineStyleEntity } from "@sub37/adapter-utils/Entities";
import type { LineStyleEntity } from "@sub37/adapter-utils/Entities";

Structurally identical to LocalStyleEntity. Same styles shape, same factory signature. The difference is semantic: in fact, a LineStyleEntity styles the containing line (the <p>-level box), while a LocalStyleEntity styles the inline span.

Adapter must know where to apply the style, on the text run or on the line container.


AnimationEntity

import { createAnimationEntity, isAnimationEntity } from "@sub37/adapter-utils/Entities";
import type { AnimationEntity } from "@sub37/adapter-utils/Entities";

The most complex entity. It represents a CSS animation derived from TTML's <animate> or <set> elements, but made to be as generic as possible.

An AnimationEntity is either discrete or continuous:

  • Discrete: maps to a CSS steps() timing function. Used for <set> and for calcMode="discrete".
  • Continuous: maps to a CSS cubic-bezier() timing function.

createAnimationEntity accepts a single object. styles maps each CSS property to an array of values, one per keyframe, parallel to keyTimes. A color transition from red to blue over 500ms looks like:

const entity = createAnimationEntity({
	id: "my-animation",
	kind: "continuous",
	duration: 500,       // milliseconds
	delay: 0,
	fill: "forwards",
	keyTimes: [0, 1],    // normalized values in [0, 1]
	splines: [[0.25, 0.1, 0.25, 1]], // one cubic-bezier per interval
	styles: {
		color: ["red", "blue"],
	},
});

For a discrete animation (e.g. from a <set> element), use kind: "discrete" and omit splines:

const entity = createAnimationEntity({
	id: "my-set",
	kind: "discrete",
	duration: 200,
	keyTimes: [0, 1],
	styles: {
		visibility: ["hidden", "visible"],
	},
});

Because AnimationEntity carries a complete, self-contained description of the animation, the renderer can translate it directly into a CSS @keyframes rule without needing to re-read the source document.


When to use each type

Entity type Use when
TagEntity The format has structural inline elements (bold, italic, voice tags, ruby)
LocalStyleEntity The format has inline or referenced styles that scope to a text span
LineStyleEntity The format has styles that scope to a paragraph or line container
AnimationEntity The format has timed visual transitions on elements or regions

Not all adapters may or will use all the entities.