@schemd/core 0.1.0
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/LICENSE +21 -0
- package/README.md +726 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +7 -0
- package/dist/layout.d.ts +151 -0
- package/dist/layout.js +592 -0
- package/dist/limits.d.ts +31 -0
- package/dist/limits.js +25 -0
- package/dist/marked-extension.d.ts +23 -0
- package/dist/marked-extension.js +75 -0
- package/dist/math-label.d.ts +50 -0
- package/dist/math-label.js +151 -0
- package/dist/parser.d.ts +56 -0
- package/dist/parser.js +598 -0
- package/dist/renderer.d.ts +44 -0
- package/dist/renderer.js +485 -0
- package/dist/types.d.ts +248 -0
- package/dist/types.js +25 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
# @schemd/core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@schemd/core)
|
|
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
|
+
[](https://github.com/Sirneij/schemd/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
**Compile bounded engineering diagrams into accessible inline SVG—without a browser runtime, DOM,
|
|
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
|
|
38
|
+
|
|
39
|
+
```sh
|
|
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
|
+
);
|
|
67
|
+
|
|
68
|
+
const mode: SchemdOutputMode = 'default';
|
|
69
|
+
const trustedSvg = renderSchematic(document, { ...fence, mode });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Compiler architecture
|
|
73
|
+
|
|
74
|
+
`@schemd/core` is a staged compiler, not a browser drawing widget:
|
|
75
|
+
|
|
76
|
+
```text
|
|
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:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
npm run build
|
|
127
|
+
npm pack --dry-run --json
|
|
128
|
+
wc -c dist/*.js
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Generated payload size is topology-dependent. `@schemd/core` enforces a 2,097,152-byte compiled SVG cap,
|
|
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.
|
|
160
|
+
|
|
161
|
+
## Markdown fence
|
|
162
|
+
|
|
163
|
+
The canonical language identifier is `schemd`:
|
|
164
|
+
|
|
165
|
+
````markdown
|
|
166
|
+
```schemd bounds="960x560" title="Mixed-signal flight-control processor"
|
|
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]
|
|
174
|
+
|
|
175
|
+
nand:G1 "Voting logic" at (210, 360) #cyan [inputs=3 outputs=2 standard=ieee]
|
|
176
|
+
ic:U1 "Mux" at (480, 400) #blue [left="S0,S1,EN" right="Y0,Y1" top="VCC" bottom="GND"]
|
|
177
|
+
hadamard:H1 "H|0〉 = |+〉" at (690, 390) #purple
|
|
178
|
+
qgate:RZ1 "R_Z^{\pi/4}" at (850, 390) #quantum-optical [phase="π/4"]
|
|
179
|
+
|
|
180
|
+
INPUT.out -> R1.in #slate [line]
|
|
181
|
+
R1.out -> C1.in #amber [ortho]
|
|
182
|
+
C1.out -> L1.in #blue [bezier]
|
|
183
|
+
L1.out -> D1.anode #cyan
|
|
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]
|
|
189
|
+
```
|
|
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
|
+
|
|
196
|
+
## DSL grammar
|
|
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
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import {
|
|
380
|
+
parseSchematic,
|
|
381
|
+
parseSchematicColor,
|
|
382
|
+
parseSchematicFence,
|
|
383
|
+
renderMathLabelTspans,
|
|
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
|
+
```
|
|
395
|
+
|
|
396
|
+
- `parseSchematicFence(info, defaultTitle?)` validates fence metadata.
|
|
397
|
+
- `parseSchematic(source, fence)` returns a frozen, renderer-authorized AST.
|
|
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
|
+
})
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
export async function compileDocument(source: string): Promise<string> {
|
|
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';
|
|
486
|
+
|
|
487
|
+
defineProps<{ svg: string }>();
|
|
488
|
+
const host = ref<HTMLDivElement | null>(null);
|
|
489
|
+
|
|
490
|
+
const select = (event: Event) => {
|
|
491
|
+
const target = event.target instanceof Element ? event.target.closest('[data-node-id]') : null;
|
|
492
|
+
if (!target || !host.value) return;
|
|
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>
|
|
500
|
+
|
|
501
|
+
<template><div ref="host" v-html="svg" /></template>
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Svelte 5
|
|
505
|
+
|
|
506
|
+
```svelte
|
|
507
|
+
<script lang="ts">
|
|
508
|
+
let { svg }: { svg: string } = $props();
|
|
509
|
+
let host: HTMLDivElement;
|
|
510
|
+
|
|
511
|
+
$effect(() => {
|
|
512
|
+
const select = (event: Event) => {
|
|
513
|
+
const target =
|
|
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>
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Angular
|
|
529
|
+
|
|
530
|
+
Angular sanitizes ordinary HTML bindings. Bypass sanitization only for a string returned directly
|
|
531
|
+
from your own server-side `@schemd/core` compiler boundary.
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
import { Component, ElementRef, Input, OnChanges, ViewChild } from '@angular/core';
|
|
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
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
## Alternative Markdown parsers
|
|
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.
|
|
565
|
+
|
|
566
|
+
### markdown-it
|
|
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
|
+
```
|
|
600
|
+
|
|
601
|
+
Reset `diagramIndex` at your document boundary when a single parser instance compiles multiple
|
|
602
|
+
documents. Apply the exported source/component/connection/output limits cumulatively if untrusted
|
|
603
|
+
documents may contain multiple schemd fences; the built-in Marked extension already does this.
|
|
604
|
+
|
|
605
|
+
### remark / rehype
|
|
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.
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
import type { Code, Html, Parent, Root, RootContent } from 'mdast';
|
|
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
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Do not remove the generated `width`, `height`, or `viewBox`; together they reserve aspect ratio
|
|
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
|
+
```
|
|
704
|
+
|
|
705
|
+
Malformed declarations, duplicate IDs, duplicate IC pins, unknown endpoints, unsafe colors,
|
|
706
|
+
out-of-range bounds, invalid options, resource exhaustion, and geometry overflow all fail closed.
|
|
707
|
+
|
|
708
|
+
## Development and release
|
|
709
|
+
|
|
710
|
+
```sh
|
|
711
|
+
npm run check
|
|
712
|
+
npm run build
|
|
713
|
+
npm test
|
|
714
|
+
npm run test:coverage
|
|
715
|
+
npm pack --dry-run --json
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
TypeScript uses `strict`, `noUncheckedIndexedAccess`, and `exactOptionalPropertyTypes` with no `any`
|
|
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
|