Custom Codes
Bliss SVG Builder comes with B-codes for individual Bliss characters. define() lets you register your own codes that work just like built-in ones: use them in DSL strings, combine them in words, attach indicators, and apply options.
Custom codes cover a range of use cases:
- Readable aliases: use
LOVEinstead ofB431 - Word-level codes: give a single code to a multi-character Bliss word
- Custom characters: define characters that don't exist in the built-in set
- Custom shapes: create new geometric building blocks from existing shapes or SVG paths
All definitions are global. Once defined, any BlissSVGBuilder instance can use them.
Naming Things
The simplest use case: give a readable name to a B-code.
import { BlissSVGBuilder } from 'bliss-svg-builder';
BlissSVGBuilder.define({
'LOVE': { codeString: 'B431' }
});
new BlissSVGBuilder('LOVE').svgCode;This creates an alias. LOVE expands to B431 during parsing, as if you had typed B431 directly. Once defined, use it anywhere you'd use a B-code.
Defining Words
Many Bliss words are built from multiple characters. You can define a word as a single code:
BlissSVGBuilder.define({
'B2661': { codeString: 'B1103;B81' }
});
new BlissSVGBuilder('B2661').svgCode;B2661Words with side-by-side characters use / in the codeString, just like in the DSL:
BlissSVGBuilder.define({
'MYWORD': { codeString: 'B335/B412' }
});MYWORDAliases can also reference other aliases. A definition stores its codeString as you wrote it (an /SP/ segment still normalizes to //), and all references are resolved during parsing, so redefining a referenced code (with overwrite: true) flows through to every definition that uses it:
BlissSVGBuilder.define({
'UNDERSTAND': { codeString: 'B1103;B81' },
'TOUNDERSTAND': { codeString: 'UNDERSTAND' }
});Portable Output
Custom codes are global within the runtime environment, but they only exist in memory. If you serialize output with toString() or toJSON() and send it to a different environment, the receiving Bliss SVG Builder won't know what LOVE or B2661 from the examples above means.
That's why both methods decompose custom codes by default, expanding them back to built-in codes:
new BlissSVGBuilder('B2661').toString();
// 'B1103;B81' (anyone can read this)
new BlissSVGBuilder('B2661').toJSON();
// groups[0].glyphs[0].parts →
// [{ codeName: 'B1103' }, { codeName: 'B81', isIndicator: true, width: 2 }]
// (built-in codes, no custom names)For output like this the receiver doesn't need your definitions: it is built-in codes all the way down. Not every name can decompose, though: a getPath primitive has no built-in form, and a ;-part alias to a composed glyph keeps its name even in default output. A custom name that stays in serialized output needs its define() wherever the output is read; see Serializing Custom Indicators and Shapes.
If you're serializing for your own use (where the custom codes are defined), { preserve: true } keeps custom names in the output. preserve keeps a custom name when the name stands for a single glyph, indicator, or shape. That covers every code defined with a type (such a definition is itself a glyph, indicator, or shape, whatever its anatomy) and every bare alias that renames one existing code. A codeString that combines several codes is shorthand for the composition it spells out; shorthand always serializes expanded, with or without preserve:
new BlissSVGBuilder('LOVE').toString({ preserve: true });
// 'LOVE' (a bare alias renaming one code keeps its name)
new BlissSVGBuilder('B2661').toString({ preserve: true });
// 'B1103;B81' (multi-code shorthand always expands)As a rule of thumb: the default is for output that leaves your app, preserve is for output that stays where your definitions live. Custom indicators and shapes have a few extra wrinkles worth knowing; see Serializing Custom Indicators and Shapes below.
Characters (Glyphs)
So far we've created aliases: transparent macros that disappear during parsing. But sometimes you want a custom code that has its own identity in the element tree, so you can find it, inspect it, and mutate it by name.
Use type: 'glyph' for this:
BlissSVGBuilder.define({
'SMILEY': {
type: 'glyph',
codeString: 'C8:0,8;DOT:2,11;DOT:6,11;HC4S:4,14'
}
});A glyph appears as its own node in the element tree. Inside the tree, the name is preserved. On export, it decomposes to portable output, just like aliases:
const builder = new BlissSVGBuilder('B313/SMILEY');
// Inside the tree: identity preserved
builder.glyph(1).codeName; // 'SMILEY'
// On export: decomposed to built-in codes
builder.toString();
// 'B313/C8:0,8;DOT:2,11;DOT:6,11;HC4S:4,14'
// A glyph is a unit whatever its anatomy, so preserve keeps its
// name even for this multi-code composition (a typeless multi-code
// shorthand would expand instead)
builder.toString({ preserve: true });
// 'B313/SMILEY'When to Use Which
Use an alias (no type) when you just want a shortcut or a word-level mapping. The name is a convenience that disappears at parse time.
Use a glyph (type: 'glyph') when you're defining a proper Bliss character that should have its own identity: something you want to traverse, mutate, or treat as a first-class element.
Character Properties
Most glyphs only need type and codeString. Some characters use anchorOffsetX and anchorOffsetY to adjust their anchor point in compositions. Indicators need additional properties like isIndicator and width. See the API Documentation for all available properties, and Metadata propagation for how each field shows up in parser output, rendering, serialization, and handles.
Shapes
Use type: 'shape' to create reusable geometric primitives that can be used as building blocks in characters. Shapes can only reference other shapes.
Composite Shapes
Build a shape from existing shape codes:
BlissSVGBuilder.define({
'CROSS': {
type: 'shape',
codeString: 'HL8:0,4;VL8:4,0'
}
});
new BlissSVGBuilder('CROSS:0,8').svgCode;Primitive Shapes
For full control, provide a path-generating function:
BlissSVGBuilder.define({
'DIAMOND': {
type: 'shape',
getPath: (x, y) => {
const cx = x + 4, cy = y + 4;
return `M${cx},${y} L${x + 8},${cy} L${cx},${y + 8} L${x},${cy} Z`;
},
width: 8,
height: 8
}
});
new BlissSVGBuilder('DIAMOND:0,8').svgCode;Mapping External ID Systems
If your application uses an external ID system (like BCI-AV-IDs or Blissary IDs), you can register those IDs as aliases. The bliss-blissary-bci-id-map repository provides a public JSON mapping:
import { BlissSVGBuilder } from 'bliss-svg-builder';
import mapping from './blissary_to_bci_mapping.json';
const definitions = {};
for (const { bciAvId, blissSvgBuilderCode } of mapping) {
definitions[String(bciAvId)] = { codeString: blissSvgBuilderCode };
}
BlissSVGBuilder.define(definitions);
new BlissSVGBuilder('14164').svgCode;
new BlissSVGBuilder('14164/14905').svgCode;Since these are aliases, toString() produces portable output automatically:
new BlissSVGBuilder('14164//14905').toString();
// 'B313//B392' (built-in codes, no external IDs)The same approach works for any external system:
BlissSVGBuilder.define({
'W17973': { codeString: 'B1103;B81' },
'W14895': { codeString: 'B431' },
});Definition Rules
define() validates every entry. A failed entry is reported in the returned errors array (the other entries still register), so check it when defining dynamically:
const result = BlissSVGBuilder.define({ 'BAD': { type: 'glyph', codeString: 'B313/B1103' } });
result.errors;
// ['"BAD": define("BAD"): a glyph definition cannot be a multi-character word …']The rules keep definitions portable and unambiguous:
- Names must be visible and unreserved. Control and format characters (like a zero-width space) are rejected by naming the code point. So are the reserved names: built-in codes,
Xfollowed by letters (the external-character namespace), and the syntax markersRK,AK,SP. Unused B-codes may be defined, but future versions may claim new built-in codes, so custom names carry no collision guarantee. - No word-level indicators in definitions. A
codeStringcannot contain;;. A word indicator is a live, reversible overlay, so it belongs at the use site:MYWORD;;B81. - A glyph or shape is a single character. Its
codeStringcannot contain/. Define a multi-character word as a bare code (omittype); a bare code may even span whole words (B291//C8). - A glyph cannot bake in an indicator. Indicators attach to characters at the use site (
SMILEY;B81) or to words (;;). To create a new indicator of its own, flag the glyph withisIndicator: true; it then behaves as one atomic indicator unit. The rule holds in every definition order: a laterdefine()that would turn an already-referenced code into an indicator is rejected too. isIndicatoris glyph-only. The flag marks atype: 'glyph'definition as a compound indicator; setting it on a bare alias, shape, or external glyph is rejected. Indicator-ness still rides through an unflagged bare alias whose target is itself an indicator ({ codeString: 'B81' }).- Word codeStrings carry no internal coordinates. In a
/-spanning codeString, apply positions at the use site (MYWORD:2,0) instead. Kerning markers (RK:-2,AK:1) are spacing, not coordinates, and are allowed. Single-character codeStrings keep their coordinate freedom (see below). - Spaces spell out as
//or explicit space codes. The top-level shorthandSPnormalizes to//when stored. defaultOptionskeys must be valid option names, and cannot include canvas-wide global-only options likemarginorgrid(they configure the whole SVG and would be inert on a definition).
patchDefinition() enforces the same rules and validates before applying, so a rejected patch changes nothing.
Indicators and Options on Custom Codes
Custom codes take indicators and options at the use site just like built-ins:
SMILEY;B81attaches the action indicator to the custom character;B313/SMILEY;;B81puts a word-level indicator on the word. Indicator positioning is computed from the glyph's actual rendered ink, including glyphs whose definition displaces its parts.- An alias to an indicator works as that indicator. After
define({ '6436': { codeString: 'B6436' } }), writingMYWORD;;6436applies the indicator exactly likeMYWORD;;B6436, on every surface (;;,;-parts,applyIndicators). Only single-code aliases resolve this way; to make an alias to a multi-code composition act as an indicator, define it as atype: 'glyph'flaggedisIndicator: trueinstead. [color=blue]>SMILEYstyles the whole glyph as one part. On serialization the option is re-emitted before each decomposed part so the styling survives portably; see Part Options on Custom Glyphs.
Serializing Custom Indicators and Shapes
Serialization carries your composition, never your definitions. For a custom indicator or shape, that split matters, because part of what you defined is metadata:
BlissSVGBuilder.define({
'MYIND': { type: 'glyph', isIndicator: true, codeString: 'C2' }
});
const builder = new BlissSVGBuilder('B291;MYIND');
builder.toString(); // 'B291;C2' — portable, but plain ink
builder.toString({ preserve: true }); // 'B291;MYIND' — your name, local useThe default string B291;C2 renders anywhere, but the receiver sees an ordinary circle part: isIndicator is metadata on your definition, and the decomposed string carries none of it. Re-parsing B291;C2 places C2 like any part, not like an indicator.
What travels through the default string output (toString()):
| From your definition | Travels? |
|---|---|
The ink (the codeString composition) | Yes, decomposed to built-in codes |
| Baked and use-site coordinates | Yes, as explicit :x,y suffixes |
defaultOptions | Yes, materialized as explicit options ([color=red]>C2) |
isIndicator and width | No. The receiver sees plain parts |
getPath geometry (primitives) | No. A primitive keeps its bare name and needs its definition to render |
That table is about the toString() string. toJSON() carries more: it serializes the full part tree, so it does include isIndicator and width, and a builder rebuilt from that object applies them, preserving indicator placement across an object round trip in the same environment (your definitions still registered). To move a custom indicator to an environment that does not have your definitions, register the same define() calls there; that is the reliable path (a custom name that stays in the serialized output needs its definition wherever the output is read).
Per definition shape, that works out to:
| Definition | Default output | preserve output |
|---|---|---|
Alias of one built-in indicator ({ codeString: 'B6436' }) | the target code, still a real indicator | your name |
Own indicator over plain ink (type: 'glyph', isIndicator: true, codeString: 'C2') | the plain shape, indicator behavior lost | your name |
Compound indicator (type: 'glyph', isIndicator: true, codeString: 'B97;B99:3,0') | the decomposed parts, no longer one atomic unit | your name |
New primitive (getPath) | your bare name, unrenderable elsewhere | your bare name |
Two remedies when the default output is not enough:
- Define at both ends. Run the same
define()calls in the receiving environment and sendpreserveoutput (or;;strings). Everything then means the same thing on both sides. - Compose with explicit coordinates. If you only need the ink to arrive exactly, skip the flag and write the anatomy with explicit positions (
B291;C2:3,0): explicitly positioned parts render at their coordinates in any environment, no definition needed.
Word-level indicators are the asymmetry to remember: B291;;MYIND keeps your code in default output too, because ;; is a live overlay resolved at render, not a baked part. A ;; string that references a custom code therefore always needs the definition at the receiving end. Without it, the receiver warns UNKNOWN_CODE, renders the word without the overlay, and drops the unknown code from its own re-serialized output. (A ;-part with an unknown name is kept in serialized output but fails its whole character at render, or shows a placeholder with the error-placeholder option; see UNKNOWN_CODE.)
Coordinates in Definitions
A coordinate baked into a definition is a real position offset and survives serialization. A use-site coordinate adds to it:
BlissSVGBuilder.define({
'INNER': { type: 'glyph', codeString: 'B291:2,3' }
});
const builder = new BlissSVGBuilder('INNER:1,2');
builder.toString();
// 'B291:3,5' — baked (2,3) + use-site (1,2); re-parsing renders identicallyThe same holds for a multi-part definition whose parts share a common offset: the offset shifts the rendered glyph and the decomposed output re-renders in exactly the same place.
Managing Definitions
Custom definitions can be overwritten, altered, removed, and inspected; built-in definitions cannot be replaced by any route (overwrite, patch, or remove), so the shared vocabulary always renders the same everywhere. See the API Documentation for define() options, Query API for getDefinition(), listDefinitions(), and removeDefinition().
Quick examples:
// Overwrite an existing definition
BlissSVGBuilder.define(
{ 'LOVE': { codeString: 'B313' } },
{ overwrite: true }
);
// Check if a code exists
BlissSVGBuilder.isDefined('LOVE'); // true
// Remove a custom definition
BlissSVGBuilder.removeDefinition('LOVE');