@schemd/core 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -8
- package/assets/brand/README.md +6 -0
- package/assets/brand/schemd-logo.svg +29 -0
- package/assets/brand/schemd-mark.svg +23 -0
- package/dist/compiler.d.ts +16 -0
- package/dist/compiler.js +17 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +2 -2
- package/dist/layout.d.ts +1 -1
- package/dist/layout.js +30 -9
- package/dist/limits.d.ts +1 -3
- package/dist/math-label.js +1 -22
- package/dist/parser.js +8 -2
- package/dist/renderer.js +43 -38
- package/dist/route-cache.d.ts +6 -0
- package/dist/route-cache.js +10 -0
- package/dist/types.d.ts +6 -11
- package/dist/types.js +1 -0
- package/dist/xml.d.ts +6 -0
- package/dist/xml.js +38 -0
- package/package.json +11 -17
- package/dist/marked-extension.d.ts +0 -23
- package/dist/marked-extension.js +0 -75
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/brand/schemd-logo.svg" alt="Schemd — engineering vectors" width="640" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# @schemd/core
|
|
2
6
|
|
|
3
7
|
Write schematics and UML as text. Get accessible, deterministic SVG.
|
|
@@ -7,21 +11,21 @@ Schemd has no runtime dependencies, browser layout pass, or DOM requirement. It
|
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```sh
|
|
10
|
-
npm
|
|
14
|
+
npm i @schemd/core # bun i @schemd/core
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
Node.js 24+ is required.
|
|
17
|
+
Node.js 24+ is required.
|
|
14
18
|
|
|
15
19
|
## Quick start
|
|
16
20
|
|
|
17
21
|
```ts
|
|
18
|
-
import {
|
|
22
|
+
import { compileSchematic, parseSchematicFence } from "@schemd/core";
|
|
19
23
|
|
|
20
24
|
const fence = parseSchematicFence(
|
|
21
25
|
'schemd bounds="640x260" title="Sensor input"',
|
|
22
26
|
)!;
|
|
23
27
|
|
|
24
|
-
const
|
|
28
|
+
const { svg, document, metrics } = compileSchematic(`
|
|
25
29
|
port:VIN "Input" at (60, 130) #blue
|
|
26
30
|
resistor:R1 "10 k\\Omega" at (220, 130) #amber
|
|
27
31
|
capacitor:C1 "100 nF" at (400, 130) #cyan
|
|
@@ -29,8 +33,6 @@ capacitor:C1 "100 nF" at (400, 130) #cyan
|
|
|
29
33
|
VIN.out -> R1.in #blue [ortho]
|
|
30
34
|
R1.out -> C1.in #amber [ortho]
|
|
31
35
|
`, fence);
|
|
32
|
-
|
|
33
|
-
const html = renderSchematic(diagram, fence);
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
The DSL is intentionally small:
|
|
@@ -55,6 +57,14 @@ Class, actor, use-case, state, lifeline, note, package, initial, and final nodes
|
|
|
55
57
|
|
|
56
58
|
Use `mode: "embedded-css"` for built-in styling or `mode: "full"` for interaction metadata. The default output stays compact and static.
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
On a colored surface, set `--schematic-surface` to that background color so hollow UML markers and connector-label halos match it.
|
|
61
|
+
|
|
62
|
+
Markdown belongs at the host boundary. Detect `schemd` fences in your server-side Markdown renderer and pass their text to `compileSchematic`; core does not ship or require a Markdown parser.
|
|
63
|
+
|
|
64
|
+
## Roadmap
|
|
65
|
+
|
|
66
|
+
**Now: topology and routing** → **Next: visual precision** → **Later: language and footprint**
|
|
67
|
+
|
|
68
|
+
Known limits and contributor-sized work live in the [active roadmap](./ROADMAP.md). Pick an item, open its claim link, and agree on the approach before starting a large change. Completed items are removed after merge.
|
|
59
69
|
|
|
60
|
-
[Documentation](https://johnowolabiidogun.dev/tools/schemd/docs/overview) · [Issues](https://github.com/Sirneij/schemd/issues) · [MIT](https://github.com/Sirneij/schemd/blob/main/LICENSE)
|
|
70
|
+
[Documentation](https://johnowolabiidogun.dev/tools/schemd/docs/overview) · [Roadmap](./ROADMAP.md) · [Issues](https://github.com/Sirneij/schemd/issues) · [MIT](https://github.com/Sirneij/schemd/blob/main/LICENSE)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Schemd brand assets
|
|
2
|
+
|
|
3
|
+
- `schemd-mark.svg` is the square product mark for favicons, avatars, and compact UI.
|
|
4
|
+
- `schemd-logo.svg` is the primary horizontal lockup for dark surfaces.
|
|
5
|
+
|
|
6
|
+
Keep one terminal-node diameter of clear space around the mark. Do not recolor individual wires or stretch either asset. The core palette is canvas `#080b0b`, signal `#72dbbb`, paper `#f4f8f5`, and secondary signal `#e1a15c`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 200" role="img" aria-labelledby="title description">
|
|
2
|
+
<title id="title">Schemd</title>
|
|
3
|
+
<desc id="description">Schemd engineering vectors logo.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="surface" x1="0" y1="0" x2="720" y2="200" gradientUnits="userSpaceOnUse">
|
|
6
|
+
<stop stop-color="#111817" />
|
|
7
|
+
<stop offset="1" stop-color="#080b0b" />
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<linearGradient id="signal" x1="42" y1="68" x2="158" y2="132" gradientUnits="userSpaceOnUse">
|
|
10
|
+
<stop stop-color="#9aead1" />
|
|
11
|
+
<stop offset="1" stop-color="#52c7a5" />
|
|
12
|
+
</linearGradient>
|
|
13
|
+
</defs>
|
|
14
|
+
<rect x="1" y="1" width="718" height="198" rx="28" fill="url(#surface)" stroke="#29473f" stroke-width="2" />
|
|
15
|
+
<g>
|
|
16
|
+
<rect x="24" y="24" width="152" height="152" rx="30" fill="#0c1110" stroke="#33574d" stroke-width="2" />
|
|
17
|
+
<path d="M44 72h31l17 28h16l17-28h31M44 128h31l17-28m16 0 17 28h31" fill="none" stroke="url(#signal)" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" />
|
|
18
|
+
<path d="M92 100h16" stroke="#e1a15c" stroke-width="8" stroke-linecap="round" />
|
|
19
|
+
<g fill="#72dbbb" stroke="#080b0b" stroke-width="3">
|
|
20
|
+
<circle cx="44" cy="72" r="7" />
|
|
21
|
+
<circle cx="156" cy="72" r="7" />
|
|
22
|
+
<circle cx="44" cy="128" r="7" />
|
|
23
|
+
<circle cx="156" cy="128" r="7" />
|
|
24
|
+
</g>
|
|
25
|
+
</g>
|
|
26
|
+
<text x="210" y="106" fill="#f4f8f5" font-family="Inter, 'Fira Sans', system-ui, sans-serif" font-size="72" font-weight="640" letter-spacing="-3">schemd</text>
|
|
27
|
+
<path d="M214 128h430" stroke="#29473f" stroke-width="2" />
|
|
28
|
+
<text x="212" y="154" fill="#72dbbb" font-family="'Fira Code', ui-monospace, monospace" font-size="15" font-weight="600" letter-spacing="4">ENGINEERING VECTORS</text>
|
|
29
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="title description">
|
|
2
|
+
<title id="title">Schemd mark</title>
|
|
3
|
+
<desc id="description">Four circuit terminals converge through a central vector path.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="surface" x1="12" y1="8" x2="116" y2="120" gradientUnits="userSpaceOnUse">
|
|
6
|
+
<stop stop-color="#17231f" />
|
|
7
|
+
<stop offset="1" stop-color="#080b0b" />
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<linearGradient id="signal" x1="18" y1="32" x2="110" y2="96" gradientUnits="userSpaceOnUse">
|
|
10
|
+
<stop stop-color="#9aead1" />
|
|
11
|
+
<stop offset="1" stop-color="#52c7a5" />
|
|
12
|
+
</linearGradient>
|
|
13
|
+
</defs>
|
|
14
|
+
<rect x="4" y="4" width="120" height="120" rx="24" fill="url(#surface)" stroke="#33574d" stroke-width="2" />
|
|
15
|
+
<path d="M20 40h25l13 24h12l13-24h25M20 88h25l13-24m12 0 13 24h25" fill="none" stroke="url(#signal)" stroke-width="7" stroke-linecap="round" stroke-linejoin="round" />
|
|
16
|
+
<path d="M58 64h12" stroke="#e1a15c" stroke-width="7" stroke-linecap="round" />
|
|
17
|
+
<g fill="#72dbbb" stroke="#080b0b" stroke-width="3">
|
|
18
|
+
<circle cx="20" cy="40" r="7" />
|
|
19
|
+
<circle cx="108" cy="40" r="7" />
|
|
20
|
+
<circle cx="20" cy="88" r="7" />
|
|
21
|
+
<circle cx="108" cy="88" r="7" />
|
|
22
|
+
</g>
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CompileSchematicOptions, SchematicDocument } from './types.js';
|
|
2
|
+
/** Small, allocation-bounded compilation counters. */
|
|
3
|
+
export interface SchematicCompilationMetrics {
|
|
4
|
+
readonly sourceCharacters: number;
|
|
5
|
+
readonly components: number;
|
|
6
|
+
readonly connections: number;
|
|
7
|
+
readonly svgBytes: number;
|
|
8
|
+
}
|
|
9
|
+
/** Validated AST, rendered SVG, and useful host-side counters. */
|
|
10
|
+
export interface SchematicCompilation {
|
|
11
|
+
readonly document: SchematicDocument;
|
|
12
|
+
readonly svg: string;
|
|
13
|
+
readonly metrics: SchematicCompilationMetrics;
|
|
14
|
+
}
|
|
15
|
+
/** Parse and render a schematic with one stable public call. */
|
|
16
|
+
export declare function compileSchematic(source: string, options: CompileSchematicOptions): SchematicCompilation;
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { utf8ByteLength } from './limits.js';
|
|
2
|
+
import { parseSchematic } from './parser.js';
|
|
3
|
+
import { renderSchematic } from './renderer.js';
|
|
4
|
+
export function compileSchematic(source, options) {
|
|
5
|
+
const document = parseSchematic(source, options);
|
|
6
|
+
const svg = renderSchematic(document, options);
|
|
7
|
+
return {
|
|
8
|
+
document,
|
|
9
|
+
svg,
|
|
10
|
+
metrics: {
|
|
11
|
+
sourceCharacters: source.length,
|
|
12
|
+
components: document.components.length,
|
|
13
|
+
connections: document.connections.length,
|
|
14
|
+
svgBytes: utf8ByteLength(svg)
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stable public entry point for the `schemd` server-side compiler.
|
|
3
3
|
*
|
|
4
|
-
* All runtime exports are dependency-free
|
|
5
|
-
* Marked contracts as types only, so applications pay for no Markdown runtime
|
|
6
|
-
* unless they already provide one at the host boundary.
|
|
4
|
+
* All runtime exports are dependency-free and safe to run without a DOM.
|
|
7
5
|
*
|
|
8
6
|
* @packageDocumentation
|
|
9
7
|
*/
|
|
10
8
|
export { parseSchematic, parseSchematicColor, parseSchematicFence } from './parser.js';
|
|
11
9
|
export { renderSchematic } from './renderer.js';
|
|
12
|
-
export {
|
|
10
|
+
export { compileSchematic, type SchematicCompilation, type SchematicCompilationMetrics } from './compiler.js';
|
|
13
11
|
export { mathLabelGlyphLength, mathLabelTextWidth, mathLabelText, parseMathLabel, renderMathLabelTspans, type MathLabelSegment, type MathLabelSegmentKind } from './math-label.js';
|
|
14
12
|
export { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_WIRE_CROSSINGS, MAX_SCHEMATIC_SOURCE_CHARACTERS, MAX_SCHEMATIC_SVG_OUTPUT_BYTES, SCHEMATIC_LIMITS } from './limits.js';
|
|
15
13
|
export { classicalGateHeight, componentObstacleRectangle, componentRectangle, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, positionIcPin, PORT_HOTSPOT_RADIUS, resolvePortPoint, resolvePortGeometry, routeConnections, routeConnection, SCHEMATIC_BRIDGE_RADIUS, SCHEMATIC_OBSTACLE_CLEARANCE, validateDocumentGeometry, type RoutedConnection, type ComponentTextAnchors, type ComponentPort, type IcPinSide, type SchematicRectangle } from './layout.js';
|
|
16
|
-
export { ANALOG_KINDS, COMPONENT_KINDS, CLASSICAL_GATE_KINDS, DIODE_TYPES, GROUND_STYLES, PASSIVE_KINDS, QUANTUM_GATE_KINDS, SCHEMATIC_SIGNAL_MARKERS, SEMANTIC_COLORS, TRANSISTOR_TYPES, UML_COMPONENT_KINDS, UML_RELATION_KINDS, SCHEMD_OUTPUT_MODES, SchematicSyntaxError, type AnalogKind, type CompileSchematicOptions, type ClassicalGateComponent, type ClassicalGateKind, type ComponentKind, type DiodeComponent, type DiodeType, type GroundComponent, type GroundStyle, type IcComponent, type IntegratedCircuitComponent, type IntegratedCircuitPins, type PassiveComponent, type PassiveKind, type PortComponent, type SemanticColor, type SchematicBounds, type SchematicComponent, type SchematicConnection, type SchematicColor, type SchematicDocument, type SchematicEndpoint, type SchematicFence, type SchematicSignalMarker, type
|
|
14
|
+
export { ANALOG_KINDS, COMPONENT_KINDS, CLASSICAL_GATE_KINDS, DIODE_TYPES, GROUND_STYLES, PASSIVE_KINDS, QUANTUM_GATE_KINDS, SCHEMATIC_SIGNAL_MARKERS, SEMANTIC_COLORS, TRANSISTOR_TYPES, UML_COMPONENT_KINDS, UML_RELATION_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, SchematicSyntaxError, type AnalogKind, type CompileSchematicOptions, type ClassicalGateComponent, type ClassicalGateKind, type ComponentKind, type DiodeComponent, type DiodeType, type GroundComponent, type GroundStyle, type IcComponent, type IntegratedCircuitComponent, type IntegratedCircuitPins, type PassiveComponent, type PassiveKind, type PortComponent, type SemanticColor, type SchematicBounds, type SchematicComponent, type SchematicConnection, type SchematicColor, type SchematicDocument, type SchematicEndpoint, type SchematicFence, type SchematicSignalMarker, type SchematicSemanticHook, type SchematicPoint, type SchemdOutputMode, type TransistorComponent, type TransistorType, type QuantumGateComponent, type QuantumGateKind, type SchematicRelationKind, type UmlActorComponent, type UmlClassComponent, type UmlComponent, type UmlComponentKind, type UmlPseudostateComponent, type UmlRelationKind, type UmlSizedComponent, type UmlStateComponent } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { parseSchematic, parseSchematicColor, parseSchematicFence } from './parser.js';
|
|
2
2
|
export { renderSchematic } from './renderer.js';
|
|
3
|
-
export {
|
|
3
|
+
export { compileSchematic } from './compiler.js';
|
|
4
4
|
export { mathLabelGlyphLength, mathLabelTextWidth, mathLabelText, parseMathLabel, renderMathLabelTspans } from './math-label.js';
|
|
5
5
|
export { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_WIRE_CROSSINGS, MAX_SCHEMATIC_SOURCE_CHARACTERS, MAX_SCHEMATIC_SVG_OUTPUT_BYTES, SCHEMATIC_LIMITS } from './limits.js';
|
|
6
6
|
export { classicalGateHeight, componentObstacleRectangle, componentRectangle, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, positionIcPin, PORT_HOTSPOT_RADIUS, resolvePortPoint, resolvePortGeometry, routeConnections, routeConnection, SCHEMATIC_BRIDGE_RADIUS, SCHEMATIC_OBSTACLE_CLEARANCE, validateDocumentGeometry } from './layout.js';
|
|
7
|
-
export { ANALOG_KINDS, COMPONENT_KINDS, CLASSICAL_GATE_KINDS, DIODE_TYPES, GROUND_STYLES, PASSIVE_KINDS, QUANTUM_GATE_KINDS, SCHEMATIC_SIGNAL_MARKERS, SEMANTIC_COLORS, TRANSISTOR_TYPES, UML_COMPONENT_KINDS, UML_RELATION_KINDS, SCHEMD_OUTPUT_MODES, SchematicSyntaxError } from './types.js';
|
|
7
|
+
export { ANALOG_KINDS, COMPONENT_KINDS, CLASSICAL_GATE_KINDS, DIODE_TYPES, GROUND_STYLES, PASSIVE_KINDS, QUANTUM_GATE_KINDS, SCHEMATIC_SIGNAL_MARKERS, SEMANTIC_COLORS, TRANSISTOR_TYPES, UML_COMPONENT_KINDS, UML_RELATION_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, SchematicSyntaxError } from './types.js';
|
package/dist/layout.d.ts
CHANGED
|
@@ -158,4 +158,4 @@ export declare function componentRectangle(component: SchematicComponent): Schem
|
|
|
158
158
|
* @param fence - Intrinsic canvas contract.
|
|
159
159
|
* @throws {SchematicSyntaxError} At the originating component or connection line.
|
|
160
160
|
*/
|
|
161
|
-
export declare function validateDocumentGeometry(document: SchematicDocument, fence: SchematicFence, routedConnections?: readonly RoutedConnection[]):
|
|
161
|
+
export declare function validateDocumentGeometry(document: SchematicDocument, fence: SchematicFence, routedConnections?: readonly RoutedConnection[]): readonly RoutedConnection[];
|
package/dist/layout.js
CHANGED
|
@@ -7,6 +7,7 @@ export const SCHEMATIC_BRIDGE_RADIUS = 5;
|
|
|
7
7
|
const MAX_ROUTER_STATES = 40_000;
|
|
8
8
|
const ROUTER_BEND_PENALTY = 0.25;
|
|
9
9
|
const CROSSING_BUCKET_SIZE = 64;
|
|
10
|
+
const MIN_RENDERABLE_BRIDGE_RADIUS = 0.001;
|
|
10
11
|
const DESIGNATOR_BASELINE_GAP = 10;
|
|
11
12
|
const LABEL_BASELINE_GAP = 19;
|
|
12
13
|
const TEXT_ASCENT_GUTTER = 12;
|
|
@@ -609,6 +610,19 @@ function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
|
609
610
|
}
|
|
610
611
|
return best ?? searchOrthogonalRoute(start, end, obstacles, bounds, line);
|
|
611
612
|
}
|
|
613
|
+
function endpointsFaceEachOther(from, to) {
|
|
614
|
+
const dx = to.point.x - from.point.x;
|
|
615
|
+
const dy = to.point.y - from.point.y;
|
|
616
|
+
if (dy === 0 && dx !== 0) {
|
|
617
|
+
const direction = Math.sign(dx);
|
|
618
|
+
return from.normal.x === direction && to.normal.x === -direction;
|
|
619
|
+
}
|
|
620
|
+
if (dx === 0 && dy !== 0) {
|
|
621
|
+
const direction = Math.sign(dy);
|
|
622
|
+
return from.normal.y === direction && to.normal.y === -direction;
|
|
623
|
+
}
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
612
626
|
function routeConnectionInternal(connection, components, bounds, cachedObstacles) {
|
|
613
627
|
const from = endpointGeometry(connection.from, components);
|
|
614
628
|
const to = endpointGeometry(connection.to, components);
|
|
@@ -635,6 +649,13 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
635
649
|
expanded: componentObstacleRectangle(component),
|
|
636
650
|
body: componentObstacleRectangle(component, 0)
|
|
637
651
|
}));
|
|
652
|
+
if (from.component.id !== to.component.id &&
|
|
653
|
+
endpointsFaceEachOther(from, to) &&
|
|
654
|
+
!obstacleCache.some((entry) => entry.id !== from.component.id &&
|
|
655
|
+
entry.id !== to.component.id &&
|
|
656
|
+
segmentIntersectsRectangle(start, end, entry.expanded))) {
|
|
657
|
+
return { curve: 'ortho', d: orthogonalPath([start, end]), points: [start, end] };
|
|
658
|
+
}
|
|
638
659
|
const fromObstacle = componentObstacleRectangle(from.component);
|
|
639
660
|
const toObstacle = componentObstacleRectangle(to.component);
|
|
640
661
|
const escape = (geometry, obstacle) => ({
|
|
@@ -651,13 +672,8 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
651
672
|
});
|
|
652
673
|
const startEscape = escape(from, fromObstacle);
|
|
653
674
|
const endEscape = escape(to, toObstacle);
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
rectangle: entry.id === from.component.id || entry.id === to.component.id
|
|
657
|
-
? entry.body
|
|
658
|
-
: entry.expanded
|
|
659
|
-
}));
|
|
660
|
-
const obstacleRectangles = obstacleEntries.map((entry) => entry.rectangle);
|
|
675
|
+
const obstacleRectangle = (entry) => entry.id === from.component.id || entry.id === to.component.id ? entry.body : entry.expanded;
|
|
676
|
+
const obstacleRectangles = obstacleCache.map(obstacleRectangle);
|
|
661
677
|
const middle = routeBetweenEscapes(startEscape, endEscape, obstacleRectangles, bounds, connection.line);
|
|
662
678
|
if (middle === undefined) {
|
|
663
679
|
throw new SchematicSyntaxError('No collision-free orthogonal route exists.', connection.line);
|
|
@@ -666,10 +682,10 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
666
682
|
for (let index = 1; index < points.length; index += 1) {
|
|
667
683
|
const allowFrom = index === 1;
|
|
668
684
|
const allowTo = index === points.length - 1;
|
|
669
|
-
for (const obstacle of
|
|
685
|
+
for (const obstacle of obstacleCache) {
|
|
670
686
|
if (!(allowFrom && obstacle.id === from.component.id) &&
|
|
671
687
|
!(allowTo && obstacle.id === to.component.id) &&
|
|
672
|
-
segmentIntersectsRectangle(points[index - 1], points[index], obstacle
|
|
688
|
+
segmentIntersectsRectangle(points[index - 1], points[index], obstacleRectangle(obstacle))) {
|
|
673
689
|
throw new SchematicSyntaxError(`Orthogonal route intersects ${obstacle.id} after routing.`, connection.line);
|
|
674
690
|
}
|
|
675
691
|
}
|
|
@@ -743,6 +759,8 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
743
759
|
let cursorX = start.x;
|
|
744
760
|
for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
|
|
745
761
|
const radius = radii[crossingIndex];
|
|
762
|
+
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS)
|
|
763
|
+
continue;
|
|
746
764
|
const before = crossing.x - direction * radius;
|
|
747
765
|
const after = crossing.x + direction * radius;
|
|
748
766
|
if (before !== cursorX)
|
|
@@ -758,6 +776,8 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
758
776
|
let cursorY = start.y;
|
|
759
777
|
for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
|
|
760
778
|
const radius = radii[crossingIndex];
|
|
779
|
+
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS)
|
|
780
|
+
continue;
|
|
761
781
|
const before = crossing.y - direction * radius;
|
|
762
782
|
const after = crossing.y + direction * radius;
|
|
763
783
|
if (before !== cursorY)
|
|
@@ -961,4 +981,5 @@ export function validateDocumentGeometry(document, fence, routedConnections) {
|
|
|
961
981
|
throw new SchematicSyntaxError('Connection trace exceeds the declared schematic bounds.', connection.line);
|
|
962
982
|
}
|
|
963
983
|
}
|
|
984
|
+
return routes;
|
|
964
985
|
}
|
package/dist/limits.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hard compiler budgets shared by the parser
|
|
2
|
+
* Hard compiler budgets shared by the parser and renderer.
|
|
3
3
|
*
|
|
4
4
|
* `parseSchematic` and `renderSchematic` enforce these limits for one diagram.
|
|
5
|
-
* `schematicMarkedExtension` additionally enforces them cumulatively across all
|
|
6
|
-
* schematic fences encountered during one Marked document pass.
|
|
7
5
|
*
|
|
8
6
|
* @packageDocumentation
|
|
9
7
|
*/
|
package/dist/math-label.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeXml } from './xml.js';
|
|
1
2
|
const MATH_SYMBOLS = Object.freeze({
|
|
2
3
|
alpha: 'α',
|
|
3
4
|
beta: 'β',
|
|
@@ -20,28 +21,6 @@ const MATH_SYMBOLS = Object.freeze({
|
|
|
20
21
|
theta: 'θ',
|
|
21
22
|
times: '×'
|
|
22
23
|
});
|
|
23
|
-
function validXmlCodePoint(codePoint) {
|
|
24
|
-
if (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d)
|
|
25
|
-
return true;
|
|
26
|
-
if (codePoint >= 0x20 && codePoint <= 0xd7ff)
|
|
27
|
-
return true;
|
|
28
|
-
if (codePoint >= 0xe000 && codePoint <= 0xfffd)
|
|
29
|
-
return true;
|
|
30
|
-
return codePoint >= 0x10000 && codePoint <= 0x10ffff;
|
|
31
|
-
}
|
|
32
|
-
function escapeXml(value) {
|
|
33
|
-
let normalized = '';
|
|
34
|
-
for (const character of value) {
|
|
35
|
-
const codePoint = character.codePointAt(0);
|
|
36
|
-
normalized += validXmlCodePoint(codePoint) ? character : '\ufffd';
|
|
37
|
-
}
|
|
38
|
-
return normalized
|
|
39
|
-
.replaceAll('&', '&')
|
|
40
|
-
.replaceAll('<', '<')
|
|
41
|
-
.replaceAll('>', '>')
|
|
42
|
-
.replaceAll('"', '"')
|
|
43
|
-
.replaceAll("'", ''');
|
|
44
|
-
}
|
|
45
24
|
function commandAt(value, index) {
|
|
46
25
|
const escaped = value[index + 1];
|
|
47
26
|
if (escaped === '\\' ||
|
package/dist/parser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ANALOG_KINDS, CLASSICAL_GATE_KINDS, COMPONENT_KINDS, DIODE_TYPES, GROUND_STYLES, PASSIVE_KINDS, SEMANTIC_COLORS, SCHEMATIC_SIGNAL_MARKERS, TRANSISTOR_TYPES, UML_COMPONENT_KINDS, UML_RELATION_KINDS, SchematicSyntaxError } from './types.js';
|
|
2
2
|
import { validateDocumentGeometry } from './layout.js';
|
|
3
3
|
import { mathLabelTextWidth } from './math-label.js';
|
|
4
|
+
import { cacheParsedSchematicRoutes } from './route-cache.js';
|
|
4
5
|
import { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_SOURCE_CHARACTERS } from './limits.js';
|
|
5
6
|
const COMPONENT_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*):([A-Za-z][A-Za-z0-9_-]*)\s+"([^"]+)"\s+at\s+\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)\s+(.+)$/;
|
|
6
7
|
const CONNECTION_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_-]*)\s*->\s*([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_-]*)\s+(.+)$/;
|
|
@@ -764,6 +765,9 @@ export function parseSchematicFence(info, defaultTitle = 'Engineering schematic'
|
|
|
764
765
|
throw new SchematicSyntaxError('Schematic bounds must be integers from 64 through 4096.');
|
|
765
766
|
}
|
|
766
767
|
const title = match[3] ?? defaultTitle;
|
|
768
|
+
if (title.trim() === '') {
|
|
769
|
+
throw new SchematicSyntaxError('Schematic titles cannot be empty.');
|
|
770
|
+
}
|
|
767
771
|
if (title.length > MAX_FENCE_TITLE_LENGTH) {
|
|
768
772
|
throw new SchematicSyntaxError('Schematic titles cannot exceed 512 characters.');
|
|
769
773
|
}
|
|
@@ -814,6 +818,8 @@ export function parseSchematic(source, fence) {
|
|
|
814
818
|
validateEndpoint(connection.to, componentsById, connection.line);
|
|
815
819
|
}
|
|
816
820
|
const document = { components, connections };
|
|
817
|
-
validateDocumentGeometry(document, fence);
|
|
818
|
-
|
|
821
|
+
const routes = validateDocumentGeometry(document, fence);
|
|
822
|
+
const parsedDocument = freezeParsedDocument(document);
|
|
823
|
+
cacheParsedSchematicRoutes(parsedDocument, fence.bounds, routes);
|
|
824
|
+
return parsedDocument;
|
|
819
825
|
}
|
package/dist/renderer.js
CHANGED
|
@@ -1,36 +1,23 @@
|
|
|
1
1
|
import { classicalGateHeight, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, positionIcPin, PORT_HOTSPOT_RADIUS, routeConnections, validateDocumentGeometry } from './layout.js';
|
|
2
2
|
import { MAX_SCHEMATIC_SVG_OUTPUT_BYTES, utf8ByteLength } from './limits.js';
|
|
3
3
|
import { assertParsedSchematicDocument } from './parser.js';
|
|
4
|
-
import {
|
|
4
|
+
import { parsedSchematicRoutes } from './route-cache.js';
|
|
5
|
+
import { CLASSICAL_GATE_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
|
|
5
6
|
import { mathLabelTextWidth, renderMathLabelTspans } from './math-label.js';
|
|
7
|
+
import { escapeXml } from './xml.js';
|
|
6
8
|
export const MAX_SVG_OUTPUT_BYTES = MAX_SCHEMATIC_SVG_OUTPUT_BYTES;
|
|
7
9
|
const STATIC_SVG_STYLES = '.schematic-token{fill:none;stroke:var(--schematic-vector,var(--schematic-vector-fallback,currentColor));stroke-linecap:round;stroke-linejoin:round;stroke-width:var(--schematic-stroke-width,1.65);vector-effect:non-scaling-stroke}.schematic-node-fill{fill:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))}.schematic-designator,.schematic-label,.schematic-gate-symbol,.schematic-quantum-detail,.schematic-pin-label,.schematic-uml-text,.schematic-connection-label{fill:currentColor}.schematic-uml-row{font-size:12px}.schematic-uml-stereotype{font-size:11px}.schematic-connection-label{font-size:11px;paint-order:stroke;stroke:var(--schematic-surface,#fff);stroke-width:4;stroke-linejoin:round}.schematic-surface{fill:var(--schematic-surface,transparent)}.schematic-grid-line{fill:none;stroke:var(--schematic-grid,currentColor);stroke-width:1;opacity:.12;vector-effect:non-scaling-stroke}';
|
|
8
10
|
const HOOK_SVG_STYLES = '.schematic-port-hotspot{fill:transparent!important;stroke:transparent!important;stroke-width:8;vector-effect:non-scaling-stroke;pointer-events:all;outline:none}.schematic-port-hotspot:focus-visible{fill:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))!important;fill-opacity:.22!important;stroke:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))!important;stroke-width:2}';
|
|
9
11
|
const INTERACTIVE_SVG_STYLES = '.schematic-component,.schematic-wire{outline:none;transition:opacity .2s ease,filter .2s ease}.schematic-component .schematic-token,.schematic-wire .schematic-token{transition:stroke .2s ease,fill .2s ease,color .2s ease,opacity .2s ease,stroke-width .2s ease}.schematic-glow-layer{opacity:0;pointer-events:none;transition:opacity .2s ease}.schematic-component:hover>.schematic-glow-layer,.schematic-component:focus>.schematic-glow-layer,.schematic-component:focus-within>.schematic-glow-layer,.schematic-component.is-hovered>.schematic-glow-layer,.schematic-component.is-active>.schematic-glow-layer,.schematic-component.is-selected>.schematic-glow-layer,.schematic-wire:hover>.schematic-glow-layer,.schematic-wire:focus>.schematic-glow-layer,.schematic-wire.is-hovered>.schematic-glow-layer,.schematic-wire.is-active>.schematic-glow-layer,.schematic-wire.is-selected>.schematic-glow-layer{opacity:1}.schematic-component.is-degraded,.schematic-wire.is-degraded{opacity:.45}.schematic-component.is-degraded>.schematic-glow-layer,.schematic-wire.is-degraded>.schematic-glow-layer{opacity:0}.schematic-component:hover .schematic-token,.schematic-component:focus .schematic-token,.schematic-component:focus-within .schematic-token,.schematic-component.is-hovered .schematic-token,.schematic-component.is-active .schematic-token,.schematic-component.is-selected .schematic-token,.schematic-wire:hover .schematic-token,.schematic-wire:focus .schematic-token,.schematic-wire.is-hovered .schematic-token,.schematic-wire.is-active .schematic-token,.schematic-wire.is-selected .schematic-token{stroke-width:var(--schematic-interactive-stroke-width,2.25)}.schematic-port-hotspot{fill:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))!important;fill-opacity:0;transition:fill-opacity .2s ease,opacity .2s ease}.schematic-port-hotspot:hover,.schematic-port-hotspot:focus,.schematic-port-hotspot.is-hovered,.schematic-port-hotspot.is-active,.schematic-port-hotspot.is-selected{fill-opacity:.2}.schematic-port-hotspot.is-degraded{opacity:.45}@media(prefers-reduced-motion:reduce){.schematic-component,.schematic-wire,.schematic-token,.schematic-glow-layer,.schematic-port-hotspot{transition:none}}';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function replaceInvalidXmlCharacters(value) {
|
|
20
|
-
let normalized = '';
|
|
21
|
-
for (const character of value) {
|
|
22
|
-
const codePoint = character.codePointAt(0);
|
|
23
|
-
normalized += validXmlCodePoint(codePoint) ? character : '\ufffd';
|
|
24
|
-
}
|
|
25
|
-
return normalized;
|
|
26
|
-
}
|
|
27
|
-
function escapeXml(value) {
|
|
28
|
-
return replaceInvalidXmlCharacters(value)
|
|
29
|
-
.replaceAll('&', '&')
|
|
30
|
-
.replaceAll('<', '<')
|
|
31
|
-
.replaceAll('>', '>')
|
|
32
|
-
.replaceAll('"', '"')
|
|
33
|
-
.replaceAll("'", ''');
|
|
12
|
+
const NODE_HOOK = 1;
|
|
13
|
+
const PORT_HOOK = 2;
|
|
14
|
+
const WIRE_HOOK = 4;
|
|
15
|
+
function semanticHookBit(hook) {
|
|
16
|
+
if (hook === 'nodes')
|
|
17
|
+
return NODE_HOOK;
|
|
18
|
+
if (hook === 'ports')
|
|
19
|
+
return PORT_HOOK;
|
|
20
|
+
return WIRE_HOOK;
|
|
34
21
|
}
|
|
35
22
|
export class BoundedSvgWriter {
|
|
36
23
|
#chunks = [];
|
|
@@ -82,9 +69,24 @@ function normalizeCompileOptions(value) {
|
|
|
82
69
|
throw new SchematicSyntaxError('Render mode must be one of: default, embedded-css, or full.');
|
|
83
70
|
}
|
|
84
71
|
const normalizedMode = (mode ?? 'default');
|
|
72
|
+
const rawSemanticHooks = candidate.semanticHooks;
|
|
73
|
+
let semanticHookMask = NODE_HOOK | PORT_HOOK | WIRE_HOOK;
|
|
74
|
+
if (rawSemanticHooks !== undefined) {
|
|
75
|
+
if (!Array.isArray(rawSemanticHooks)) {
|
|
76
|
+
throw new SchematicSyntaxError('Render semanticHooks must be an array.');
|
|
77
|
+
}
|
|
78
|
+
semanticHookMask = 0;
|
|
79
|
+
for (const hook of rawSemanticHooks) {
|
|
80
|
+
if (typeof hook !== 'string' ||
|
|
81
|
+
!SCHEMD_SEMANTIC_HOOKS.includes(hook)) {
|
|
82
|
+
throw new SchematicSyntaxError('Render semanticHooks may contain only: nodes, ports, or wires.');
|
|
83
|
+
}
|
|
84
|
+
semanticHookMask |= semanticHookBit(hook);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
85
87
|
return idPrefix === undefined
|
|
86
|
-
? { bounds: { width, height }, title, mode: normalizedMode }
|
|
87
|
-
: { bounds: { width, height }, title, idPrefix, mode: normalizedMode };
|
|
88
|
+
? { bounds: { width, height }, title, mode: normalizedMode, semanticHookMask }
|
|
89
|
+
: { bounds: { width, height }, title, idPrefix, mode: normalizedMode, semanticHookMask };
|
|
88
90
|
}
|
|
89
91
|
function svgNumber(value) {
|
|
90
92
|
return String(Number(value.toFixed(3)));
|
|
@@ -284,7 +286,7 @@ function integratedCircuitShape(component, paint) {
|
|
|
284
286
|
return `<rect ${paint} x="${left}" y="${top}" width="${component.bodyWidth}" height="${component.bodyHeight}" rx="3" />${icPinMarkup(component, 'left', paint)}${icPinMarkup(component, 'right', paint)}${icPinMarkup(component, 'top', paint)}${icPinMarkup(component, 'bottom', paint)}<text class="schematic-gate-symbol" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="12" textLength="${fittedTextLength(component.label, component.bodyWidth - 12, 7)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(component.label)}</text>`;
|
|
285
287
|
}
|
|
286
288
|
function umlRow(value, x, y) {
|
|
287
|
-
return `<text class="schematic-uml-text schematic-uml-row" fill="currentColor" stroke="none" x="${svgNumber(x)}" y="${svgNumber(y)}" font-size="12">${
|
|
289
|
+
return `<text class="schematic-uml-text schematic-uml-row" fill="currentColor" stroke="none" x="${svgNumber(x)}" y="${svgNumber(y)}" font-size="12">${renderMathLabelTspans(value)}</text>`;
|
|
288
290
|
}
|
|
289
291
|
function umlClassShape(component, paint) {
|
|
290
292
|
const left = -component.bodyWidth / 2;
|
|
@@ -295,7 +297,7 @@ function umlClassShape(component, paint) {
|
|
|
295
297
|
const operationSeparator = attributeSeparator + attributeHeight;
|
|
296
298
|
const stereotype = component.stereotype === undefined
|
|
297
299
|
? ''
|
|
298
|
-
: `<text class="schematic-uml-text schematic-uml-stereotype" fill="currentColor" stroke="none" x="0" y="${svgNumber(top + 13)}" text-anchor="middle" font-size="11">«${
|
|
300
|
+
: `<text class="schematic-uml-text schematic-uml-stereotype" fill="currentColor" stroke="none" x="0" y="${svgNumber(top + 13)}" text-anchor="middle" font-size="11">«${renderMathLabelTspans(component.stereotype)}»</text>`;
|
|
299
301
|
const nameY = top + (component.stereotype === undefined ? 23 : 32);
|
|
300
302
|
const attributes = component.attributes
|
|
301
303
|
.map((row, index) => umlRow(row, left + 8, attributeSeparator + 17 + index * 16))
|
|
@@ -516,19 +518,18 @@ function componentPortMarkup(component) {
|
|
|
516
518
|
})
|
|
517
519
|
.join('');
|
|
518
520
|
}
|
|
519
|
-
function componentMarkup(component, index, idPrefix, glowId, mode, symbolId) {
|
|
521
|
+
function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, portHooks, symbolId) {
|
|
520
522
|
const label = escapeXml(component.label);
|
|
521
523
|
const renderedLabel = renderMathLabelTspans(component.label);
|
|
522
524
|
const anchors = componentTextAnchors(component);
|
|
523
525
|
const id = escapeXml(component.id);
|
|
524
526
|
const vectorId = `${idPrefix}-node-${index}-vector`;
|
|
525
527
|
const styles = mode === 'embedded-css' || mode === 'full';
|
|
526
|
-
const
|
|
527
|
-
const dataAttributes = hooks
|
|
528
|
+
const dataAttributes = nodeHooks
|
|
528
529
|
? ` data-node-id="${id}" data-node-kind="${component.kind}" data-node-label="${label}" data-component="${id}" data-kind="${component.kind}"`
|
|
529
530
|
: '';
|
|
530
531
|
let accessibility = '';
|
|
531
|
-
if (styles
|
|
532
|
+
if (styles) {
|
|
532
533
|
const metadata = componentMetadata(component);
|
|
533
534
|
const ariaLabel = escapeXml(`${component.id}, ${component.kind}, ${component.label}${metadata === '' ? '' : `, ${metadata}`}`);
|
|
534
535
|
accessibility = ` tabindex="0" role="group" aria-label="${ariaLabel}"`;
|
|
@@ -537,7 +538,7 @@ function componentMarkup(component, index, idPrefix, glowId, mode, symbolId) {
|
|
|
537
538
|
const glow = styles
|
|
538
539
|
? `<use class="schematic-glow-layer" href="#${vectorId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />`
|
|
539
540
|
: '';
|
|
540
|
-
const hotspots =
|
|
541
|
+
const hotspots = portHooks ? componentPortMarkup(component) : '';
|
|
541
542
|
const vector = symbolId === undefined
|
|
542
543
|
? `<g${vectorIdAttribute} class="schematic-component-vector">${componentShape(component)}</g>`
|
|
543
544
|
: `<use${vectorIdAttribute} ${colorAttributes(component.color, 'schematic-component-vector')} href="#${symbolId}" />`;
|
|
@@ -550,7 +551,8 @@ export function renderSchematic(document, options) {
|
|
|
550
551
|
assertParsedSchematicDocument(document);
|
|
551
552
|
const normalized = normalizeCompileOptions(options);
|
|
552
553
|
const components = new Map(document.components.map((component) => [component.id, component]));
|
|
553
|
-
const routedConnections =
|
|
554
|
+
const routedConnections = parsedSchematicRoutes(document, normalized.bounds) ??
|
|
555
|
+
routeConnections(document.connections, components, normalized.bounds);
|
|
554
556
|
validateDocumentGeometry(document, normalized, routedConnections);
|
|
555
557
|
const signature = `${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`;
|
|
556
558
|
const candidatePrefix = (normalized.idPrefix ?? `schematic-${stableHash(signature)}`).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
@@ -563,6 +565,9 @@ export function renderSchematic(document, options) {
|
|
|
563
565
|
const glowId = `${safePrefix}-schematic-glow-filter`;
|
|
564
566
|
const styles = normalized.mode === 'embedded-css' || normalized.mode === 'full';
|
|
565
567
|
const hooks = normalized.mode === 'full';
|
|
568
|
+
const nodeHooks = hooks && (normalized.semanticHookMask & NODE_HOOK) !== 0;
|
|
569
|
+
const portHooks = hooks && (normalized.semanticHookMask & PORT_HOOK) !== 0;
|
|
570
|
+
const wireHooks = hooks && (normalized.semanticHookMask & WIRE_HOOK) !== 0;
|
|
566
571
|
const reusableSymbols = new Map();
|
|
567
572
|
for (const component of document.components) {
|
|
568
573
|
const key = reusableSymbolKey(component);
|
|
@@ -574,7 +579,7 @@ export function renderSchematic(document, options) {
|
|
|
574
579
|
const componentCount = document.components.length;
|
|
575
580
|
const description = `${componentCount} component${componentCount === 1 ? '' : 's'} and ${document.connections.length} connection${document.connections.length === 1 ? '' : 's'}.`;
|
|
576
581
|
const writer = new BoundedSvgWriter();
|
|
577
|
-
const hookStyles =
|
|
582
|
+
const hookStyles = portHooks ? HOOK_SVG_STYLES : '';
|
|
578
583
|
const interactionStyles = styles ? INTERACTIVE_SVG_STYLES : '';
|
|
579
584
|
const glowFilter = styles
|
|
580
585
|
? `<filter id="${glowId}" x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="2.2" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter>`
|
|
@@ -587,7 +592,7 @@ export function renderSchematic(document, options) {
|
|
|
587
592
|
: '';
|
|
588
593
|
const definitions = `${embeddedDefinitions}${markerDefinitions}${symbolDefinitions}`;
|
|
589
594
|
writer.append(`<figure class="schematic-frame"${hooks ? ' data-schematic' : ''}><svg class="schematic-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${normalized.bounds.width} ${normalized.bounds.height}" width="${normalized.bounds.width}" height="${normalized.bounds.height}" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" role="img" aria-labelledby="${titleId} ${descriptionId}" preserveAspectRatio="xMidYMid meet"><title id="${titleId}">${title}</title><desc id="${descriptionId}">${description}</desc>${definitions === '' ? '' : `<defs>${definitions}</defs>`}${styles ? `<rect class="schematic-surface" width="100%" height="100%" /><rect class="schematic-grid" width="100%" height="100%" fill="url(#${gridId})" />` : ''}<g class="schematic-vectors">`);
|
|
590
|
-
if (
|
|
595
|
+
if (wireHooks) {
|
|
591
596
|
for (const [index, connection] of document.connections.entries()) {
|
|
592
597
|
writer.append(connectionMarkup(connection, routedConnections[index], index, safePrefix, glowId));
|
|
593
598
|
}
|
|
@@ -598,7 +603,7 @@ export function renderSchematic(document, options) {
|
|
|
598
603
|
for (const [index, component] of document.components.entries()) {
|
|
599
604
|
const key = reusableSymbolKey(component);
|
|
600
605
|
const symbolId = key === undefined ? undefined : reusableSymbols.get(key)?.id;
|
|
601
|
-
writer.append(componentMarkup(component, index, safePrefix, glowId, normalized.mode, symbolId));
|
|
606
|
+
writer.append(componentMarkup(component, index, safePrefix, glowId, normalized.mode, nodeHooks, portHooks, symbolId));
|
|
602
607
|
}
|
|
603
608
|
writer.append(`</g></svg><figcaption>${title}</figcaption></figure>`);
|
|
604
609
|
return writer.finish();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RoutedConnection } from './layout.js';
|
|
2
|
+
import type { SchematicBounds, SchematicDocument } from './types.js';
|
|
3
|
+
/** Cache a validated route set against the bounds that produced it. */
|
|
4
|
+
export declare function cacheParsedSchematicRoutes(document: SchematicDocument, bounds: SchematicBounds, routes: readonly RoutedConnection[]): void;
|
|
5
|
+
/** Reuse routes only when render bounds match the parser's geometry contract. */
|
|
6
|
+
export declare function parsedSchematicRoutes(document: SchematicDocument, bounds: SchematicBounds): readonly RoutedConnection[] | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const parsedDocumentRoutes = new WeakMap();
|
|
2
|
+
export function cacheParsedSchematicRoutes(document, bounds, routes) {
|
|
3
|
+
parsedDocumentRoutes.set(document, { width: bounds.width, height: bounds.height, routes });
|
|
4
|
+
}
|
|
5
|
+
export function parsedSchematicRoutes(document, bounds) {
|
|
6
|
+
const cached = parsedDocumentRoutes.get(document);
|
|
7
|
+
return cached?.width === bounds.width && cached.height === bounds.height
|
|
8
|
+
? cached.routes
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -261,6 +261,10 @@ export interface SchematicDocument {
|
|
|
261
261
|
export declare const SCHEMD_OUTPUT_MODES: readonly ["default", "embedded-css", "full"];
|
|
262
262
|
/** Static, CSS-enhanced, or fully attributed SVG output mode. */
|
|
263
263
|
export type SchemdOutputMode = (typeof SCHEMD_OUTPUT_MODES)[number];
|
|
264
|
+
/** Optional semantic payloads emitted by `full` mode. */
|
|
265
|
+
export declare const SCHEMD_SEMANTIC_HOOKS: readonly ["nodes", "ports", "wires"];
|
|
266
|
+
/** A selectable delegated-interaction payload. */
|
|
267
|
+
export type SchematicSemanticHook = (typeof SCHEMD_SEMANTIC_HOOKS)[number];
|
|
264
268
|
/** Marker primitives that can terminate or originate a signal trace. */
|
|
265
269
|
export declare const SCHEMATIC_SIGNAL_MARKERS: readonly ["none", "arrow", "open-arrow", "dot", "triangle", "diamond", "diamond-filled"];
|
|
266
270
|
/** A validated connection marker selection. */
|
|
@@ -271,17 +275,8 @@ export interface CompileSchematicOptions extends SchematicFence {
|
|
|
271
275
|
idPrefix?: string;
|
|
272
276
|
/** Markup and interaction budget for the generated SVG. */
|
|
273
277
|
mode?: SchemdOutputMode;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
export interface SchematicMarkedOptions {
|
|
277
|
-
/** Accessible title used when the fence omits one. */
|
|
278
|
-
defaultTitle?: string;
|
|
279
|
-
/** Fixed output mode for every schemd fence in a Markdown pass. */
|
|
280
|
-
mode?: SchemdOutputMode;
|
|
281
|
-
/** Synchronous mode resolver for hosts with request-scoped rendering state. */
|
|
282
|
-
resolveMode?: () => SchemdOutputMode;
|
|
283
|
-
/** Optional safe fallback renderer for bounded syntax failures. */
|
|
284
|
-
onError?: (error: SchematicSyntaxError, source: string) => string;
|
|
278
|
+
/** Full-mode metadata to emit. Defaults to nodes, ports, and wires. */
|
|
279
|
+
semanticHooks?: readonly SchematicSemanticHook[];
|
|
285
280
|
}
|
|
286
281
|
/**
|
|
287
282
|
* Deterministic syntax or geometry failure with optional source location.
|
package/dist/types.js
CHANGED
|
@@ -38,6 +38,7 @@ export const UML_RELATION_KINDS = [
|
|
|
38
38
|
'extend'
|
|
39
39
|
];
|
|
40
40
|
export const SCHEMD_OUTPUT_MODES = ['default', 'embedded-css', 'full'];
|
|
41
|
+
export const SCHEMD_SEMANTIC_HOOKS = ['nodes', 'ports', 'wires'];
|
|
41
42
|
export const SCHEMATIC_SIGNAL_MARKERS = [
|
|
42
43
|
'none',
|
|
43
44
|
'arrow',
|
package/dist/xml.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared single-pass XML character-data escaping for generated SVG markup. */
|
|
2
|
+
/**
|
|
3
|
+
* Replace XML-forbidden code points and escape XML-significant characters in
|
|
4
|
+
* one pass, avoiding the normalized copy plus five full-string replacements.
|
|
5
|
+
*/
|
|
6
|
+
export declare function escapeXml(value: string): string;
|
package/dist/xml.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function validXmlCodePoint(codePoint) {
|
|
2
|
+
if (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d)
|
|
3
|
+
return true;
|
|
4
|
+
if (codePoint >= 0x20 && codePoint <= 0xd7ff)
|
|
5
|
+
return true;
|
|
6
|
+
if (codePoint >= 0xe000 && codePoint <= 0xfffd)
|
|
7
|
+
return true;
|
|
8
|
+
return codePoint >= 0x10000 && codePoint <= 0x10ffff;
|
|
9
|
+
}
|
|
10
|
+
export function escapeXml(value) {
|
|
11
|
+
let escaped = '';
|
|
12
|
+
for (const character of value) {
|
|
13
|
+
if (!validXmlCodePoint(character.codePointAt(0))) {
|
|
14
|
+
escaped += '\ufffd';
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
switch (character) {
|
|
18
|
+
case '&':
|
|
19
|
+
escaped += '&';
|
|
20
|
+
break;
|
|
21
|
+
case '<':
|
|
22
|
+
escaped += '<';
|
|
23
|
+
break;
|
|
24
|
+
case '>':
|
|
25
|
+
escaped += '>';
|
|
26
|
+
break;
|
|
27
|
+
case '"':
|
|
28
|
+
escaped += '"';
|
|
29
|
+
break;
|
|
30
|
+
case "'":
|
|
31
|
+
escaped += ''';
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
escaped += character;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return escaped;
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schemd/core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Zero-dependency text-to-SVG compiler for schematics and UML.",
|
|
5
|
-
"homepage": "https://johnowolabiidogun.dev/tools/schemd
|
|
5
|
+
"homepage": "https://johnowolabiidogun.dev/tools/schemd",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/Sirneij/schemd/issues"
|
|
8
8
|
},
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
-
"README.md"
|
|
20
|
+
"README.md",
|
|
21
|
+
"assets/brand/schemd-logo.svg",
|
|
22
|
+
"assets/brand/schemd-mark.svg"
|
|
21
23
|
],
|
|
22
24
|
"exports": {
|
|
23
25
|
".": {
|
|
@@ -32,6 +34,10 @@
|
|
|
32
34
|
"types": "./dist/renderer.d.ts",
|
|
33
35
|
"import": "./dist/renderer.js"
|
|
34
36
|
},
|
|
37
|
+
"./compiler": {
|
|
38
|
+
"types": "./dist/compiler.d.ts",
|
|
39
|
+
"import": "./dist/compiler.js"
|
|
40
|
+
},
|
|
35
41
|
"./layout": {
|
|
36
42
|
"types": "./dist/layout.d.ts",
|
|
37
43
|
"import": "./dist/layout.js"
|
|
@@ -40,10 +46,6 @@
|
|
|
40
46
|
"types": "./dist/math-label.d.ts",
|
|
41
47
|
"import": "./dist/math-label.js"
|
|
42
48
|
},
|
|
43
|
-
"./marked": {
|
|
44
|
-
"types": "./dist/marked-extension.d.ts",
|
|
45
|
-
"import": "./dist/marked-extension.js"
|
|
46
|
-
},
|
|
47
49
|
"./types": {
|
|
48
50
|
"types": "./dist/types.d.ts",
|
|
49
51
|
"import": "./dist/types.js"
|
|
@@ -53,21 +55,14 @@
|
|
|
53
55
|
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json",
|
|
54
56
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
55
57
|
"prepack": "npm run build",
|
|
58
|
+
"size": "node scripts/check-bundle-size.mjs",
|
|
56
59
|
"test": "vitest run",
|
|
57
60
|
"test:coverage": "vitest run --coverage"
|
|
58
61
|
},
|
|
59
|
-
"peerDependencies": {
|
|
60
|
-
"marked": ">=17 <19"
|
|
61
|
-
},
|
|
62
|
-
"peerDependenciesMeta": {
|
|
63
|
-
"marked": {
|
|
64
|
-
"optional": true
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
62
|
"devDependencies": {
|
|
68
63
|
"@vitest/coverage-v8": "^4.1.10",
|
|
69
|
-
"marked": "^18.0.6",
|
|
70
64
|
"typescript": "^6.0.3",
|
|
65
|
+
"vite": "^8.1.4",
|
|
71
66
|
"vitest": "^4.1.10"
|
|
72
67
|
},
|
|
73
68
|
"engines": {
|
|
@@ -75,7 +70,6 @@
|
|
|
75
70
|
},
|
|
76
71
|
"keywords": [
|
|
77
72
|
"schemd",
|
|
78
|
-
"marked",
|
|
79
73
|
"schematic",
|
|
80
74
|
"svg",
|
|
81
75
|
"compiler",
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type-only Marked adapter for compiling fenced `schemd` diagrams during SSR.
|
|
3
|
-
*
|
|
4
|
-
* The module imports no Marked runtime. Its factory returns ordinary extension
|
|
5
|
-
* hooks to a host-owned parser and enforces both per-diagram compiler ceilings
|
|
6
|
-
* and cumulative limits across each Markdown document pass.
|
|
7
|
-
*
|
|
8
|
-
* @packageDocumentation
|
|
9
|
-
*/
|
|
10
|
-
import type { MarkedExtension } from 'marked';
|
|
11
|
-
import { type SchematicMarkedOptions } from './types.js';
|
|
12
|
-
/**
|
|
13
|
-
* Create a bounded Marked extension that compiles `schemd` fences into inline SVG.
|
|
14
|
-
*
|
|
15
|
-
* The returned extension keeps aggregate per-document budgets in closure state,
|
|
16
|
-
* resets them from Marked's `preprocess` hook, and recognizes only the canonical
|
|
17
|
-
* `schemd` language identifier.
|
|
18
|
-
*
|
|
19
|
-
* @param options - Output mode, accessible-title fallback, dynamic mode resolver,
|
|
20
|
-
* and trusted server-side diagnostic renderer.
|
|
21
|
-
* @returns A Marked extension suitable for a server-owned `Marked` instance.
|
|
22
|
-
*/
|
|
23
|
-
export declare function schematicMarkedExtension(options?: SchematicMarkedOptions): MarkedExtension;
|
package/dist/marked-extension.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_SOURCE_CHARACTERS, MAX_SCHEMATIC_SVG_OUTPUT_BYTES, utf8ByteLength } from './limits.js';
|
|
2
|
-
import { parseSchematic, parseSchematicFence } from './parser.js';
|
|
3
|
-
import { renderSchematic } from './renderer.js';
|
|
4
|
-
import { SchematicSyntaxError } from './types.js';
|
|
5
|
-
class SchematicDocumentBudgetError extends SchematicSyntaxError {
|
|
6
|
-
}
|
|
7
|
-
function emptyDocumentBudget() {
|
|
8
|
-
return { sourceCharacters: 0, components: 0, connections: 0, svgOutputBytes: 0 };
|
|
9
|
-
}
|
|
10
|
-
function consumeBudget(used, amount, limit, unit) {
|
|
11
|
-
if (amount > limit - used) {
|
|
12
|
-
throw new SchematicDocumentBudgetError(`Schematic document exceeds the cumulative ${limit.toLocaleString('en-US')} ${unit} limit.`);
|
|
13
|
-
}
|
|
14
|
-
return used + amount;
|
|
15
|
-
}
|
|
16
|
-
function escapeHtml(value) {
|
|
17
|
-
return value
|
|
18
|
-
.replaceAll('&', '&')
|
|
19
|
-
.replaceAll('<', '<')
|
|
20
|
-
.replaceAll('>', '>')
|
|
21
|
-
.replaceAll('"', '"')
|
|
22
|
-
.replaceAll("'", ''');
|
|
23
|
-
}
|
|
24
|
-
function defaultErrorRenderer(error, source) {
|
|
25
|
-
return `<figure class="schematic-error" role="group" aria-label="Schematic compilation error"><figcaption>${escapeHtml(error.message)}</figcaption><pre><code class="language-schemd">${escapeHtml(source)}</code></pre></figure>`;
|
|
26
|
-
}
|
|
27
|
-
export function schematicMarkedExtension(options = {}) {
|
|
28
|
-
let diagramIndex = 0;
|
|
29
|
-
let budget = emptyDocumentBudget();
|
|
30
|
-
let budgetError;
|
|
31
|
-
return {
|
|
32
|
-
hooks: {
|
|
33
|
-
preprocess(source) {
|
|
34
|
-
diagramIndex = 0;
|
|
35
|
-
budget = emptyDocumentBudget();
|
|
36
|
-
budgetError = undefined;
|
|
37
|
-
return source;
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
renderer: {
|
|
41
|
-
code(token) {
|
|
42
|
-
if (!token.lang || !/^schemd(?:\s|$)/i.test(token.lang.trim())) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
if (budgetError)
|
|
47
|
-
throw budgetError;
|
|
48
|
-
budget.sourceCharacters = consumeBudget(budget.sourceCharacters, token.text.length, MAX_SCHEMATIC_SOURCE_CHARACTERS, 'schematic source character');
|
|
49
|
-
const fence = parseSchematicFence(token.lang, options.defaultTitle);
|
|
50
|
-
diagramIndex += 1;
|
|
51
|
-
const document = parseSchematic(token.text, fence);
|
|
52
|
-
const nextComponents = consumeBudget(budget.components, document.components.length, MAX_SCHEMATIC_COMPONENTS, 'component');
|
|
53
|
-
const nextConnections = consumeBudget(budget.connections, document.connections.length, MAX_SCHEMATIC_CONNECTIONS, 'connection');
|
|
54
|
-
const html = renderSchematic(document, {
|
|
55
|
-
...fence,
|
|
56
|
-
idPrefix: `schematic-${diagramIndex}`,
|
|
57
|
-
mode: options.resolveMode?.() ?? options.mode ?? 'default'
|
|
58
|
-
});
|
|
59
|
-
const nextSvgOutputBytes = consumeBudget(budget.svgOutputBytes, utf8ByteLength(html), MAX_SCHEMATIC_SVG_OUTPUT_BYTES, 'compiled SVG byte');
|
|
60
|
-
budget.components = nextComponents;
|
|
61
|
-
budget.connections = nextConnections;
|
|
62
|
-
budget.svgOutputBytes = nextSvgOutputBytes;
|
|
63
|
-
return html;
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
if (!(error instanceof SchematicSyntaxError))
|
|
67
|
-
throw error;
|
|
68
|
-
if (error instanceof SchematicDocumentBudgetError)
|
|
69
|
-
budgetError = error;
|
|
70
|
-
return (options.onError ?? defaultErrorRenderer)(error, token.text);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
}
|