jssm
Version:
A Javascript finite state machine (FSM) with a terse DSL and a simple API. Most FSMs are one-liners. Fast, easy, powerful, well tested, typed with TypeScript, and visualizations. MIT License.
136 lines (133 loc) • 5.02 kB
JavaScript
import { css, LitElement, html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { fsl_to_svg_string } from 'jssm/viz';
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
/**
* Normalize an arbitrary thrown value into a {@link JssmVizErrorDetail}.
* Accepts anything (Error instances, JssmErrors with `.location`, plain
* strings, etc.) and always produces a string `message`.
*
* ```typescript
* normalize_viz_error(new Error('boom'));
* // => { message: 'boom', location: undefined }
*
* normalize_viz_error({ message: 'parse failed', location: { line: 1 } });
* // => { message: 'parse failed', location: { line: 1 } }
*
* normalize_viz_error('bare string failure');
* // => { message: 'bare string failure', location: undefined }
* ```
*
* @param e The thrown value to normalize.
* @returns A `{ message, location }` object suitable for use as the
* `detail` of a `viz-error` `CustomEvent`.
*/
function normalize_viz_error(e) {
if (typeof e === 'object' && e !== null) {
const rec = e;
const raw_message = rec.message;
const message = (typeof raw_message === 'string' && raw_message.length > 0)
? raw_message
: String(e);
return { message, location: rec.location };
}
return { message: String(e), location: undefined };
}
/**
* Web component that renders a jssm machine as inline SVG.
*
* @element jssm-viz
* @cssproperty [--jssm-viz-min-height=100px] - Minimum height of the rendered SVG container.
* @fires {CustomEvent<{ message: string; location?: unknown }>} viz-error - Fires when the FSL source fails to parse or render.
*/
class JssmViz extends LitElement {
constructor() {
super(...arguments);
/** FSL source to render. */
this.fsl = '';
/** Optional Graphviz layout engine override (e.g. 'dot', 'neato'). */
this.engine = undefined;
this._svg = '';
}
/**
* Lit lifecycle hook. Triggers an async SVG render whenever `fsl` or
* `engine` change.
*
* @param changed - Map of changed reactive properties supplied by Lit.
*/
willUpdate(changed) {
if (changed.has('fsl') || changed.has('engine')) {
this._renderSvg();
}
}
/**
* Render the current `fsl` source to an SVG string via the headless
* `fsl_to_svg_string` pipeline. Updates `_svg` on success; emits a
* `viz-error` `CustomEvent` on failure. Guards against stale results
* when `fsl` changes mid-flight.
*
* @returns A promise that resolves once the render attempt has finished.
*/
async _renderSvg() {
const source = this.fsl;
if (!source) {
this._svg = '';
return;
}
try {
const result = await fsl_to_svg_string(source, this.engine ? { engine: this.engine } : undefined);
// Guard against stale results: only commit if fsl has not changed since this render started.
if (this.fsl === source) {
this._svg = result;
}
}
catch (e) {
this._svg = '';
this.dispatchEvent(new CustomEvent('viz-error', {
detail: normalize_viz_error(e),
bubbles: true,
composed: true,
}));
}
}
/**
* Lit render method. Injects the most recent SVG string into the shadow
* tree via the `unsafeHTML` directive.
*
* SVG content originates from `@viz-js/viz` (Graphviz WASM), which emits
* sanitized SVG. `unsafeHTML` is required because Lit's template-literal
* interpolation otherwise escapes the markup as text. The directive name
* makes the trust boundary explicit at the call site.
*
* @returns A Lit `TemplateResult` wrapping the SVG in a `.container` div.
*/
render() {
return html `<div class="container">${unsafeHTML(this._svg)}</div>`;
}
}
JssmViz.styles = css `
:host {
display: block;
min-height: var(--jssm-viz-min-height, 100px);
}
.container {
width: 100%;
height: 100%;
}
`;
__decorate([
property({ type: String })
], JssmViz.prototype, "fsl", void 0);
__decorate([
property({ type: String })
], JssmViz.prototype, "engine", void 0);
__decorate([
state()
], JssmViz.prototype, "_svg", void 0);
export { JssmViz, normalize_viz_error };