lispgram
v0.7.0
Published
A lispy surface language compiler and SVG visualizer for NCF diagrams.
Maintainers
Readme
lispgram
lispgram is a small JavaScript package that gives you:
- a user-facing lispy surface language
- a compiler from that surface language into NCF
- a lightweight SVG visualizer that can render either Lispgram source or raw NCF
The public compiler now has three explicit syntaxes:
Lispgram surface human-friendly input
Semantic core official tiny fact language: node, arrow, contains
NCF visualization-oriented graph syntaxIt also exposes optional interpretations of the semantic core. The first is the normalized tree interpretation, which derives a single-owner containment tree using a generated root and least-common-ancestor ownership for arrows.
NCF remains the rendering representation. Author-facing source compiles through the semantic core into:
(cy
(nodes ...)
(edges ...))Source layout by visual language layer
The implementation is organized around layers that can each be emitted as a small S-expression language for inspection or visualization:
src/layers/00-sexpr generic S-expression substrate
src/layers/01-surface typed Lispgram surface AST
src/layers/02-surface-facts surface facts / author-intent facts, including fan-in and fan-out
src/layers/03-semantic-core official semantic core: node, contains, arrow only
src/layers/04-ncf-model reified graph model: object nodes, arrow nodes, typed edges
src/layers/05-ncf-text textual NCF parse / validate / emit
src/layers/06-render-svg NCF model -> SVG
src/layers/07-browser browser integration
src/interpretations/normalized-tree optional derived ownership/tree viewUse toLayerSexp(source) to inspect the visualizable layer snapshots. See LAYERS.md, CORE_GRAMMAR.md, INTERPRETATIONS.md, and INTERACTIONS.md for details.
Supported surface forms
; initialize a node
(P)
; initialize a node with a label
(P{hi})
; initialize a node with label plus fields
(P{Person { color blue rank 2 }})
; fully explicit data field map
(P{{ label Person color blue rank 2 }})
; parent/child relationships
(X Y Z)
(X Y (Z P))
(X (Y (Z P)))
; arrows
(-> e A B)
(-> e{hi} A B)
(-> e{maps { color purple weight 2 }} A B)
; fan-out
(-> e A [B C D])
; fan-in
(-> e [A B C] D)What this starter implements
- nodes and arrows with optional
{label}suffixes and richer{label { key value ... }}/{{ key value ... }}field maps - parent/child trees compiled to
(@parent X) - arrows compiled as NCF arrow carrier nodes with:
(@class "arrow")(@class "arrowFrom")(@class "arrowTo")
- fan-in / fan-out expansion into multiple arrows that keep the same visible label
- an official semantic core grammar with only
node,arrow, andcontains - semantic core parsing/emitting for external rule or constraint systems
- a normalized tree interpretation for derived ownership/layout
- containment of arrows as well as object nodes in the semantic core
- raw NCF passthrough
- browser auto-initialization for
<pre class="lispgram">...</pre>blocks - heuristic SVG routing for parallel, anti-parallel, and higher-order arrows
- compound/container labels rendered only in the header band
Install
npm install lispgramBrowser usage
<pre class="lispgram">
(Y X)
(-> f A B)
(-> g B A)
(-> h X Y)
</pre>
<script type="module">
import lispgram from "lispgram";
lispgram.initialize({
startOnLoad: true,
showArrowGlyphs: true,
clickToFocus: true
});
</script>Library usage
import lispgram from "lispgram";
const source = `
(Y X)
(-> f A B)
(-> g B A)
(-> h X Y)
`;
const ncfString = lispgram.toNCF(source);
const ncfDoc = lispgram.toNCFDoc(source);Render into a DOM node:
import { render } from "lispgram";
render(document.querySelector("#app"), `
(P A B)
(-> left A B)
(-> right B A)
`);Interactive diagrams
Interactivity is attached from JavaScript, not from Lispgram syntax. Give a render a stable diagramId, or set data-lispgram-diagram on an auto-rendered <pre>, then select semantic elements by their normalized data fields.
import { render, diagram } from "lispgram";
render(document.querySelector("#app"), `
(lg-core
(node (A (@data clickable true info "Node A")))
(node B)
(arrow (f (@data clickable true info "A to B")) A B))
`, { diagramId: "P" });
diagram("P")
.select((elem) => elem.data.clickable === true)
.addonclick((elem) => alert(elem.data.info));Each selected elem describes the semantic cell rather than the raw SVG node:
{
id: "f",
type: "arrow", // or "node"
kind: "arrow",
data: { info: "A to B" },
source: "A",
target: "B",
parent: null,
elements: [/* generated SVG path/label/glyph elements */]
}You can also select with a small object matcher:
diagram("P")
.select({ type: "arrow", data: { clickable: true } })
.addOnClick((elem) => console.log(elem.source, elem.target));For auto-rendered diagrams:
<pre class="lispgram" data-lispgram-diagram="P">
(A{{ clickable true info "Node A" }} B)
</pre>For full interaction details, see INTERACTIONS.md.
API
parseDocument(source)
Parses Lispgram surface syntax into an AST.
compileLispgram(source)
Returns:
{
ast,
surfaceFacts,
semanticCore, // official semantic core
doc // normalized NCF JS object
}sourceToSemanticCore(source)
Compiles Lispgram surface syntax to the official semantic core: fan-free node, arrow, and contains facts. If the input is already wrapped semantic core (lg-core ...), it is parsed directly.
parseSemanticCore(source)
Parses textual semantic core syntax wrapped in (lg-core ...).
isSemanticCoreSource(source)
Detects wrapped semantic core input without treating it as Lispgram surface syntax.
emitSemanticCoreSexp(semanticCore)
Serializes the semantic core back to textual (lg-core ...) syntax.
validateSemanticCore(semanticCore)
Validates the tiny semantic core shape. This does only structural validation; richer semantics are intended for external rule/constraint systems.
semanticCoreToNCFDoc(semanticCore)
Projects an official semantic core document into the NCF document object used by the renderer.
semanticCoreToNormalizedTree(semanticCore, options?)
Derives the optional normalized tree interpretation from semantic-core facts. This creates a generated root, keeps explicit contains, and infers otherwise-unowned arrow owners from the least common ancestor of their endpoints.
emitNormalizedTreeSexp(tree)
Serializes the normalized tree interpretation as (lg-tree ...) for inspection.
normalizedTreeToNCFDoc(tree)
Projects a normalized tree interpretation to an NCF document object. The devtools can optionally render through this interpretation.
toNCFDoc(source)
Compiles Lispgram source or wrapped semantic core to an NCF document object. If the input is already raw NCF, it is parsed and returned.
toNCF(source)
Compiles Lispgram source into an NCF string.
parseNCF(source)
Parses raw NCF.
emitNCFDoc(doc)
Serializes an NCF doc object back to text.
render(target, source, options?)
Renders Lispgram or NCF into an SVG inside target.
diagram(id?)
Returns the registered interactive diagram controller for id. With no argument, returns the most recently registered diagram. Controllers support .select(predicateOrMatcher), .selectAll(), .get(id), .on(eventName, selector, handler), .addOnClick(selector, handler), and .clearInteractions().
registerDiagram(id, { doc, svg, target? })
Registers an already-rendered NCF document/SVG pair for interactive selection. render() and initialize() call this automatically.
listDiagrams()
Returns the registered diagram IDs.
initialize(options?)
Finds matching elements and auto-renders them.
Options:
{
selector: ".lispgram",
startOnLoad: true,
showNCF: false,
showArrowGlyphs: false,
clickToFocus: true,
arrowGlyphRadius: 5.75,
width: "100%",
height: 480,
diagramId: undefined
}Per-diagram overrides are also available with data attributes:
<pre class="lispgram"
data-lispgram-diagram="P"
data-lispgram-arrow-glyphs="true"
data-lispgram-click-focus="true"
data-lispgram-arrow-glyph-radius="6.5">
(-> f A B)
</pre>Notes
This package is intentionally small and readable. It is a good base for:
- improving the layout engine
- extending the surface language
- integrating external rule/constraint systems over the semantic core
- adding richer styling directives
- expanding support for higher-order arrows and editor UX
Dev visualizer
The package now includes a browser-based dev tool with Lispgram + raw NCF input, compiled/normalized NCF output, orthogonal routing, wiring overlay, arrow-node glyphs, click focus, zoom, and fit-to-view.
Run it from the repo with:
npm run dev:visualizerThen open:
http://localhost:4173/examples/devtool.htmlThe dev tool accepts Lispgram surface syntax, wrapped semantic core (lg-core ...), or raw NCF. When the input is Lispgram, it compiles it through the semantic core first and shows both the semantic core and normalized NCF in the side panel.
It can also run as a standalone static page. examples/devtool.html now auto-loads the local ../src/index.js module when it is available and otherwise falls back to:
https://unpkg.com/lispgram/src/index.jsThat means you can open the file directly, host it from any static site, or copy examples/devtool-standalone.html by itself and still use the package from unpkg.
Optional query param overrides:
?module=localforces the local repo module?module=unpkgforces the unpkg module
Development
npm testFocus behavior
When clickToFocus is enabled, clicking a node, compound, edge path, edge label, or arrow glyph focuses that element within its own diagram. clickToFocus defaults to true; set data-lispgram-click-focus="false" or pass clickToFocus: false to disable it.
Demo
go to repo root directory
python -m http.servernavigate to examples/basic.html in browser
Browser script Drop-in use
<pre class="lispgram">
(lispgram
(A)
(-> e X Y)
)
</pre>
<script type="module">
import lispgram from "https://unpkg.com/lispgram/src/index.js";
lispgram.initialize({
startOnLoad: true,
showArrowGlyphs: true,
clickToFocus: true,
arrowGlyphRadius: 5.75
});
</script>