OscillaScore CueDSL Parser and Runtime Integration

Technical Overview & Migration Guide


Purpose

OscillaScore’s CueDSL (Cue Domain-Specific Language) provides a unified textual syntax for describing time-based or event-based actions within SVG scores — e.g.:

cue:page(seq: page1:4, loop(page2:2,page3:3){x:2}, mode:scroll)

This system replaces a collection of independent regex parsers (cueAudio(...), cueRepeat_*, etc.) with a single robust grammar built using Chevrotain 11+.
The parser translates human-readable cue strings into structured AST objects that drive runtime behavior in cues.js.


Architecture Overview

Flow Diagram

┌────────────┐        ┌────────────┐        ┌──────────────┐        ┌─────────────┐
│ SVG cue ID │ ─────▶ │ parser.js  │ ─────▶ │ AST object   │ ─────▶ │ cues.js     │
│ e.g. cue:page(...) │             │ CST→AST│ e.g. {type:"cuePage",…}│ runtime exec│
└────────────┘        └────────────┘        └──────────────┘        └─────────────┘

Components

Component Description
parser.js Chevrotain-based grammar definition. Produces CST (Concrete Syntax Tree).
cstToAst() Simplifies the verbose CST into a usable AST.
handleCueFromAST(ast) Central dispatcher that routes to a cue-specific runtime (e.g. handlePageCueFromAST).
handlePageCueFromAST() Executes sequenced, looped, and random page changes defined in the AST.

Why CST → AST

Chevrotain generates a CST that mirrors the grammar hierarchy.
This is verbose but precise — great for debugging.

The AST is a simplified domain model used by the runtime.
Example:

CST fragment:

{
  "name": "loopItem",
  "children": {
    "Loop": [ { "image": "loop" } ],
    "pageItem": [...],
    "xVal": [ { "image": "3" } ]
  }
}

AST result:

{ "type": "loop", "pages": [{ "name": "page1", "dur": 2 }], "repeat": 3 }

This separation makes the system:


Example: cue:page DSL

Input

cue:page(seq: page1:4, loop(page2:2,page3:3){x:2}, mode:scroll)

Parsed AST

{
  type: "cuePage",
  args: [
    { type: "page", name: "page1", dur: 4 },
    { type: "loop", pages: [
        { name: "page2", dur: 2 },
        { name: "page3", dur: 3 }
      ], repeat: 2
    },
    { type: "control", name: "mode", value: "scroll" }
  ]
}

Runtime behavior

This is managed by:

export async function handlePageCueFromAST(ast) {
  for (const item of ast.args) {
    switch (item.type) {
      case "page":   await handlePageCue(...); break;
      case "loop":   await handleLoop(...); break;
      case "rand":   await handleRand(...); break;
      case "choose": await handleChoose(...); break;
    }
  }
}

Adding a New Cue Type

Adding a new cue (e.g. cue:audio, cue:traverse, cue:repeat) involves three layers:

🧱 Step 1: Grammar in parser.js

Add a new sub-rule:

$.RULE("audioCue", () => {
  $.CONSUME(Identifier, { LABEL: "audioKeyword" }); // "audio"
  $.CONSUME(LParen);
  $.CONSUME(Identifier, { LABEL: "filename" });
  $.CONSUME(RParen);

  // optional parameters in {...}
  $.OPTION(() => {
    $.CONSUME(LBrace);
    $.CONSUME(Identifier, { LABEL: "param" });
    $.CONSUME(Colon);
    $.CONSUME(NumberLiteral, { LABEL: "value" });
    $.CONSUME(RBrace);
  });
});

And register it in the root:

$.RULE("cueRoot", () => {
  $.CONSUME(Cue);
  $.CONSUME(Colon);
  $.OR([
    { ALT: () => $.SUBRULE($.pageCue) },
    { ALT: () => $.SUBRULE($.audioCue) },
    { ALT: () => $.SUBRULE($.traverseCue) },
  ]);
});

Step 2: Extend cstToAst()

Transform that CST into a clean AST node:

if (c.audioCue) {
  const file = c.audioCue[0].children.filename[0].image;
  const params = extractParams(c.audioCue[0]);
  ast = { type: "cueAudio", file, params };
}

Step 3: Add a runtime handler in cues.js

export async function handleAudioCueFromAST(ast) {
  const { file, params } = ast;
  console.log(`[CueDSL] 🎧 Playing ${file} with`, params);
  // play audio via native Web Audio or WaveSurfer
}

And register it in the unified dispatcher:

export async function handleCueFromAST(ast) {
  switch (ast.type) {
    case "cuePage":    return handlePageCueFromAST(ast);
    case "cueAudio":   return handleAudioCueFromAST(ast);
    case "cueTraverse":return handleTraverseCueFromAST(ast);
    case "cueRepeat":  return handleRepeatCueFromAST(ast);
    default: console.warn("Unhandled cue type:", ast.type);
  }
}

Debugging Tips

Use the built-in CST logger at any time:

console.log("Parsed CST structure ↓↓↓");
printCST(cst);

You’ll see hierarchical token traces like:

cuePage
  Cue: "cue"
  Colon: ":"
  pageKeyword: "page"
  playlist
    loopItem
      Loop: "loop"
      ...

This shows exactly what the parser recognized and helps pinpoint rule mismatches.


Migration Strategy for Legacy Cues

Legacy cue Current format Target format Migration strategy
cueAudio(file)_loop(3)_amp(1) flat regex cue:audio(file:"file.wav"){loop:3,amp:1} new grammar + runtime
cueRepeat_s_a_e_b_x_3 namespace cue:repeat(start:a,end:b,x:3) map fields to structured args
cueTraverse(p(p1,p2,p3),o(obj),s(1),d(2)) parameterized cue:traverse(points:[p1,p2,p3],object:obj,speed:1,dir:2) direct parameter parsing
cueText(...), cueChoice(...), etc. mixed unified under parser progressive migration

Each migration simply adds a grammar rule + small runtime adapter —
no more regex chains or duplicated logic.


Best Practices for New Grammar Design


Refactoring Outlook

Once all cues migrate:


Summary

You now have:

Next steps:

  1. Migrate remaining cue families (audio, repeat, traverse, text).
  2. Centralize all runtime handlers in cues.js.
  3. Write one unified handleCueFromAST(ast) dispatcher.

TL;DR

Each cue string → CST → AST → runtime.
Add one grammar rule, one AST extractor, one runtime function.
The rest — parsing, debugging, and sequencing — is already done.

Tip: use ← → or ↑ ↓ to navigate the docs