bliss-svg-builder
Version:
Compose, render, and manipulate Blissymbolics SVG using a compact DSL and a programmatic mutation API.
601 lines (474 loc) • 21.7 kB
TypeScript
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// --- Options ---
/** Known option keys accepted by BlissSVGBuilder (kebab-case). */
export interface BlissOptions {
// Stroke and spacing
'stroke-width'?: number;
'dot-extra-width'?: number;
'char-space'?: number;
'word-space'?: number;
'external-glyph-space'?: number;
// Margins (builder-level)
margin?: number;
'margin-top'?: number;
'margin-bottom'?: number;
'margin-left'?: number;
'margin-right'?: number;
// Sizing
'min-width'?: number;
center?: boolean;
// Cropping (builder-level)
crop?: number | 'auto' | 'auto-vertical' | 'compact';
'crop-top'?: number | 'auto';
'crop-bottom'?: number | 'auto';
'crop-left'?: number | 'auto';
'crop-right'?: number | 'auto';
// Grid (builder-level)
grid?: boolean;
'grid-color'?: string;
'grid-major-color'?: string;
'grid-medium-color'?: string;
'grid-minor-color'?: string;
'grid-sky-color'?: string;
'grid-earth-color'?: string;
'grid-stroke-width'?: number;
'grid-major-stroke-width'?: number;
'grid-medium-stroke-width'?: number;
'grid-minor-stroke-width'?: number;
'grid-sky-stroke-width'?: number;
'grid-earth-stroke-width'?: number;
// Colors and background
color?: string;
background?: string;
'background-top'?: string;
'background-mid'?: string;
'background-bottom'?: string;
// Text and metadata
text?: string;
'svg-desc'?: string;
'svg-title'?: string;
'svg-height'?: number;
// Error handling
'error-placeholder'?: boolean;
// SVG pass-through attributes (any key not in the known set)
[key: string]: string | number | undefined;
}
/** Cascading option layers: defaults (lowest priority) and overrides (highest priority). */
export interface OptionLayers {
defaults?: BlissOptions;
overrides?: BlissOptions;
}
// --- Element snapshots ---
/** Bounding box of an element in absolute SVG coordinates. */
export interface ElementBounds {
readonly minX: number;
readonly maxX: number;
readonly minY: number;
readonly maxY: number;
readonly width: number;
readonly height: number;
}
/** A frozen, read-only snapshot of an element in the composition tree. */
export interface ElementSnapshot {
readonly key: string;
/**
* The input code that produces this element (e.g. `'B431'`, `'Xa'`, `'H'`).
* At part level, the structural lookup key the user would write (`'B81'`,
* `'H'`, `'Xa'`, `'TSP'`, `'Xα'`, `'Xhαllo'`). At glyph level, the input
* code only when the glyph is actually a glyph: B-codes (`'B431'`), single
* X-codes (`'Xa'`, `'Xα'`), or `define()`d `type:'glyph'` aliases
* (`'LOVE'`); `''` for composites, bare shape primitives, and
* multi-character text fallback. Always `''` at group level. Note: this is
* the live identity. `toString()` and `toJSON()` decompose alias names by
* default; pass `{ preserve: true }` to keep them in serialized output.
*/
readonly codeName: string;
/**
* The rendered Unicode character for an external glyph (e.g. `'a'` for
* `Xa`, `'α'` for `Xα`). `''` for B-codes, composites, shape primitives,
* multi-character text fallback, and non-glyph levels.
*/
readonly char: string;
readonly x: number;
readonly y: number;
readonly offsetX: number;
readonly offsetY: number;
readonly width: number;
readonly height: number;
readonly advanceX: number;
readonly baseWidth: number;
readonly level: number;
readonly isRoot: boolean;
readonly isGroup: boolean;
readonly isGlyph: boolean;
readonly isPart: boolean;
readonly bounds: ElementBounds;
readonly isIndicator: boolean;
readonly isShape: boolean;
readonly isBlissGlyph: boolean;
readonly isExternalGlyph: boolean;
readonly isHeadGlyph: boolean;
readonly isSpaceGroup: boolean;
readonly index: number;
readonly parentKey: string | null;
readonly children: readonly ElementSnapshot[];
}
// --- Element handle (live mutation API) ---
/**
* A live handle referencing a node in the raw composition object.
* Returned by `getElementByKey()`, `group()`, `glyph()`, and `part()`.
* Mutations through a handle trigger a rebuild of the composition.
*/
export declare class ElementHandle {
/** Structural depth: 1 = group, 2 = glyph, 3+ = part. */
readonly level: number;
/** True when level === 1 (a word group). */
readonly isGroup: boolean;
/** True when level === 2 (a Bliss character). */
readonly isGlyph: boolean;
/** True when level >= 3 (a part within a character). */
readonly isPart: boolean;
/**
* The input code that produces this element (e.g. `'B431'`, `'Xa'`, `'H'`).
* At part level, the structural lookup key the user would write (`'B81'`,
* `'H'`, `'Xa'`, `'TSP'`, `'Xα'`, `'Xhαllo'`). At glyph level, the input
* code only when the glyph is actually a glyph: B-codes (`'B431'`), single
* X-codes (`'Xa'`, `'Xα'`), or `define()`d `type:'glyph'` aliases
* (`'LOVE'`); `''` for composites, bare shape primitives, and
* multi-character text fallback. Always `''` at group level. Note: this is
* the live identity. `toString()` and `toJSON()` decompose alias names by
* default; pass `{ preserve: true }` to keep them in serialized output.
*/
readonly codeName: string;
/**
* The rendered Unicode character for an external glyph (e.g. `'a'` for
* `Xa`, `'α'` for `Xα`). `''` for B-codes, composites, shape primitives,
* multi-character text fallback, and non-glyph levels.
*/
readonly char: string;
/** Stable across mutations. Use with `getElementByKey(key)` to recover a handle to this same node later. */
readonly key: string;
/** Whether this part is an indicator. Only true on part-level handles. */
readonly isIndicator: boolean;
/** Whether this part is a shape primitive. */
readonly isShape: boolean;
/** Whether this glyph is a B-code Bliss character. */
readonly isBlissGlyph: boolean;
/** Whether this glyph is an external font character. */
readonly isExternalGlyph: boolean;
/** Whether this glyph is the head of its word group. */
readonly isHeadGlyph: boolean;
/** Whether this group is a space separator (TSP/QSP). */
readonly isSpaceGroup: boolean;
// --- Dimensions (read-only, from snapshot) ---
/** Absolute x position of this element's origin. */
readonly x: number;
/** Absolute y position of this element's origin. */
readonly y: number;
/** Position offset relative to the parent. */
readonly offsetX: number;
/** Position offset relative to the parent. */
readonly offsetY: number;
/** Total width including indicator overhang. */
readonly width: number;
/** Total height. */
readonly height: number;
/** Absolute bounding box. */
readonly bounds: ElementBounds;
/** Horizontal spacing step to next sibling. */
readonly advanceX: number;
/** Width excluding indicators. Equals width when no indicators present. */
readonly baseWidth: number;
/** Returns all dimension properties at once. */
measure(): {
x: number;
y: number;
offsetX: number;
offsetY: number;
width: number;
height: number;
bounds: ElementBounds;
advanceX: number;
baseWidth: number;
};
// --- Navigation ---
/** Returns the head glyph handle within this group. Only valid on group handles. */
headGlyph(): ElementHandle | null;
/** Returns the glyph at the given index within this group. Negative indices count from the end (-1 = last). Only valid on group handles. */
glyph(index: number): ElementHandle | null;
/**
* Returns the part at the given index. Negative indices count from the end (-1 = last).
* Valid on glyph handles (returns a part of the glyph) and
* part handles (returns a nested sub-part).
*/
part(index: number): ElementHandle | null;
// --- Mutation: add/insert ---
/** Appends a glyph to this group. Only valid on group handles. */
addGlyph(code: string, opts?: BlissOptions | OptionLayers): this;
/** Inserts a glyph at the given index in this group. Only valid on group handles. */
insertGlyph(index: number, code: string, opts?: BlissOptions | OptionLayers): this;
/** Appends a part to this glyph. On group handles, delegates to the last glyph. */
addPart(code: string, opts?: BlissOptions | OptionLayers): this;
/** Inserts a part at the given index in this glyph. Only valid on glyph handles. */
insertPart(index: number, code: string, opts?: BlissOptions | OptionLayers): this;
// --- Mutation: remove/replace (self) ---
/** Removes this element. Cascades: removing the last part removes its glyph, etc. */
remove(): undefined;
/** Disconnects this element from its parent without cascade cleanup. May leave empty containers. */
detach(): undefined;
/** Replaces this element with a new one. Valid on glyph and part handles. */
replace(code: string, opts?: BlissOptions | OptionLayers): this;
// --- Mutation: remove/replace (parent-centric, by index) ---
/** Removes the glyph at the given index in this group. Only valid on group handles. */
removeGlyph(index: number): this;
/** Replaces the glyph at the given index in this group. Only valid on group handles. */
replaceGlyph(index: number, code: string, opts?: BlissOptions | OptionLayers): this;
/** Removes the part at the given index in this glyph. Only valid on glyph handles. */
removePart(index: number): this;
/** Replaces the part at the given index in this glyph. Only valid on glyph handles. */
replacePart(index: number, code: string, opts?: BlissOptions | OptionLayers): this;
// --- Mutation: indicators ---
/** Replaces all indicators on this glyph with the given indicator codes. Preserves semantic indicators by default. Only valid on glyph handles. */
applyIndicators(code: string, opts?: { stripSemantic?: boolean }): this;
/** Removes all grammatical indicators from this glyph. Preserves semantic indicators by default. Only valid on glyph handles. */
clearIndicators(opts?: { stripSemantic?: boolean }): this;
/** Applies indicators to the head glyph of this group. Preserves semantic indicators by default. Only valid on group handles. */
applyHeadIndicators(code: string, opts?: { stripSemantic?: boolean }): this;
/** Removes grammatical indicators from the head glyph of this group. Preserves semantic indicators by default. Only valid on group handles. */
clearHeadIndicators(opts?: { stripSemantic?: boolean }): this;
// --- Mutation: space/word structure ---
/** Splits this word group into two at the glyph boundary, inserting a space between. Only valid on group handles. */
splitAt(glyphIndex: number): this;
/** Merges this word group with the next one, removing spaces between them. Only valid on group handles. */
mergeWithNext(): this;
// --- Mutation: options ---
/** Sets or merges options on this element. Accepts flat options (treated as overrides) or { defaults, overrides }. */
setOptions(opts: BlissOptions | OptionLayers): this;
/** Removes specific option keys from this element. */
removeOptions(...keys: string[]): this;
}
// --- Definition types ---
/** Context object passed to custom `getPath` functions. */
export interface ShapeContext {
[key: string]: any;
}
/** Definition for a custom glyph (composed from existing codes). */
export interface GlyphDefinition {
type?: 'glyph';
codeString: string;
isIndicator?: boolean;
anchorOffsetX?: number;
anchorOffsetY?: number;
width?: number;
shrinksPrecedingWordSpace?: boolean;
kerningRules?: Record<string, any>;
defaultOptions?: BlissOptions;
}
/** Definition for a custom shape (rendered via getPath or codeString). */
export interface ShapeDefinition {
type?: 'shape';
getPath?: (ctx: ShapeContext) => string;
codeString?: string;
width?: number;
height?: number;
x?: number;
y?: number;
extraPathOptions?: Record<string, any>;
defaultOptions?: BlissOptions;
}
/** Definition for an external glyph (custom rendering around a Unicode character). */
export interface ExternalGlyphDefinition {
type: 'externalGlyph';
getPath: (ctx: ShapeContext) => string;
width: number;
/** The rendered Unicode character (e.g. `'a'` for the external glyph registered as `'Xa'`). */
char: string;
y?: number;
height?: number;
kerningRules?: Record<string, any>;
defaultOptions?: BlissOptions;
}
/** Bare alias definition (maps a code name to a code string). */
export interface BareDefinition {
codeString: string;
defaultOptions?: BlissOptions;
}
/** Union of all definition types accepted by `BlissSVGBuilder.define()`. */
export type CodeDefinition =
| GlyphDefinition
| ShapeDefinition
| ExternalGlyphDefinition
| BareDefinition;
/** Definition type identifiers. */
export type DefinitionType = 'shape' | 'glyph' | 'externalGlyph' | 'bare' | 'space';
/** Result returned by `BlissSVGBuilder.define()`. */
export interface DefineResult {
defined: string[];
skipped: string[];
errors: string[];
}
/** Frozen metadata returned by `BlissSVGBuilder.getDefinition()`. */
export interface DefinitionMetadata {
readonly type: DefinitionType;
readonly isBuiltIn: boolean;
readonly [key: string]: any;
}
// --- Serialization output ---
/** Normalized parsed structure returned by `toJSON()`. */
export interface BlissJSON {
options?: Record<string, string>;
groups: Array<{
options?: Record<string, string>;
glyphs?: Array<{
codeName?: string;
options?: Record<string, string>;
isHeadGlyph?: boolean;
parts?: Array<{
codeName: string;
options?: Record<string, string>;
x?: number;
y?: number;
parts?: Array<any>;
}>;
}>;
}>;
}
// --- Warnings ---
/** A warning generated when the builder encounters an unknown or invalid code. */
export interface Warning {
/** Warning type identifier (e.g., 'UNKNOWN_CODE'). */
readonly code: string;
/** Human-readable description of the issue. */
readonly message: string;
/** The problematic DSL code that triggered the warning. */
readonly source: string;
}
// --- Builder stats ---
export interface BuilderStats {
groupCount: number;
glyphCount: number;
}
// --- Main class ---
export declare class BlissSVGBuilder {
/**
* Creates an instance of BlissSVGBuilder.
* @param input - A DSL string, a plain object from `toJSON()`, or omitted for an empty builder
* @param options - Defaults/overrides to merge, or flat options treated as overrides
*/
constructor(input?: string | BlissJSON, options?: BlissOptions | OptionLayers);
/**
* Library version string (set at build time).
* Also exported as the named `LIB_VERSION` constant.
*/
static readonly LIB_VERSION: string;
// --- SVG output (getters) ---
/** SVG content (path elements and groups) without the outer `<svg>` wrapper. */
readonly svgContent: string;
/** Parsed SVG as a DOM element. Requires a DOM environment. */
readonly svgElement: SVGSVGElement;
/** Complete SVG markup without XML declaration. */
readonly svgCode: string;
/** Complete SVG markup with XML declaration. */
readonly standaloneSvg: string;
// --- Warnings ---
/** Warnings generated during parsing/rendering (unknown codes, invalid syntax, etc.). */
readonly warnings: readonly Warning[];
// --- Element tree (getters) ---
/** Root element snapshot (frozen tree of all elements). */
readonly elements: ElementSnapshot;
/** Non-space group snapshots. */
readonly groups: readonly ElementSnapshot[];
/** Group and glyph counts. */
readonly stats: BuilderStats;
// --- Traversal and querying ---
/** Depth-first traversal of all element snapshots. Return `false` to stop early. */
traverse(callback: (el: ElementSnapshot) => boolean | void): void;
/** Returns all element snapshots matching the predicate. */
query(predicate: (el: ElementSnapshot) => boolean): ElementSnapshot[];
/** Looks up an element handle by its snapshot key. */
getElementByKey(key: string): ElementHandle | null;
/** Returns a handle to the non-space group at the given index. Negative indices count from the end (-1 = last). */
group(index: number): ElementHandle | null;
/** Returns a handle to any group (including spaces) at the given raw index. Negative indices count from the end (-1 = last). */
element(index: number): ElementHandle | null;
/** Total number of raw groups (including space groups). */
readonly elementCount: number;
/** Returns a handle to the glyph at the given flat index across all groups. Negative indices count from the end (-1 = last). */
glyph(flatIndex: number): ElementHandle | null;
/** Returns a handle to the part at the given flat index across all glyphs. Negative indices count from the end (-1 = last). */
part(flatIndex: number): ElementHandle | null;
/** Returns the root element snapshot (alias for `elements`). */
snapshot(): ElementSnapshot;
// --- Building and manipulation ---
/** Appends a new glyph group with automatic space management. */
addGroup(code: string, opts?: BlissOptions | OptionLayers): this;
/** Appends a glyph to the last non-space group (creates one if empty). */
addGlyph(code: string, opts?: BlissOptions | OptionLayers): this;
/** Appends a part to the last glyph of the last group. */
addPart(code: string, opts?: BlissOptions | OptionLayers): this;
/** Inserts a group at the given index. Negative indices count from the end. */
insertGroup(index: number, code: string, opts?: BlissOptions | OptionLayers): this;
/** Removes the group at the given index. Negative indices count from the end. */
removeGroup(index: number): this;
/** Replaces the group at the given index with new content. Negative indices count from the end. */
replaceGroup(index: number, code: string, opts?: BlissOptions | OptionLayers): this;
/** Merges another builder's content into this one. Appends the other builder's groups with a space between. The other builder's global options are discarded. */
merge(other: BlissSVGBuilder): this;
/** Splits this builder at the given group index. This builder keeps the left half; a new builder with the right half is returned. Both share the same global options. */
splitAt(groupIndex: number): BlissSVGBuilder;
/** Appends a raw group with no automatic space management. SP auto-resolves to TSP/QSP. */
addElement(code: string, opts?: BlissOptions | OptionLayers): this;
/** Inserts a raw group at the given index with no automatic space management. SP auto-resolves. */
insertElement(index: number, code: string, opts?: BlissOptions | OptionLayers): this;
/** Removes the raw group at the given index (plain splice, no space cleanup). */
removeElement(index: number): this;
/** Replaces the raw group at the given index with new content. */
replaceElement(index: number, code: string, opts?: BlissOptions | OptionLayers): this;
/** Removes all content from the builder. */
clear(): this;
// --- Serialization ---
/**
* Returns a portable DSL string. Custom codes are decomposed to built-in
* codes by default; pass `{ preserve: true }` to keep custom names.
*/
toString(options?: { preserve?: boolean }): string;
/**
* Returns a normalized parsed structure (plain object). Custom glyph codes
* are resolved to built-in codes by default; pass `{ preserve: true }` to keep them.
*/
toJSON(options?: { preserve?: boolean; deep?: boolean }): BlissJSON;
// --- Static: definition management ---
/**
* Defines one or more custom codes (glyphs, shapes, external glyphs, or bare aliases).
* @param definitions - Map of code names to their definitions
* @param options - Pass `{ overwrite: true }` to replace existing custom definitions
*/
static define(
definitions: Record<string, CodeDefinition>,
options?: { overwrite?: boolean }
): DefineResult;
/** Returns `true` if a code is defined (built-in or custom). */
static isDefined(code: string): boolean;
/** Returns frozen metadata for a code, or `null` if not found. */
static getDefinition(code: string): DefinitionMetadata | null;
/** Lists all defined codes, optionally filtered by type. */
static listDefinitions(filter?: { type?: DefinitionType }): string[];
/** Removes a custom definition. Throws if the code is built-in. */
static removeDefinition(code: string): boolean;
/**
* Patches properties on an existing custom definition.
* Only keys valid for the definition's type are accepted.
* Built-in definitions cannot be patched.
*/
static patchDefinition(code: string, changes: Partial<CodeDefinition>): { patched: true };
}
/**
* Library version string (set at build time).
* Also accessible as the `BlissSVGBuilder.LIB_VERSION` static.
*/
export declare const LIB_VERSION: string;
export default BlissSVGBuilder;