@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.
Files changed (2) hide show
  1. package/README.md +64 -671
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,725 +2,118 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@schemd/core.svg?style=flat-square)](https://www.npmjs.com/package/@schemd/core)
4
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
5
  [![license](https://img.shields.io/npm/l/@schemd/core.svg?style=flat-square)](https://github.com/Sirneij/schemd/blob/main/LICENSE)
8
6
 
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
7
+ Write engineering diagrams as text and compile them to accessible inline SVG.
38
8
 
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
- );
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
- const mode: SchemdOutputMode = 'default';
69
- const trustedSvg = renderSchematic(document, { ...fence, mode });
70
- ```
11
+ ## Why Schemd?
71
12
 
72
- ## Compiler architecture
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
- `@schemd/core` is a staged compiler, not a browser drawing widget:
19
+ ## Install
75
20
 
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:
21
+ Schemd requires Node.js 24 or newer. Install `marked` if you want to use Markdown fences.
124
22
 
125
23
  ```sh
126
- npm run build
127
- npm pack --dry-run --json
128
- wc -c dist/*.js
24
+ npm install @schemd/core marked
129
25
  ```
130
26
 
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.
27
+ ## Use with Markdown
160
28
 
161
- ## Markdown fence
162
-
163
- The canonical language identifier is `schemd`:
29
+ ```ts
30
+ import { Marked } from "marked";
31
+ import { schematicMarkedExtension } from "@schemd/core";
164
32
 
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]
33
+ const markdown = new Marked();
34
+ markdown.use(schematicMarkedExtension());
174
35
 
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"]
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
- INPUT.out -> R1.in #slate [line]
42
+ VIN.out -> R1.in #blue [ortho]
181
43
  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]
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
- ## 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
48
+ ## Use the compiler directly
377
49
 
378
50
  ```ts
379
51
  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
- ```
52
+ parseSchematic,
53
+ parseSchematicFence,
54
+ renderSchematic,
55
+ } from "@schemd/core";
395
56
 
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
- })
57
+ const fence = parseSchematicFence(
58
+ 'schemd bounds="640x260" title="Sensor input"',
435
59
  );
436
60
 
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';
61
+ if (!fence) throw new Error("Expected a schemd fence");
486
62
 
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>
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
- <template><div ref="host" v-html="svg" /></template>
70
+ const svg = renderSchematic(diagram, fence);
502
71
  ```
503
72
 
504
- ### Svelte 5
73
+ ## The language
505
74
 
506
- ```svelte
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
- $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>
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
- ### Angular
82
+ Available components include resistors, capacitors, inductors, diodes, transistors, grounds, ports, logic gates, quantum gates, and configurable IC blocks.
529
83
 
530
- Angular sanitizes ordinary HTML bindings. Bypass sanitization only for a string returned directly
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
- ```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
- }
86
+ ```text
87
+ U1.out -> U2.in #emerald [ortho marker-end=arrow]
559
88
  ```
560
89
 
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.
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
- ### 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
- ```
92
+ ## Output modes
600
93
 
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.
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
- ### 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.
100
+ Pass a mode to `renderSchematic` or `schematicMarkedExtension`:
610
101
 
611
102
  ```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
- }
103
+ const svg = renderSchematic(diagram, { ...fence, mode: "embedded-css" });
677
104
  ```
678
105
 
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
- ```
106
+ ## Security
704
107
 
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.
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 and release
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 test:coverage
715
- npm pack --dry-run --json
116
+ npm run build
716
117
  ```
717
118
 
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
119
+ Schemd is available under the [MIT License](https://github.com/Sirneij/schemd/blob/main/LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schemd/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Strict, bounded, server-side engineering schematic DSL compiler for Marked.",
5
5
  "homepage": "https://johnowolabiidogun.dev/tools/schemd/docs/overview",
6
6
  "bugs": {