@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/README.md ADDED
@@ -0,0 +1,726 @@
1
+ # @schemd/core
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@schemd/core.svg?style=flat-square)](https://www.npmjs.com/package/@schemd/core)
4
+ [![CI status](https://github.com/Sirneij/schemd/actions/workflows/ci.yml/badge.svg?style=flat-square)](https://github.com/Sirneij/schemd/actions/workflows/ci.yml)
5
+ [![bundle size](https://deno.bundlejs.com/badge?q=@schemd/core@0.1.2&style=flat-square)](https://bundlejs.com/?q=@schemd/core)
6
+ [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square)](https://github.com/Sirneij/schemd/actions/workflows/ci.yml)
7
+ [![license](https://img.shields.io/npm/l/@schemd/core.svg?style=flat-square)](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('&', '&amp;')
420
+ .replaceAll('<', '&lt;')
421
+ .replaceAll('>', '&gt;')
422
+ .replaceAll('"', '&quot;')
423
+ .replaceAll("'", '&#39;');
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