@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 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 install @schemd/core
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
- [Documentation](https://johnowolabiidogun.dev/tools/schemd/docs/overview) · [Issues](https://github.com/Sirneij/schemd/issues) · [MIT](https://github.com/Sirneij/schemd/blob/main/LICENSE)
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[]): void;
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 obstacleEntries = obstacleCache.map((entry) => ({
655
- id: entry.id,
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 obstacleEntries) {
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.rectangle)) {
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
  }
@@ -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('&', '&amp;')
40
- .replaceAll('<', '&lt;')
41
- .replaceAll('>', '&gt;')
42
- .replaceAll('"', '&quot;')
43
- .replaceAll("'", '&#39;');
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
- return freezeParsedDocument(document);
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('&', '&amp;')
40
- .replaceAll('<', '&lt;')
41
- .replaceAll('>', '&gt;')
42
- .replaceAll('"', '&quot;')
43
- .replaceAll("'", '&#39;');
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">${escapeXml(value)}</text>`;
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">«${escapeXml(component.stereotype)}»</text>`;
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 = routeConnections(document.connections, components, normalized.bounds);
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 += '&amp;';
20
+ break;
21
+ case '<':
22
+ escaped += '&lt;';
23
+ break;
24
+ case '>':
25
+ escaped += '&gt;';
26
+ break;
27
+ case '"':
28
+ escaped += '&quot;';
29
+ break;
30
+ case "'":
31
+ escaped += '&#39;';
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.0",
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/docs/overview",
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": {