Programmatic Mutation
Build and modify Bliss compositions programmatically: add glyphs, rearrange parts, change options, all from JavaScript.
Why Mutate?
Sometimes a static DSL string isn't enough. You might need to:
- Build compositions step by step from user input
- Add or remove glyphs in a dynamic UI
- Apply conditional styling based on application state
- Transform existing compositions programmatically
The mutation API lets you do all of this on a live builder instance, with every change immediately reflected in the SVG output.
The Element Tree
Every composition has a three-level hierarchy:
Builder
└─ Glyph Groups (words)
└─ Glyphs (characters)
└─ Parts (components within a character)For example, B313/B1103//B431;B81 creates:
- Group 0: B313, B1103
- Group 1: one glyph with parts B431 and B81
Character-level indicators (;) are parts within a glyph, marked with isIndicator: true. A word-level indicator (;;) is different: it is stored on its group as a reversible wordIndicators overlay and resolved onto the head glyph only when the SVG is rendered, so it has no part node in the tree. See How ;; is stored and serialized for details.
Navigating the Tree
The builder provides methods to get live handles into this tree:
const builder = new BlissSVGBuilder('B313/B1103//B431');
builder.group(0); // first glyph group → B313/B1103
builder.group(1); // second glyph group → B431
builder.glyph(0); // first glyph across all groups → B313
builder.glyph(2); // third glyph → B431Handles can navigate deeper:
builder.group(0).glyph(1); // B1103 within first group
builder.glyph(0).part(0); // first part of B313Negative indices count from the end:
builder.group(-1); // last group
builder.glyph(-1); // last glyph
builder.glyph(-2); // second-to-last glyphOut-of-range indices return null:
builder.group(99); // nullFinding Elements by Key
Use getElementByKey() with keys from a snapshot:
const snap = builder.snapshot();
const key = snap.children[0].children[0].key;
const handle = builder.getElementByKey(key);Adding Content
Starting from an Empty Builder
The constructor accepts no arguments, creating an empty builder you can populate programmatically:
const builder = new BlissSVGBuilder();
builder.addGlyph('B313');
builder.addGlyph('B1103');
// equivalent to new BlissSVGBuilder('B313/B1103')Adding Glyph Groups
addGroup() appends a new glyph group with automatic spacing:
const builder = new BlissSVGBuilder('B313');
builder.addGroup('B431');
// now equivalent to 'B313//B431'Adding Glyphs
Add to the last group with the builder's addGlyph():
const builder = new BlissSVGBuilder('B313');
builder.addGlyph('B1103');
// now equivalent to 'B313/B1103'Or add to a specific group via its handle:
builder.group(0).addGlyph('B291');Insert at a specific position:
builder.group(0).insertGlyph(0, 'B431'); // prependAdding Parts
Add parts to compose characters inline:
const builder = new BlissSVGBuilder('H');
builder.glyph(0).addPart('C8');
// now equivalent to 'H;C8'The builder also has a shorthand that appends to the last glyph of the last group:
const builder = new BlissSVGBuilder('B313');
builder.addPart('B81');
// appends B81 to B313, equivalent to 'B313;B81'Insert at a specific position:
builder.glyph(0).insertPart(0, 'B81'); // prependModifying Content
Replacing Elements
Replace a glyph or part with new content:
const builder = new BlissSVGBuilder('B313/B1103');
builder.glyph(0).replace('B431');
// now equivalent to 'B431/B1103'Setting Options
Merge options onto any element:
builder.glyph(0).setOptions({ color: 'red' });
builder.group(0).setOptions({ strokeWidth: 0.6 });Options use camelCase keys, the same keys as programmatic options.
Removing Options
Remove specific options by key:
builder.glyph(0).removeOptions('color');Removing Content
remove() deletes an element from its parent:
const builder = new BlissSVGBuilder('B313/B1103//B431');
builder.glyph(1).remove();
// now equivalent to 'B313//B431'Removal cascades automatically:
- Removing the last glyph in a group removes the group (and its space)
- Removing the last part in a glyph removes the glyph
const builder = new BlissSVGBuilder('B313//B431');
builder.glyph(1).remove(); // B431 was the only glyph in group 1
// now equivalent to 'B313' (group and space removed)Non-Cascading Removal with detach()
Sometimes you want to remove an element without cascade cleanup. detach() does a plain splice: it disconnects the node from its parent and nothing more.
const builder = new BlissSVGBuilder('B313//B431');
builder.glyph(1).detach(); // removes B431 but leaves the empty group and spaceCompare with remove():
| Method | Behavior |
|---|---|
remove() | Cascades: empty parents and adjacent spaces are cleaned up |
detach() | Plain splice: just disconnects from parent, no cleanup |
Use detach() for fine-grained structural control. You can compose it with navigation instead of needing separate raw methods:
builder.group(0).glyph(1).detach(); // plain-splice a glyph from a group
builder.glyph(0).part(2).detach(); // plain-splice a part from a glyphLike remove(), detach() returns undefined and cannot be chained. It can produce empty containers that render as zero-width elements. This is intentional for fine-grained structural control.
Parent-Centric Remove and Replace
Instead of getting a handle and calling remove() on it, you can operate by index from the parent:
// Remove/replace glyphs within a group
builder.group(0).removeGlyph(-1); // remove last glyph
builder.group(0).replaceGlyph(0, 'B431'); // replace first glyph
// Remove/replace parts within a glyph
builder.glyph(0).removePart(-1); // remove last part
builder.glyph(0).replacePart(0, 'B81'); // replace first partThe parent-centric methods return the parent handle (not undefined), so they can be chained.
Builder Group Operations
The builder provides direct methods for removing and replacing groups:
builder.removeGroup(-1); // remove last group
builder.replaceGroup(0, 'B431', { color: 'red' }); // replace first group
builder.insertGroup(1, 'B291'); // insert at position 1Clear everything:
builder.clear();Raw Element Access
The managed API (group(), addGroup(), removeGroup()) auto-manages space groups between words. For direct control, including access to space groups, use the raw element API.
Navigating All Elements
element() accesses all groups by raw index, including spaces:
const builder = new BlissSVGBuilder('B313//B431');
// Raw layout: [word group] [space group] [word group]
builder.element(0); // first word group (B313)
builder.element(1); // space group (TSP)
builder.element(2); // second word group (B431)
builder.element(-1); // last group
builder.elementCount; // 3Both return the same ElementHandle. The only difference is indexing:
| Navigation | Indexing |
|---|---|
builder.group(i) | Skips space groups (word 0, word 1, ...) |
builder.element(i) | Raw index over all groups including spaces |
Space management is a property of the builder-level CRUD methods, not the handles:
| CRUD methods | Manages spaces? |
|---|---|
addGroup / insertGroup / removeGroup | Yes (auto-inserts/removes space groups) |
addElement / insertElement / removeElement | No (plain splice) |
Modifying Raw Elements
Insert, remove, and replace by raw index with no automatic space management:
// Insert a space between two adjacent word groups
builder.insertElement(1, 'SP');
// Append a raw group (no auto-space inserted)
builder.addElement('B291');
// Remove a space group (adjacent words are not merged)
builder.removeElement(1);
// Replace a space with a different type
builder.replaceElement(1, 'QSP');SP is the standard space code. It auto-resolves to TSP (standard word spacing) or QSP (reduced spacing before punctuation). You can also use TSP or QSP explicitly for precise control.
Inspecting Space Groups
Space groups are identified in snapshots with the isSpaceGroup flag:
const snap = builder.snapshot();
snap.children.forEach(child => {
if (child.isSpaceGroup) {
console.log('space group at index', child.index);
}
});Handles returned by element() are standard group handles. All existing methods work on them: glyph(), addGlyph(), removeGlyph(), setOptions(), remove(), detach().
Space Manipulation
Splitting Words
splitAt() divides a word group into two separate words with a space between:
const builder = new BlissSVGBuilder('B313/B1103/B431');
builder.group(0).splitAt(2);
// now equivalent to 'B313/B1103//B431'The handle stays on the first half (the original group). Access the second half via builder.group(1). The first half retains the original options object. The second half receives a shallow copy with the same values.
A word-level (;;) indicator overlay follows the head: it stays with whichever half keeps the head glyph, and the other half gets none.
Merging Words
mergeWithNext() absorbs the next word group into the current one, removing spaces in between:
const builder = new BlissSVGBuilder('B313//B431');
builder.group(0).mergeWithNext();
// now equivalent to 'B313/B431'The merged word keeps the first word's options. The absorbed word's options are discarded. If there is no next word group, mergeWithNext() is a no-op.
Likewise, the merged word keeps the first word's word-level (;;) indicator overlay. A ;; overlay on the absorbed word is dropped, and a DROPPED_WORD_INDICATOR entry is added to builder.warnings so the loss is visible.
Indicator Operations
Indicators mark a glyph's grammatical role (action, description, thing). The mutation API provides dedicated methods for managing indicators with the same semantic preservation behavior as the DSL's ;; syntax.
Applying Indicators
Replace all indicators on a glyph with applyIndicators():
const builder = new BlissSVGBuilder('B291');
builder.glyph(0).applyIndicators('B86');
// now equivalent to 'B291;B86'Multiple indicators use semicolon-separated codes:
builder.glyph(0).applyIndicators('B81;B86');Non-indicator codes in the argument are filtered out with a per-code warning, matching DSL behavior; a call whose codes are all invalid is refused and changes nothing on a glyph handle (see Validation and no-ops below for the one group-level exception).
An empty code (omitted, '', or whitespace-only) is the deliberate empty set: on a glyph handle it behaves exactly like clearIndicators(); on a group handle it stores the empty ;; overlay (see Word-Level Indicators).
Semantic Preservation
Semantic indicators (thing, abstract) are automatically preserved when you replace indicators, unless the new indicators already include one:
const builder = new BlissSVGBuilder('B291;B97'); // B97 = thing indicator
builder.glyph(0).applyIndicators('B81');
// B97 preserved: now 'B291;B81;B97'To strip the semantic indicator, pass { stripSemantic: true }:
builder.glyph(0).applyIndicators('B86', { stripSemantic: true });
// B97 removed: now 'B291;B86'Clearing Indicators
Remove all grammatical indicators with clearIndicators(). It is the pure undo: semantic indicators are always preserved (it takes no stripSemantic):
const builder = new BlissSVGBuilder('B291;B86;B97');
builder.glyph(0).clearIndicators();
// B86 removed, B97 preserved: now 'B291;B97'To clear everything including the semantic, use the empty apply with stripSemantic:
builder.glyph(0).applyIndicators('', { stripSemantic: true });
// now 'B291'Word-Level Indicators
Call applyIndicators() / clearIndicators() on a group handle to manage a word-level (;;) indicator. The group-level call stores a reversible overlay on the word rather than baking onto a glyph, so the base glyphs stay intact and a later clear restores them:
const builder = new BlissSVGBuilder('B291/B303');
builder.group(0).applyIndicators('B86');
// equivalent to the DSL 'B291/B303;;B86'builder.group(0).clearIndicators();
// removes the overlay, back to 'B291/B303'Clearing is the undo of applying: any character-level indicators the head glyph carried before the overlay hid them show again.
An empty apply is the third overlay state — the deliberately empty ;;, which hides the head's own character-level indicators without adding any:
builder.group(0).applyIndicators('');
// equivalent to the DSL 'B291/B303;;'On an apply, pass { stripSemantic: true } for the ;;! strip form (applyIndicators('', { stripSemantic: true }) is the bare ;;!), or { flatten: true } to bake the indicator onto the head glyph's parts instead of keeping the reversible overlay.
Validation and no-ops
Indicator calls never fail silently. applyIndicators() validates its codes at both levels: a recognized non-indicator warns NON_INDICATOR_AS_WORD_INDICATOR on a group handle and NON_INDICATOR_AS_CHARACTER_INDICATOR on a glyph handle (an unknown code warns UNKNOWN_CODE), and only real indicators apply. A call whose codes are all invalid is refused: the element keeps its indicators unchanged, and the per-code warnings are the only effect. One deliberate exception: on a group handle, { stripSemantic: true } is itself valid overlay content, so an all-invalid apply with it still stores the ;;! strip overlay — matching the DSL WORD;;!ZZ9, where the bad code drops and the ! stays. A call that cannot apply or remove anything for structural reasons (clearing a glyph that has no indicators, clearing a group that has no ;; overlay, targeting a space glyph) records a NOOP_INDICATOR_MUTATION warning in builder.warnings. See Warning Codes.
Inspecting Indicators
The isIndicator getter on part handles returns whether a part is an indicator:
const builder = new BlissSVGBuilder('B291;B86');
builder.glyph(0).part(0).isIndicator; // false (B291 is base)
builder.glyph(0).part(1).isIndicator; // true (B86 is indicator)indicatorLevel and indicatorKind classify an indicator part (both null for non-indicators):
builder.glyph(0).part(1).indicatorLevel; // 'character' (a ; indicator)
builder.glyph(0).part(1).indicatorKind; // 'grammatical' (vs 'semantic' for a thing/abstract root)A part handle never reports 'word': a ;; overlay has no raw part node, so word-level indicators are classified on the resolved snapshot() tree instead. See the API reference.
Chaining
Builder methods return the builder, so you can chain into builder properties:
const svg = new BlissSVGBuilder('B313')
.addGroup('B1103')
.addGroup('B431')
.svgCode;Handle methods return the handle, so you can chain further handle operations:
builder.group(0)
.setOptions({ color: 'blue' })
.addGlyph('B291');Handle chains stay on the handle. To access builder properties after handle mutations, use a separate statement:
builder.group(0).addGlyph('B291');
builder.svgCode; // back on the builderNote: remove() returns undefined and cannot be chained.
Combining and Splitting Builders
Merging Two Builders
Combine saved texts into one builder. The receiving builder's global options apply to everything:
const morning = new BlissSVGBuilder('[color=red]||B313/B1103');
const afternoon = new BlissSVGBuilder('[color=blue]||B431//B291');
morning.merge(afternoon);
// morning now contains all words, styled with color=red
// afternoon is unchangedSplitting a Builder
Split a builder at a word boundary. Both halves keep the same global options:
const text = new BlissSVGBuilder('[color=red]||B313//B431//B291');
const page2 = text.splitAt(2);
// text: B313, B431 (color=red)
// page2: B291 (color=red)Snapshots vs Live Handles
The builder offers two ways to inspect the tree:
snapshot() | group() / glyph() / part() | |
|---|---|---|
| Type | Frozen tree | Live ElementHandle |
| Mutates? | No | Yes |
| Survives rebuilds? | Snapshot is isolated | Handle stays live across mutations |
| Dimensions? | Via properties on each node | Via .x, .y, .width, .height, .bounds, .measure() |
| Use for | Reading, diffing, serialization | Modifying content, measuring elements |
Handles stay live across mutations to other parts of the tree. A handle throws only when its own node has been removed (similar to DOM references).
Snapshots are frozen and unaffected by later mutations:
const snap = builder.snapshot();
builder.glyph(0).remove();
snap.children; // still has the original structureOptions
All mutation methods that accept a code string also accept an options parameter. Pass flat options to apply them as overrides:
builder.addGroup('B431', { color: 'red' });
builder.glyph(0).replace('B313', { color: 'green' });
builder.glyph(0).setOptions({ color: 'red', strokeWidth: 0.6 });Defaults and Overrides
When you need separate defaults and overrides, use the structured format with the same precedence as the constructor:
builder.addGroup('[color=red]B431', {
defaults: { strokeWidth: 0.6 }, // applied if not set in DSL
overrides: { fill: 'blue' } // always applied
});Precedence: defaults → DSL string → overrides.
Flat options { color: 'red' } are equivalent to { overrides: { color: 'red' } }. The structured format is only needed when you want to set defaults that the DSL string can override.