@schemd/core 0.2.0 → 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 +14 -2
- 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/layout.d.ts +1 -1
- package/dist/layout.js +30 -9
- package/dist/math-label.js +1 -22
- package/dist/parser.js +8 -2
- package/dist/renderer.js +6 -28
- package/dist/route-cache.d.ts +6 -0
- package/dist/route-cache.js +10 -0
- package/dist/xml.d.ts +6 -0
- package/dist/xml.js +38 -0
- package/package.json +7 -3
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,7 +11,7 @@ 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
17
|
Node.js 24+ is required.
|
|
@@ -53,6 +57,14 @@ Class, actor, use-case, state, lifeline, note, package, initial, and final nodes
|
|
|
53
57
|
|
|
54
58
|
Use `mode: "embedded-css"` for built-in styling or `mode: "full"` for interaction metadata. The default output stays compact and static.
|
|
55
59
|
|
|
60
|
+
On a colored surface, set `--schematic-surface` to that background color so hollow UML markers and connector-label halos match it.
|
|
61
|
+
|
|
56
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.
|
|
57
63
|
|
|
58
|
-
|
|
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.
|
|
69
|
+
|
|
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>
|
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/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,8 +1,10 @@
|
|
|
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 { parsedSchematicRoutes } from './route-cache.js';
|
|
4
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}';
|
|
@@ -17,31 +19,6 @@ function semanticHookBit(hook) {
|
|
|
17
19
|
return PORT_HOOK;
|
|
18
20
|
return WIRE_HOOK;
|
|
19
21
|
}
|
|
20
|
-
function validXmlCodePoint(codePoint) {
|
|
21
|
-
if (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d)
|
|
22
|
-
return true;
|
|
23
|
-
if (codePoint >= 0x20 && codePoint <= 0xd7ff)
|
|
24
|
-
return true;
|
|
25
|
-
if (codePoint >= 0xe000 && codePoint <= 0xfffd)
|
|
26
|
-
return true;
|
|
27
|
-
return codePoint >= 0x10000 && codePoint <= 0x10ffff;
|
|
28
|
-
}
|
|
29
|
-
function replaceInvalidXmlCharacters(value) {
|
|
30
|
-
let normalized = '';
|
|
31
|
-
for (const character of value) {
|
|
32
|
-
const codePoint = character.codePointAt(0);
|
|
33
|
-
normalized += validXmlCodePoint(codePoint) ? character : '\ufffd';
|
|
34
|
-
}
|
|
35
|
-
return normalized;
|
|
36
|
-
}
|
|
37
|
-
function escapeXml(value) {
|
|
38
|
-
return replaceInvalidXmlCharacters(value)
|
|
39
|
-
.replaceAll('&', '&')
|
|
40
|
-
.replaceAll('<', '<')
|
|
41
|
-
.replaceAll('>', '>')
|
|
42
|
-
.replaceAll('"', '"')
|
|
43
|
-
.replaceAll("'", ''');
|
|
44
|
-
}
|
|
45
22
|
export class BoundedSvgWriter {
|
|
46
23
|
#chunks = [];
|
|
47
24
|
#bytes = 0;
|
|
@@ -309,7 +286,7 @@ function integratedCircuitShape(component, paint) {
|
|
|
309
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>`;
|
|
310
287
|
}
|
|
311
288
|
function umlRow(value, x, y) {
|
|
312
|
-
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>`;
|
|
313
290
|
}
|
|
314
291
|
function umlClassShape(component, paint) {
|
|
315
292
|
const left = -component.bodyWidth / 2;
|
|
@@ -320,7 +297,7 @@ function umlClassShape(component, paint) {
|
|
|
320
297
|
const operationSeparator = attributeSeparator + attributeHeight;
|
|
321
298
|
const stereotype = component.stereotype === undefined
|
|
322
299
|
? ''
|
|
323
|
-
: `<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>`;
|
|
324
301
|
const nameY = top + (component.stereotype === undefined ? 23 : 32);
|
|
325
302
|
const attributes = component.attributes
|
|
326
303
|
.map((row, index) => umlRow(row, left + 8, attributeSeparator + 17 + index * 16))
|
|
@@ -574,7 +551,8 @@ export function renderSchematic(document, options) {
|
|
|
574
551
|
assertParsedSchematicDocument(document);
|
|
575
552
|
const normalized = normalizeCompileOptions(options);
|
|
576
553
|
const components = new Map(document.components.map((component) => [component.id, component]));
|
|
577
|
-
const routedConnections =
|
|
554
|
+
const routedConnections = parsedSchematicRoutes(document, normalized.bounds) ??
|
|
555
|
+
routeConnections(document.connections, components, normalized.bounds);
|
|
578
556
|
validateDocumentGeometry(document, normalized, routedConnections);
|
|
579
557
|
const signature = `${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`;
|
|
580
558
|
const candidatePrefix = (normalized.idPrefix ?? `schematic-${stableHash(signature)}`).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
@@ -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/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.2.
|
|
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
|
".": {
|
|
@@ -53,12 +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
62
|
"devDependencies": {
|
|
60
63
|
"@vitest/coverage-v8": "^4.1.10",
|
|
61
64
|
"typescript": "^6.0.3",
|
|
65
|
+
"vite": "^8.1.4",
|
|
62
66
|
"vitest": "^4.1.10"
|
|
63
67
|
},
|
|
64
68
|
"engines": {
|