@schemd/core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -671
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,725 +2,118 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@schemd/core)
|
|
4
4
|
[](https://github.com/Sirneij/schemd/actions/workflows/ci.yml)
|
|
5
|
-
[](https://bundlejs.com/?q=@schemd/core)
|
|
6
|
-
[](https://github.com/Sirneij/schemd/actions/workflows/ci.yml)
|
|
7
5
|
[](https://github.com/Sirneij/schemd/blob/main/LICENSE)
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
layout engine, or graphics dependency.**
|
|
11
|
-
|
|
12
|
-
`@schemd/core` is a strict TypeScript compiler for coordinate-authored electrical, digital, quantum, and
|
|
13
|
-
system-architecture diagrams. It is designed for trusted server and build boundaries: validate a
|
|
14
|
-
small text DSL, produce intrinsically sized SVG, cache the result, and ship only markup to readers.
|
|
15
|
-
The generic `ic`, `port`, and connection primitives can express UML-style component and architecture
|
|
16
|
-
views; `@schemd/core` does not claim to implement the full UML class, sequence, or state-machine grammar.
|
|
17
|
-
|
|
18
|
-
## Why @schemd/core
|
|
19
|
-
|
|
20
|
-
- **Zero runtime dependencies.** `marked` is a type-only peer integration; emitted compiler
|
|
21
|
-
JavaScript does not import it.
|
|
22
|
-
- **Zero browser compiler cost.** The package is declared `browser: false`. Parse on a server,
|
|
23
|
-
content-ingestion worker, CLI, or static-build boundary.
|
|
24
|
-
- **Deterministic and bounded.** Every source, topology, output, coordinate, color, and option is
|
|
25
|
-
validated before SVG is returned.
|
|
26
|
-
- **Zero layout shift by construction.** Required `bounds="WIDTHxHEIGHT"` metadata becomes static
|
|
27
|
-
`width`, `height`, and `viewBox` attributes during compilation.
|
|
28
|
-
- **Engineering-aware routing.** Straight, cubic Bézier, and orthogonal routes share real component
|
|
29
|
-
port coordinates; orthogonal traces avoid component AABBs and receive bridge arcs at crossings.
|
|
30
|
-
- **Accessible output.** Every diagram has `<title>` and `<desc>` metadata. Interactive mode adds
|
|
31
|
-
keyboard targets and semantic event-delegation hooks.
|
|
32
|
-
- **Themeable without recompilation.** Semantic color tokens become CSS classes and safe aliases
|
|
33
|
-
resolve through host-owned custom properties.
|
|
34
|
-
- **Tiny math labels.** A linear, zero-dependency micro-parser converts common engineering notation
|
|
35
|
-
into SVG `<tspan>` runs—no TeX, MathML, font asset, or client hydration required.
|
|
36
|
-
|
|
37
|
-
## Installation
|
|
7
|
+
Write engineering diagrams as text and compile them to accessible inline SVG.
|
|
38
8
|
|
|
39
|
-
|
|
40
|
-
npm install @schemd/core marked
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`marked` is a peer because the host application owns its Markdown version. Direct parser/renderer
|
|
44
|
-
usage does not instantiate Marked.
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
import {
|
|
48
|
-
parseSchematic,
|
|
49
|
-
parseSchematicFence,
|
|
50
|
-
renderSchematic,
|
|
51
|
-
type SchemdOutputMode
|
|
52
|
-
} from '@schemd/core';
|
|
53
|
-
|
|
54
|
-
const info = 'schemd bounds="640x260" title="Sensor front end"';
|
|
55
|
-
const fence = parseSchematicFence(info);
|
|
56
|
-
|
|
57
|
-
if (!fence) throw new Error('Expected a schemd fence');
|
|
58
|
-
|
|
59
|
-
const document = parseSchematic(
|
|
60
|
-
`port:VIN "V_{in}" at (60, 130) #blue
|
|
61
|
-
resistor:R1 "10 k\\Omega" at (220, 130) #amber
|
|
62
|
-
capacitor:C1 "100 nF" at (400, 130) #cyan
|
|
63
|
-
VIN.out -> R1.in #blue [ortho]
|
|
64
|
-
R1.out -> C1.in #amber [ortho marker-end=dot]`,
|
|
65
|
-
fence
|
|
66
|
-
);
|
|
9
|
+
Schemd supports electrical components, logic gates, quantum gates, and general system diagrams. It runs on the server or during a build, so the browser only receives SVG—no diagram runtime, DOM, or layout engine.
|
|
67
10
|
|
|
68
|
-
|
|
69
|
-
const trustedSvg = renderSchematic(document, { ...fence, mode });
|
|
70
|
-
```
|
|
11
|
+
## Why Schemd?
|
|
71
12
|
|
|
72
|
-
|
|
13
|
+
- Small, readable text format
|
|
14
|
+
- Deterministic SVG output with fixed dimensions
|
|
15
|
+
- Straight, curved, and obstacle-aware orthogonal connections
|
|
16
|
+
- Accessible titles and descriptions
|
|
17
|
+
- No runtime dependencies
|
|
73
18
|
|
|
74
|
-
|
|
19
|
+
## Install
|
|
75
20
|
|
|
76
|
-
|
|
77
|
-
Markdown host / direct API
|
|
78
|
-
│
|
|
79
|
-
▼
|
|
80
|
-
fence metadata + bounded DSL source
|
|
81
|
-
│
|
|
82
|
-
▼
|
|
83
|
-
lexer and semantic parser ──► frozen, provenance-checked AST
|
|
84
|
-
│
|
|
85
|
-
▼
|
|
86
|
-
port resolution + geometry validation + deterministic routing
|
|
87
|
-
│
|
|
88
|
-
▼
|
|
89
|
-
bounded SVG writer ──► trusted, intrinsically sized inline markup
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
- `parser` validates fence metadata, declarations, colors, component options, ports, and resource
|
|
93
|
-
ceilings before producing an AST.
|
|
94
|
-
- `layout` computes component extents, dynamic IC pins, physical port coordinates, AABB avoidance,
|
|
95
|
-
crossing bridges, and final bounds safety.
|
|
96
|
-
- `renderer` writes escaped SVG through a byte-capped sink and applies the selected output mode.
|
|
97
|
-
- `marked-extension` is an optional host adapter. It owns cumulative per-document budgets and
|
|
98
|
-
delegates every non-`schemd` code block back to the host renderer.
|
|
99
|
-
- `math-label` is a linear label preprocessor invoked by the renderer; it is not a general TeX
|
|
100
|
-
interpreter.
|
|
101
|
-
|
|
102
|
-
Every stage is synchronous and deterministic. There is no DOM measurement, network access,
|
|
103
|
-
filesystem access, timer, random source, global mutable diagram cache, or client-side hydration
|
|
104
|
-
requirement in the package runtime.
|
|
105
|
-
|
|
106
|
-
## Deployment and footprint
|
|
107
|
-
|
|
108
|
-
The compiler belongs exclusively in a trusted server/build graph. Its generated SVG is the only
|
|
109
|
-
artifact that should cross into a browser application.
|
|
110
|
-
|
|
111
|
-
| Surface | Physical cost | Browser execution cost |
|
|
112
|
-
| ------------------------- | ------------------------------------------------------------------------------------------------: | ---------------------: |
|
|
113
|
-
| npm package tarball | 41,386 B (40.4 KiB) in the current `1.0.0` workspace build | 0 B |
|
|
114
|
-
| Unpacked package | 156,011 B (152.4 KiB), including stripped ESM, documented declarations, metadata, and this README | 0 B |
|
|
115
|
-
| Emitted server JavaScript | 92,453 B raw / 22,613 B as individually Gzip-compressed modules in the current build | Server only |
|
|
116
|
-
| `default` SVG | Diagram-dependent; smallest output contract | 0 B JavaScript |
|
|
117
|
-
| `embedded-css` SVG | `default` plus compact CSS, grid, transitions, and glow definitions | 0 B JavaScript |
|
|
118
|
-
| `full` SVG | `embedded-css` plus per-node, wire, and port interaction metadata | Host adapter only |
|
|
119
|
-
|
|
120
|
-
Those server-module figures are measurements, not bundle guarantees. Tree-shaking, minification,
|
|
121
|
-
README growth, and the package manager affect installed size. The build strips comments from
|
|
122
|
-
runtime JavaScript while retaining full TSDoc in `.d.ts` files; source maps are intentionally not
|
|
123
|
-
published. Before publishing, record the authoritative release artifact with:
|
|
21
|
+
Schemd requires Node.js 24 or newer. Install `marked` if you want to use Markdown fences.
|
|
124
22
|
|
|
125
23
|
```sh
|
|
126
|
-
npm
|
|
127
|
-
npm pack --dry-run --json
|
|
128
|
-
wc -c dist/*.js
|
|
24
|
+
npm install @schemd/core marked
|
|
129
25
|
```
|
|
130
26
|
|
|
131
|
-
|
|
132
|
-
and its renderer tests guarantee the relative ordering `default < embedded-css < full` for an
|
|
133
|
-
equivalent document.
|
|
134
|
-
|
|
135
|
-
## Security and resource contract
|
|
136
|
-
|
|
137
|
-
Compilation is deliberately finite:
|
|
138
|
-
|
|
139
|
-
| Budget | Limit |
|
|
140
|
-
| -------------------------------------- | ----------------------------------------: |
|
|
141
|
-
| Source per diagram | 131,072 UTF-16 source characters |
|
|
142
|
-
| Components per diagram/document pass | 512 |
|
|
143
|
-
| Connections per diagram/document pass | 2,048 |
|
|
144
|
-
| Compiled SVG per diagram/document pass | 2,097,152 UTF-8 bytes |
|
|
145
|
-
| Bounds | `64x64` through `4096x4096` integer units |
|
|
146
|
-
| Fence title | 512 characters |
|
|
147
|
-
| IC pins | 64 per side |
|
|
148
|
-
|
|
149
|
-
The Marked extension applies the same budgets cumulatively across every `schemd` fence in one
|
|
150
|
-
Markdown parse. Its `preprocess` hook resets counters for the next document. Once a cumulative limit
|
|
151
|
-
is exhausted, later schemd fences fail immediately without another parse or render. Ordinary prose
|
|
152
|
-
and unrelated code fences never consume these budgets.
|
|
153
|
-
|
|
154
|
-
IDs, labels, options, colors, and XML text are validated or escaped. Parsed ASTs are deeply frozen
|
|
155
|
-
and capability-branded; `renderSchematic` rejects forged or mutated document objects. The grammar
|
|
156
|
-
uses no recursive rule and no data-dependent unbounded loop.
|
|
157
|
-
|
|
158
|
-
> Treat compiler output as trusted only when it came directly from `@schemd/core`. Never pass arbitrary
|
|
159
|
-
> user HTML through the framework examples below.
|
|
27
|
+
## Use with Markdown
|
|
160
28
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
29
|
+
```ts
|
|
30
|
+
import { Marked } from "marked";
|
|
31
|
+
import { schematicMarkedExtension } from "@schemd/core";
|
|
164
32
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
port:INPUT "Sensor bus" at (46, 140) #slate
|
|
168
|
-
resistor:R1 "10 k\Omega" at (150, 140) #amber
|
|
169
|
-
capacitor:C1 "100 nF" at (280, 140) #blue
|
|
170
|
-
inductor:L1 "22 \muH" at (410, 140) #cyan
|
|
171
|
-
diode:D1 "Clamp" at (540, 140) #cyan [type=schottky]
|
|
172
|
-
transistor:Q1 "Output switch" at (690, 140) #phosphor [type=nmos]
|
|
173
|
-
ground:GND "Signal ground" at (690, 250) #slate [style=signal]
|
|
33
|
+
const markdown = new Marked();
|
|
34
|
+
markdown.use(schematicMarkedExtension());
|
|
174
35
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
36
|
+
const html = await markdown.parse(`
|
|
37
|
+
\`\`\`schemd bounds="640x260" title="Sensor input"
|
|
38
|
+
port:VIN "Input" at (60, 130) #blue
|
|
39
|
+
resistor:R1 "10 k\\Omega" at (220, 130) #amber
|
|
40
|
+
capacitor:C1 "100 nF" at (400, 130) #cyan
|
|
179
41
|
|
|
180
|
-
|
|
42
|
+
VIN.out -> R1.in #blue [ortho]
|
|
181
43
|
R1.out -> C1.in #amber [ortho]
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
D1.cathode -> Q1.gate #phosphor [ortho]
|
|
185
|
-
Q1.source -> GND.in #slate
|
|
186
|
-
G1.out2 -> U1.S0 #emerald [ortho marker-end=arrow]
|
|
187
|
-
U1.Y1 -> H1.in #quantum-optical [bezier]
|
|
188
|
-
H1.out -> RZ1.in #purple [line marker-end=dot]
|
|
44
|
+
\`\`\`
|
|
45
|
+
`);
|
|
189
46
|
```
|
|
190
|
-
````
|
|
191
|
-
|
|
192
|
-
`schemd` is case-insensitive in fence metadata, but the language identifier itself is canonical:
|
|
193
|
-
other fenced languages are delegated to the host Markdown renderer and are never compiled as
|
|
194
|
-
diagrams.
|
|
195
47
|
|
|
196
|
-
##
|
|
197
|
-
|
|
198
|
-
Blank lines and lines beginning with `//` are ignored. All other records are either components or
|
|
199
|
-
connections.
|
|
200
|
-
|
|
201
|
-
```text
|
|
202
|
-
kind:ID "label" at (x, y) color [key=value ...]
|
|
203
|
-
ID.port -> ID.port color [line|bezier|ortho marker-start=... marker-end=...]
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### Coordinates, identifiers, and labels
|
|
207
|
-
|
|
208
|
-
- Coordinates use diagram units and must be finite values inside the declared bounds.
|
|
209
|
-
- The renderer reserves the physical component, terminal hotspot, external designator, and label
|
|
210
|
-
gutters; a visually clipped declaration fails rather than producing partially hidden SVG.
|
|
211
|
-
- Component IDs and port names begin with a letter and may contain letters, digits, `_`, or `-`.
|
|
212
|
-
- IDs are unique within one diagram. IC pin names are case-sensitive and unique across all sides.
|
|
213
|
-
- Labels are quoted. Renderer-owned escaping prevents label text from creating markup.
|
|
214
|
-
- Component options use one trailing bracket list. Values containing punctuation or lists are
|
|
215
|
-
quoted as shown in the IC and quantum examples.
|
|
216
|
-
|
|
217
|
-
### Complete component reference
|
|
218
|
-
|
|
219
|
-
| Kind | Options and defaults | Canonical ports and aliases |
|
|
220
|
-
| --------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
221
|
-
| `resistor` | none | `in`, `out`; `left`/`l` → `in`, `right`/`r` → `out` |
|
|
222
|
-
| `capacitor` | none | `in`, `out`; `left`/`l` → `in`, `right`/`r` → `out` |
|
|
223
|
-
| `inductor` | none | `in`, `out`; `left`/`l` → `in`, `right`/`r` → `out` |
|
|
224
|
-
| `diode` | `type=standard\|schottky\|zener\|led`; default `standard` | `anode`/`a`, `cathode`/`k`/`c` |
|
|
225
|
-
| `transistor` | `type=npn\|pnp\|nmos\|pmos`; default `npn` | `base`/`gate`/`b`/`g`, `collector`/`drain`/`c`/`d`, `emitter`/`source`/`e`/`s` |
|
|
226
|
-
| `port` | none | `in`, `out` |
|
|
227
|
-
| `ground` | `style=chassis\|earth\|signal`; default `signal` | `in` |
|
|
228
|
-
| `and`, `nand`, `or`, `nor`, `xor` | `inputs=1..32`, `outputs=1..32`, `standard=ieee\|iec`; defaults `2`, `1`, `ieee` | `in1`…`inN`, `out1`…`outM`; `in`/`out` alias pin 1 |
|
|
229
|
-
| `not` | same gate options; default one input and one output | `in1`…`inN`, `out1`…`outM`; `in`/`out` alias pin 1 |
|
|
230
|
-
| `hadamard` | none | `in`, `out` |
|
|
231
|
-
| `cnot` | none | `in`, `out`, `control`, `target` |
|
|
232
|
-
| `qgate` | optional quoted `parameter`, `matrix`, `phase` | `in`, `out` |
|
|
233
|
-
| `ic` | quoted comma-separated `left`, `right`, `top`, `bottom` lists | Every declared pin; stable `in`/`out` fallbacks described below |
|
|
234
|
-
|
|
235
|
-
IEEE gates use conventional curved or triangular contours. IEC gates use rectangular logic blocks.
|
|
236
|
-
Input and output pins are distributed deterministically across the computed body height.
|
|
237
|
-
|
|
238
|
-
### Dynamic IC blocks and UML-style architecture views
|
|
239
|
-
|
|
240
|
-
An `ic` creates a general rectangular component whose pins are part of the addressable graph:
|
|
241
|
-
|
|
242
|
-
```schemd
|
|
243
|
-
ic:U1 "Flight computer" at (420, 220) #blue [left="CLK,DATA,ENABLE" right="FILTERED,FAULT" top="VCC" bottom="GND,RESET"]
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
The body is calculated without a measurement API:
|
|
247
|
-
|
|
248
|
-
```text
|
|
249
|
-
bodyWidth = max(88, max(topPins, bottomPins) * 22 + 24)
|
|
250
|
-
bodyHeight = max(64, max(leftPins, rightPins) * 18 + 24)
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
`U1.CLK`, `U1.DATA`, `U1.FAULT`, and every other declared pin are valid endpoints. The `in` alias
|
|
254
|
-
first resolves a declared `in1`, then the first left or first top pin. `out` first resolves `out1`,
|
|
255
|
-
then the first right or first bottom pin. Literal `in` and `out` pin names are reserved. At least one
|
|
256
|
-
pin must be declared.
|
|
257
|
-
|
|
258
|
-
Combined with `port` terminals and labeled connections, IC blocks can model deployment nodes,
|
|
259
|
-
services, processors, buses, and UML-style component boundaries. They are intentionally generic;
|
|
260
|
-
there is no implicit UML relationship, multiplicity, class member, lifeline, or sequence semantics.
|
|
261
|
-
|
|
262
|
-
### Connection routing
|
|
263
|
-
|
|
264
|
-
```text
|
|
265
|
-
SOURCE.port -> TARGET.port color [ortho marker-start=dot marker-end=arrow]
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
- `[line]` is the default direct segment.
|
|
269
|
-
- `[bezier]` emits a deterministic cubic curve.
|
|
270
|
-
- `[ortho]` emits axis-aligned 90-degree trace segments.
|
|
271
|
-
- `[arrow]` and `[dot]` are shorthand end markers.
|
|
272
|
-
- `marker-start=none|arrow|dot` and `marker-end=none|arrow|dot` configure each endpoint.
|
|
273
|
-
|
|
274
|
-
Routes begin and end at the exact resolved physical port coordinates. Orthogonal routing expands
|
|
275
|
-
every non-endpoint component into an Axis-Aligned Bounding Box with a **12-unit clearance**. A
|
|
276
|
-
deterministic segment splitter compares bounded detours and routes around intersected boxes. It does
|
|
277
|
-
not run a general graph-search library.
|
|
278
|
-
|
|
279
|
-
After all routes are known, the document router indexes orthogonal segments in a fixed spatial grid.
|
|
280
|
-
When unrelated horizontal and vertical traces cross without sharing a port or junction, the trace
|
|
281
|
-
that appears later in source order receives a **5-unit SVG elliptical arc** bridge. True shared
|
|
282
|
-
endpoints remain junctions and never receive a bridge. Control points, obstacle detours, bridge
|
|
283
|
-
extrema, and physical endpoints all participate in static bounds validation.
|
|
284
|
-
|
|
285
|
-
### Color and theme grammar
|
|
286
|
-
|
|
287
|
-
The color field accepts:
|
|
288
|
-
|
|
289
|
-
- semantic tokens with or without `#`: `amber`, `#blue`, `cyan`, `purple`, `slate`, `emerald`;
|
|
290
|
-
- strict hex literals: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`;
|
|
291
|
-
- validated legacy and modern `rgb()` / `rgba()` values;
|
|
292
|
-
- validated legacy and modern `hsl()` / `hsla()` values;
|
|
293
|
-
- safe custom aliases such as `#phosphor`, `quantum-optical`, or `brand-vector`.
|
|
294
|
-
|
|
295
|
-
Known tokens emit classes such as `.schematic-token--amber`. Custom aliases emit sanitized classes
|
|
296
|
-
and resolve through `--schematic-color-<alias>`, then `--schematic-vector-fallback`, then
|
|
297
|
-
`currentColor`. Validated CSS literals are assigned only to the compiler-owned
|
|
298
|
-
`--schematic-vector` property. Semicolons, `url()`, `var()`, injected attributes, invalid channels,
|
|
299
|
-
and values outside the documented grammar are rejected.
|
|
300
|
-
|
|
301
|
-
```css
|
|
302
|
-
.schemd-host {
|
|
303
|
-
--schematic-vector-fallback: currentColor;
|
|
304
|
-
--schematic-color-phosphor: oklch(88% 0.24 145deg);
|
|
305
|
-
--schematic-color-quantum-optical: oklch(76% 0.2 300deg);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
.schemd-host .schematic-token--amber {
|
|
309
|
-
--schematic-vector: oklch(72% 0.17 65deg);
|
|
310
|
-
}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### Micro-math labels
|
|
314
|
-
|
|
315
|
-
Labels support a deliberately bounded notation subset. It is linear-time and produces native SVG
|
|
316
|
-
text runs.
|
|
317
|
-
|
|
318
|
-
| Input | Output meaning |
|
|
319
|
-
| -------------------------------------------- | ----------------------- |
|
|
320
|
-
| `V_{in}` or `V_i` | subscript |
|
|
321
|
-
| `x^{2}` or `x^2` | superscript |
|
|
322
|
-
| `\alpha`, `\beta`, `\Delta` | `α`, `β`, `Δ` |
|
|
323
|
-
| `\lambda`, `\mu`, `\sigma`, `\theta`, `\phi` | `λ`, `μ`, `σ`, `θ`, `φ` |
|
|
324
|
-
| `\pi`, `\omega`, `\Omega` | `π`, `ω`, `Ω` |
|
|
325
|
-
| `\cdot`, `\times`, `\pm` | `·`, `×`, `±` |
|
|
326
|
-
| `\le`, `\ge`, `\neq` | `≤`, `≥`, `≠` |
|
|
327
|
-
| `\rightarrow`, `\sqrt`, `\infty` | `→`, `√`, `∞` |
|
|
328
|
-
|
|
329
|
-
```schemd
|
|
330
|
-
port:VIN "V_{in} = V_0 \cdot e^{-t/\tau}" at (100, 100) #blue
|
|
331
|
-
resistor:R1 "R_{load} = 10 k\Omega" at (360, 100) #amber
|
|
332
|
-
qgate:RZ "R_Z^{\pi/4}" at (620, 100) #purple [phase="π/4"]
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
Subscripts use a 70% `<tspan>` with a positive baseline shift; superscripts use a negative shift.
|
|
336
|
-
Each shifted segment emits an explicit inverse shift so subsequent text returns to the original
|
|
337
|
-
baseline. Unknown backslash commands remain literal text. Unmatched braces are treated as grouping
|
|
338
|
-
text, not executable syntax. Use surrounding prose, KaTeX, MathML, or another document-level system
|
|
339
|
-
for fractions, stacked matrices, semantic equations, or line breaking.
|
|
340
|
-
|
|
341
|
-
## Compilation modes
|
|
342
|
-
|
|
343
|
-
The output mode is a capability and payload budget, not a visual-quality switch:
|
|
344
|
-
|
|
345
|
-
| Mode | Embedded CSS, grid, transitions, glow | Interaction `data-*`, focus targets, port hotspots | Best fit |
|
|
346
|
-
| -------------- | ------------------------------------- | -------------------------------------------------- | ----------------------------------------------------- |
|
|
347
|
-
| `default` | No | No | Articles, static sites, PDFs, email/image conversion |
|
|
348
|
-
| `embedded-css` | Yes | No | Responsive themed visuals with zero client JavaScript |
|
|
349
|
-
| `full` | Yes | Yes | Diagnostics, simulations, tooltips, logic probes |
|
|
350
|
-
|
|
351
|
-
All modes retain intrinsic dimensions, accessible title/description data, semantic color classes,
|
|
352
|
-
atomic SVG symbols, and a figure caption. `default` and `embedded-css` compound compatible connection
|
|
353
|
-
paths. `full` preserves one trace per connection because each owns distinct source and target data.
|
|
354
|
-
|
|
355
|
-
Full mode exposes:
|
|
356
|
-
|
|
357
|
-
```html
|
|
358
|
-
<g
|
|
359
|
-
class="schematic-component"
|
|
360
|
-
data-node-id="R1"
|
|
361
|
-
data-node-kind="resistor"
|
|
362
|
-
data-node-label="10 kΩ"
|
|
363
|
-
tabindex="0"
|
|
364
|
-
>
|
|
365
|
-
<!-- component vector and port hotspots -->
|
|
366
|
-
</g>
|
|
367
|
-
<g class="schematic-wire" data-wire-source="R1.out" data-wire-target="C1.in" tabindex="0">
|
|
368
|
-
<!-- trace vector and opt-in glow copy -->
|
|
369
|
-
</g>
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
Port hotspots expose `data-port-id` and `data-parent-node`. Visual states are standardized as
|
|
373
|
-
`.is-hovered`, `.is-active`, `.is-selected`, and `.is-degraded`. Embedded transitions honor
|
|
374
|
-
`prefers-reduced-motion: reduce`.
|
|
375
|
-
|
|
376
|
-
## Public API
|
|
48
|
+
## Use the compiler directly
|
|
377
49
|
|
|
378
50
|
```ts
|
|
379
51
|
import {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
renderSchematic,
|
|
385
|
-
routeConnection,
|
|
386
|
-
routeConnections,
|
|
387
|
-
schematicMarkedExtension,
|
|
388
|
-
SCHEMATIC_LIMITS,
|
|
389
|
-
SCHEMD_OUTPUT_MODES,
|
|
390
|
-
SchematicSyntaxError,
|
|
391
|
-
type SchematicDocument,
|
|
392
|
-
type SchemdOutputMode
|
|
393
|
-
} from '@schemd/core';
|
|
394
|
-
```
|
|
52
|
+
parseSchematic,
|
|
53
|
+
parseSchematicFence,
|
|
54
|
+
renderSchematic,
|
|
55
|
+
} from "@schemd/core";
|
|
395
56
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
- `parseSchematicColor(source, line)` validates semantic, CSS, or alias color input.
|
|
399
|
-
- `renderSchematic(document, options)` generates escaped accessible SVG.
|
|
400
|
-
- `schematicMarkedExtension(options?)` compiles canonical `schemd` fences through Marked.
|
|
401
|
-
- `parseMathLabel`, `mathLabelText`, `mathLabelGlyphLength`, and `renderMathLabelTspans` expose the
|
|
402
|
-
bounded label subsystem.
|
|
403
|
-
- `resolvePortPoint`, `positionIcPin`, `enumerateComponentPorts`, `componentTextAnchors`,
|
|
404
|
-
`componentRectangle`, and `componentObstacleRectangle` expose deterministic layout data.
|
|
405
|
-
- `routeConnection` resolves one route; `routeConnections` applies document-level bridge crossings.
|
|
406
|
-
- `validateDocumentGeometry` verifies every component and route against the intrinsic viewBox.
|
|
407
|
-
- `SCHEMATIC_LIMITS`, the `MAX_SCHEMATIC_*` constants, `SCHEMD_OUTPUT_MODES`, component-kind domains,
|
|
408
|
-
marker domains, and strict AST/layout types expose the exact compiler contract.
|
|
409
|
-
|
|
410
|
-
## Marked integration
|
|
411
|
-
|
|
412
|
-
```ts
|
|
413
|
-
// Server-only module
|
|
414
|
-
import { Marked } from 'marked';
|
|
415
|
-
import { schematicMarkedExtension } from '@schemd/core';
|
|
416
|
-
|
|
417
|
-
function escapeHtml(value: string): string {
|
|
418
|
-
return value
|
|
419
|
-
.replaceAll('&', '&')
|
|
420
|
-
.replaceAll('<', '<')
|
|
421
|
-
.replaceAll('>', '>')
|
|
422
|
-
.replaceAll('"', '"')
|
|
423
|
-
.replaceAll("'", ''');
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const markdown = new Marked();
|
|
427
|
-
markdown.use(
|
|
428
|
-
schematicMarkedExtension({
|
|
429
|
-
mode: 'default',
|
|
430
|
-
onError(error, source) {
|
|
431
|
-
console.error('schemd compilation failed', { message: error.message, line: error.line });
|
|
432
|
-
return `<pre><code class="language-schemd">${escapeHtml(source)}</code></pre>`;
|
|
433
|
-
}
|
|
434
|
-
})
|
|
57
|
+
const fence = parseSchematicFence(
|
|
58
|
+
'schemd bounds="640x260" title="Sensor input"',
|
|
435
59
|
);
|
|
436
60
|
|
|
437
|
-
|
|
438
|
-
return await markdown.parse(source);
|
|
439
|
-
}
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
The built-in error renderer emits escaped source and an accessible diagnostic. A custom `onError`
|
|
443
|
-
handler is a trusted server boundary: escape any source it returns. Unrelated code fences return
|
|
444
|
-
`false` and continue through the host renderer unchanged.
|
|
445
|
-
|
|
446
|
-
## Framework integration
|
|
447
|
-
|
|
448
|
-
Compile once on the server, then pass the resulting SVG string as trusted data. Full-mode examples
|
|
449
|
-
attach one delegated listener to the host rather than one listener per SVG node.
|
|
450
|
-
|
|
451
|
-
### React
|
|
452
|
-
|
|
453
|
-
```tsx
|
|
454
|
-
import { useEffect, useRef } from 'react';
|
|
455
|
-
|
|
456
|
-
export function SchemdDiagram({ svg }: { readonly svg: string }) {
|
|
457
|
-
const host = useRef<HTMLDivElement>(null);
|
|
458
|
-
|
|
459
|
-
useEffect(() => {
|
|
460
|
-
const root = host.current;
|
|
461
|
-
if (!root) return;
|
|
462
|
-
|
|
463
|
-
const select = (event: Event) => {
|
|
464
|
-
const target =
|
|
465
|
-
event.target instanceof Element
|
|
466
|
-
? event.target.closest<SVGGElement>('[data-node-id]')
|
|
467
|
-
: null;
|
|
468
|
-
if (!target) return;
|
|
469
|
-
root.querySelector('.is-selected')?.classList.remove('is-selected');
|
|
470
|
-
target.classList.add('is-selected');
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
root.addEventListener('click', select);
|
|
474
|
-
return () => root.removeEventListener('click', select);
|
|
475
|
-
}, [svg]);
|
|
476
|
-
|
|
477
|
-
return <div ref={host} dangerouslySetInnerHTML={{ __html: svg }} />;
|
|
478
|
-
}
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
### Vue 3
|
|
482
|
-
|
|
483
|
-
```vue
|
|
484
|
-
<script setup lang="ts">
|
|
485
|
-
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
61
|
+
if (!fence) throw new Error("Expected a schemd fence");
|
|
486
62
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
host.value.querySelector('.is-selected')?.classList.remove('is-selected');
|
|
494
|
-
target.classList.add('is-selected');
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
onMounted(() => host.value?.addEventListener('click', select));
|
|
498
|
-
onBeforeUnmount(() => host.value?.removeEventListener('click', select));
|
|
499
|
-
</script>
|
|
63
|
+
const diagram = parseSchematic(
|
|
64
|
+
`port:VIN "Input" at (60, 130) #blue
|
|
65
|
+
resistor:R1 "10 k\\Omega" at (220, 130) #amber
|
|
66
|
+
VIN.out -> R1.in #blue [ortho]`,
|
|
67
|
+
fence,
|
|
68
|
+
);
|
|
500
69
|
|
|
501
|
-
|
|
70
|
+
const svg = renderSchematic(diagram, fence);
|
|
502
71
|
```
|
|
503
72
|
|
|
504
|
-
|
|
73
|
+
## The language
|
|
505
74
|
|
|
506
|
-
|
|
507
|
-
<script lang="ts">
|
|
508
|
-
let { svg }: { svg: string } = $props();
|
|
509
|
-
let host: HTMLDivElement;
|
|
75
|
+
A diagram has component declarations and connections:
|
|
510
76
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
event.target instanceof Element ? event.target.closest('[data-node-id]') : null;
|
|
515
|
-
if (!target) return;
|
|
516
|
-
host.querySelector('.is-selected')?.classList.remove('is-selected');
|
|
517
|
-
target.classList.add('is-selected');
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
host.addEventListener('click', select);
|
|
521
|
-
return () => host.removeEventListener('click', select);
|
|
522
|
-
});
|
|
523
|
-
</script>
|
|
524
|
-
|
|
525
|
-
<div bind:this={host}>{@html svg}</div>
|
|
77
|
+
```text
|
|
78
|
+
kind:ID "label" at (x, y) color [options]
|
|
79
|
+
ID.port -> ID.port color [line|bezier|ortho]
|
|
526
80
|
```
|
|
527
81
|
|
|
528
|
-
|
|
82
|
+
Available components include resistors, capacitors, inductors, diodes, transistors, grounds, ports, logic gates, quantum gates, and configurable IC blocks.
|
|
529
83
|
|
|
530
|
-
|
|
531
|
-
from your own server-side `@schemd/core` compiler boundary.
|
|
84
|
+
Use `line`, `bezier`, or `ortho` to choose a connection style. Arrow and dot markers are also supported:
|
|
532
85
|
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
536
|
-
|
|
537
|
-
@Component({
|
|
538
|
-
selector: 'app-schemd-diagram',
|
|
539
|
-
template: '<div #host [innerHTML]="trustedSvg" (click)="select($event)"></div>'
|
|
540
|
-
})
|
|
541
|
-
export class SchemdDiagramComponent implements OnChanges {
|
|
542
|
-
@Input({ required: true }) svg = '';
|
|
543
|
-
@ViewChild('host', { static: true }) host!: ElementRef<HTMLDivElement>;
|
|
544
|
-
trustedSvg!: SafeHtml;
|
|
545
|
-
|
|
546
|
-
constructor(private readonly sanitizer: DomSanitizer) {}
|
|
547
|
-
|
|
548
|
-
ngOnChanges(): void {
|
|
549
|
-
this.trustedSvg = this.sanitizer.bypassSecurityTrustHtml(this.svg);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
select(event: MouseEvent): void {
|
|
553
|
-
const target = event.target instanceof Element ? event.target.closest('[data-node-id]') : null;
|
|
554
|
-
if (!target) return;
|
|
555
|
-
this.host.nativeElement.querySelector('.is-selected')?.classList.remove('is-selected');
|
|
556
|
-
target.classList.add('is-selected');
|
|
557
|
-
}
|
|
558
|
-
}
|
|
86
|
+
```text
|
|
87
|
+
U1.out -> U2.in #emerald [ortho marker-end=arrow]
|
|
559
88
|
```
|
|
560
89
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
`@schemd/core` owns the DSL and SVG contracts, not Markdown tokenization. Adapters should intercept only
|
|
564
|
-
the canonical `schemd` fence and delegate every other token to the host parser.
|
|
90
|
+
See the [full documentation](https://johnowolabiidogun.dev/tools/schemd/docs/overview) for component ports, options, colors, math labels, and framework integrations.
|
|
565
91
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
```ts
|
|
569
|
-
import MarkdownIt from 'markdown-it';
|
|
570
|
-
import { parseSchematic, parseSchematicFence, renderSchematic } from '@schemd/core';
|
|
571
|
-
|
|
572
|
-
export function createMarkdown(): MarkdownIt {
|
|
573
|
-
const markdown = new MarkdownIt({ html: true });
|
|
574
|
-
const fallback = markdown.renderer.rules.fence;
|
|
575
|
-
let diagramIndex = 0;
|
|
576
|
-
|
|
577
|
-
markdown.renderer.rules.fence = (tokens, index, options, environment, renderer) => {
|
|
578
|
-
const token = tokens[index]!;
|
|
579
|
-
const info = token.info.trim();
|
|
580
|
-
if (!/^schemd(?:\s|$)/i.test(info)) {
|
|
581
|
-
return fallback
|
|
582
|
-
? fallback(tokens, index, options, environment, renderer)
|
|
583
|
-
: renderer.renderToken(tokens, index, options);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const fence = parseSchematicFence(info);
|
|
587
|
-
if (!fence) return '';
|
|
588
|
-
const document = parseSchematic(token.content, fence);
|
|
589
|
-
diagramIndex += 1;
|
|
590
|
-
return renderSchematic(document, {
|
|
591
|
-
...fence,
|
|
592
|
-
idPrefix: `schemd-${diagramIndex}`,
|
|
593
|
-
mode: 'default'
|
|
594
|
-
});
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
return markdown;
|
|
598
|
-
}
|
|
599
|
-
```
|
|
92
|
+
## Output modes
|
|
600
93
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
94
|
+
| Mode | Use it for |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `default` | Small, static SVG |
|
|
97
|
+
| `embedded-css` | Built-in styles and responsive visuals |
|
|
98
|
+
| `full` | Interactive diagrams with node and wire metadata |
|
|
604
99
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
This transformer is intentionally visitor-free. It walks MDAST, replaces only `code` nodes whose
|
|
608
|
-
language is `schemd`, and creates trusted `html` nodes. Configure `remark-rehype` and the final HTML
|
|
609
|
-
serializer to preserve trusted raw HTML according to your pipeline's security model.
|
|
100
|
+
Pass a mode to `renderSchematic` or `schematicMarkedExtension`:
|
|
610
101
|
|
|
611
102
|
```ts
|
|
612
|
-
|
|
613
|
-
import { parseSchematic, parseSchematicFence, renderSchematic } from '@schemd/core';
|
|
614
|
-
|
|
615
|
-
function isParent(node: RootContent | Root): node is Parent {
|
|
616
|
-
return 'children' in node && Array.isArray(node.children);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
export function remarkSchemd() {
|
|
620
|
-
return (tree: Root): void => {
|
|
621
|
-
let diagramIndex = 0;
|
|
622
|
-
|
|
623
|
-
const transform = (parent: Parent): void => {
|
|
624
|
-
parent.children = parent.children.map((node) => {
|
|
625
|
-
if (node.type === 'code' && node.lang?.toLowerCase() === 'schemd') {
|
|
626
|
-
const code = node as Code;
|
|
627
|
-
const info = `schemd${code.meta ? ` ${code.meta}` : ''}`;
|
|
628
|
-
const fence = parseSchematicFence(info);
|
|
629
|
-
if (!fence) return node;
|
|
630
|
-
const document = parseSchematic(code.value, fence);
|
|
631
|
-
diagramIndex += 1;
|
|
632
|
-
return {
|
|
633
|
-
type: 'html',
|
|
634
|
-
value: renderSchematic(document, {
|
|
635
|
-
...fence,
|
|
636
|
-
idPrefix: `schemd-${diagramIndex}`,
|
|
637
|
-
mode: 'default'
|
|
638
|
-
})
|
|
639
|
-
} satisfies Html;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
if (isParent(node)) transform(node);
|
|
643
|
-
return node;
|
|
644
|
-
});
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
transform(tree);
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
## Responsive rendering
|
|
653
|
-
|
|
654
|
-
The SVG already owns intrinsic dimensions and a `viewBox`. Let CSS scale its box; do not mutate
|
|
655
|
-
coordinates after compilation.
|
|
656
|
-
|
|
657
|
-
```css
|
|
658
|
-
.schemd-container {
|
|
659
|
-
container-type: inline-size;
|
|
660
|
-
inline-size: 100%;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
.schemd-container .schematic-frame,
|
|
664
|
-
.schemd-container .schematic-svg {
|
|
665
|
-
display: block;
|
|
666
|
-
inline-size: 100%;
|
|
667
|
-
max-inline-size: 100%;
|
|
668
|
-
block-size: auto;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
@container (inline-size < 36rem) {
|
|
672
|
-
.schemd-container .schematic-label,
|
|
673
|
-
.schemd-container .schematic-designator {
|
|
674
|
-
font-size: 0.9em;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
103
|
+
const svg = renderSchematic(diagram, { ...fence, mode: "embedded-css" });
|
|
677
104
|
```
|
|
678
105
|
|
|
679
|
-
|
|
680
|
-
before CSS loads and keep server-rendered pages at zero CLS.
|
|
681
|
-
|
|
682
|
-
## Diagnostics and observability
|
|
683
|
-
|
|
684
|
-
Syntax failures throw `SchematicSyntaxError`. When available, `error.line` identifies the DSL line.
|
|
685
|
-
Log bounded metadata—not entire potentially sensitive source—in production:
|
|
686
|
-
|
|
687
|
-
```ts
|
|
688
|
-
import { SchematicSyntaxError } from 'schemd';
|
|
689
|
-
|
|
690
|
-
try {
|
|
691
|
-
// parse and render
|
|
692
|
-
} catch (error) {
|
|
693
|
-
if (error instanceof SchematicSyntaxError) {
|
|
694
|
-
logger.warn('schemd.compile_rejected', {
|
|
695
|
-
line: error.line,
|
|
696
|
-
message: error.message,
|
|
697
|
-
sourceCharacters: source.length
|
|
698
|
-
});
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
throw error;
|
|
702
|
-
}
|
|
703
|
-
```
|
|
106
|
+
## Security
|
|
704
107
|
|
|
705
|
-
|
|
706
|
-
out-of-range bounds, invalid options, resource exhaustion, and geometry overflow all fail closed.
|
|
108
|
+
Schemd validates input, escapes labels, limits diagram size, and returns bounded SVG. Compile diagrams on a trusted server or build system, then send the generated SVG to the browser. Do not treat arbitrary HTML as Schemd output.
|
|
707
109
|
|
|
708
|
-
## Development
|
|
110
|
+
## Development
|
|
709
111
|
|
|
710
112
|
```sh
|
|
113
|
+
npm install
|
|
711
114
|
npm run check
|
|
712
|
-
npm run build
|
|
713
115
|
npm test
|
|
714
|
-
npm run
|
|
715
|
-
npm pack --dry-run --json
|
|
116
|
+
npm run build
|
|
716
117
|
```
|
|
717
118
|
|
|
718
|
-
|
|
719
|
-
fallback. Vitest coverage gates are 100% for statements, branches, functions, and lines. The
|
|
720
|
-
published artifact contains stripped ESM, documented declarations, this README, package metadata,
|
|
721
|
-
and the MIT license. Source maps are intentionally excluded; `.d.ts` files retain public TSDoc for
|
|
722
|
-
first-class editor support.
|
|
723
|
-
|
|
724
|
-
## License
|
|
725
|
-
|
|
726
|
-
MIT
|
|
119
|
+
Schemd is available under the [MIT License](https://github.com/Sirneij/schemd/blob/main/LICENSE).
|
package/package.json
CHANGED