Skip to content

Extending the Player

This guide covers the extension points available in @reast/engine for developers who want to customize or extend the player's behaviour.

Plugin Architecture

The player supports a lightweight plugin system. Plugins can hook into rendering, choice handling, and lifecycle events.

Defining a Plugin

ts
import { definePlugin } from '@reast/engine';

const analyticsPlugin = definePlugin({
  name: 'analytics',

  onStoryLoaded(ctx) {
    console.log('Story loaded:', ctx.src);
  },

  beforeRenderNode(ctx) {
    // Return false to suppress rendering of this node
    if (ctx.node.type === 'command') return false;
  },

  afterRenderNode(ctx) {
    // Track rendered nodes
    trackEvent('node-rendered', { type: ctx.node.type });
  },

  onChoiceSelected(ctx) {
    trackEvent('choice', { index: ctx.index });
  },

  onStoryComplete(ctx) {
    trackEvent('story-complete', { readingTimeMs: ctx.readingTimeMs });
  },

  onDisconnect(ctx) {
    // Cleanup when player is removed from DOM
    cleanup();
  },
});

Registering Plugins

ts
import { registerPlugin, unregisterPlugin, clearPlugins } from '@reast/engine';

registerPlugin(analyticsPlugin);

// Later, to remove:
unregisterPlugin('analytics');

// Or remove all:
clearPlugins();

Plugin Hooks

HookContextCan Veto?Description
onStoryLoadedPluginContextNoCalled after story is loaded and parsed
beforeRenderNodeRenderHookContextYes (return false)Before a node is rendered to DOM
afterRenderNodeRenderHookContextNoAfter a node is rendered
onChoiceSelectedChoiceHookContextNoWhen the reader selects a choice
onStoryCompleteCompleteHookContextNoWhen the story reaches its end
onDisconnectPluginContextNoWhen the player is disconnected from DOM

Hook Context Types

ts
interface PluginContext {
  readonly host: HTMLElement;
  readonly src?: string;
}

interface RenderHookContext extends PluginContext {
  readonly node: ReaNode;
  readonly element?: HTMLElement | null;
}

interface ChoiceHookContext extends PluginContext {
  readonly index: number;
}

interface CompleteHookContext extends PluginContext {
  readonly readingTimeMs?: number;
}

CSS Custom Properties

All visual aspects of the player are themeable via CSS custom properties. See the full reference in Theming.

Quick example:

css
reast-player {
  --rp-font-body: 'Merriweather', serif;
  --rp-color-bg: #1a1a2e;
  --rp-color-text: #e0e0e0;
  --rp-color-accent: #e94560;
}

Custom Built-in Functions

The runtime supports registering custom functions that REA stories can call:

ts
import { StoryEngine } from '@reast/engine';

const engine = new StoryEngine(doc);

// Stories can use: {call myFunc("hello")}
engine.registerFunction('myFunc', (args) => {
  return args[0].toUpperCase();
});

Built-in Function Categories

CategoryFunctions
Mathrandom, round, floor, ceil, abs, min, max
Stringupper, lower, length, contains, replace
Arraycount, pick, shuffle
Datenow, today, formatDate, parseDate, dateDiff, dayOfWeek, dateAdd

State Management

The StateManager handles reading progress persistence:

ts
import { StateManager } from '@reast/engine';

const sm = new StateManager();

// Serialize current state (e.g. for localStorage)
const state = sm.serialize(engine.getAllVariables());
localStorage.setItem('story-progress', JSON.stringify(state));

// Restore later
const saved = JSON.parse(localStorage.getItem('story-progress')!);
sm.restore(saved);

State includes:

  • Choice selections
  • Variable values
  • Visited choice groups
  • Schema version for forward-compatible migration

Accessibility Utilities

The player includes accessibility helpers you can use when building custom integrations:

ts
import {
  createLiveRegion,
  announce,
  focusFirstContent,
  prefersReducedMotion,
  onReducedMotionChange,
} from '@reast/engine';

// Create a screen-reader announcement region
const region = createLiveRegion(document);
container.appendChild(region);

// Announce content changes
announce(region, 'New chapter loaded');

// Focus the first content element after navigation
focusFirstContent(container);

// Respect motion preferences
if (prefersReducedMotion()) {
  // Skip animations
}

Event Bus

The runtime emits events you can subscribe to:

ts
import { EventBus } from '@reast/engine';

const bus = new EventBus();
bus.on('choice-selected', (data) => {
  /* ... */
});
bus.on('variable-changed', (data) => {
  /* ... */
});
bus.on('story-complete', (data) => {
  /* ... */
});

Project Structure

text
src/
├── parser/          # REA source → AST
│   ├── lexer.ts         # Tokenizer (line-level)
│   ├── block-parser.ts  # Token stream → node tree
│   ├── inline-parser.ts # Inline formatting
│   └── analyser.ts      # Top-level parse orchestrator
├── runtime/         # AST → evaluated output
│   ├── interpreter.ts       # StoryEngine (conditionals, loops, etc.)
│   ├── expression-evaluator.ts
│   ├── state-manager.ts     # Save/load progress
│   ├── event-bus.ts
│   └── builtins/            # Built-in function implementations
├── player/          # Web Component UI
│   ├── reast-player.ts     # <reast-player> Custom Element
│   ├── renderer.ts         # AST → DOM rendering
│   ├── styles.ts           # Shadow DOM CSS
│   ├── plugins.ts          # Plugin architecture
│   ├── accessibility.ts    # A11y utilities
│   └── progress-bar.ts     # Reading progress
└── types.ts         # Shared TypeScript interfaces