@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 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 install @schemd/core
14
+ npm i @schemd/core # bun i @schemd/core
11
15
  ```
12
16
 
13
- Node.js 24+ is required. Install `marked` too if you use Markdown fences.
17
+ Node.js 24+ is required.
14
18
 
15
19
  ## Quick start
16
20
 
17
21
  ```ts
18
- import { parseSchematic, parseSchematicFence, renderSchematic } from "@schemd/core";
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 diagram = parseSchematic(`
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
- For Markdown, use `schematicMarkedExtension()` from the main package or the `@schemd/core/marked` subpath.
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;
@@ -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. The Marked integration imports
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 { schematicMarkedExtension } from './marked-extension.js';
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 SchematicMarkedOptions, 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';
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 { schematicMarkedExtension } from './marked-extension.js';
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[]): 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
  }
package/dist/limits.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  /**
2
- * Hard compiler budgets shared by the parser, renderer, and Marked integration.
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
  */
@@ -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,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 { CLASSICAL_GATE_KINDS, SCHEMD_OUTPUT_MODES, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
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
- function validXmlCodePoint(codePoint) {
11
- if (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d)
12
- return true;
13
- if (codePoint >= 0x20 && codePoint <= 0xd7ff)
14
- return true;
15
- if (codePoint >= 0xe000 && codePoint <= 0xfffd)
16
- return true;
17
- return codePoint >= 0x10000 && codePoint <= 0x10ffff;
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('&', '&amp;')
30
- .replaceAll('<', '&lt;')
31
- .replaceAll('>', '&gt;')
32
- .replaceAll('"', '&quot;')
33
- .replaceAll("'", '&#39;');
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">${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>`;
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">«${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>`;
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 hooks = mode === 'full';
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 || hooks) {
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 = hooks ? componentPortMarkup(component) : '';
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 = routeConnections(document.connections, components, normalized.bounds);
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 = hooks ? HOOK_SVG_STYLES : '';
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 (hooks) {
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
- /** Configuration accepted by the type-only Marked extension factory. */
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 += '&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.1.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/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
  ".": {
@@ -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;
@@ -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('&', '&amp;')
19
- .replaceAll('<', '&lt;')
20
- .replaceAll('>', '&gt;')
21
- .replaceAll('"', '&quot;')
22
- .replaceAll("'", '&#39;');
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
- }