@schemd/core 0.1.1 → 0.2.0

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/dist/renderer.js CHANGED
@@ -1,12 +1,22 @@
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, SchematicSyntaxError } from './types.js';
5
- import { renderMathLabelTspans } from './math-label.js';
4
+ import { CLASSICAL_GATE_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
5
+ import { mathLabelTextWidth, renderMathLabelTspans } from './math-label.js';
6
6
  export const MAX_SVG_OUTPUT_BYTES = MAX_SCHEMATIC_SVG_OUTPUT_BYTES;
7
- 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{fill:currentColor}.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}';
7
+ 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
8
  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
9
  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
+ const NODE_HOOK = 1;
11
+ const PORT_HOOK = 2;
12
+ const WIRE_HOOK = 4;
13
+ function semanticHookBit(hook) {
14
+ if (hook === 'nodes')
15
+ return NODE_HOOK;
16
+ if (hook === 'ports')
17
+ return PORT_HOOK;
18
+ return WIRE_HOOK;
19
+ }
10
20
  function validXmlCodePoint(codePoint) {
11
21
  if (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d)
12
22
  return true;
@@ -82,15 +92,30 @@ function normalizeCompileOptions(value) {
82
92
  throw new SchematicSyntaxError('Render mode must be one of: default, embedded-css, or full.');
83
93
  }
84
94
  const normalizedMode = (mode ?? 'default');
95
+ const rawSemanticHooks = candidate.semanticHooks;
96
+ let semanticHookMask = NODE_HOOK | PORT_HOOK | WIRE_HOOK;
97
+ if (rawSemanticHooks !== undefined) {
98
+ if (!Array.isArray(rawSemanticHooks)) {
99
+ throw new SchematicSyntaxError('Render semanticHooks must be an array.');
100
+ }
101
+ semanticHookMask = 0;
102
+ for (const hook of rawSemanticHooks) {
103
+ if (typeof hook !== 'string' ||
104
+ !SCHEMD_SEMANTIC_HOOKS.includes(hook)) {
105
+ throw new SchematicSyntaxError('Render semanticHooks may contain only: nodes, ports, or wires.');
106
+ }
107
+ semanticHookMask |= semanticHookBit(hook);
108
+ }
109
+ }
85
110
  return idPrefix === undefined
86
- ? { bounds: { width, height }, title, mode: normalizedMode }
87
- : { bounds: { width, height }, title, idPrefix, mode: normalizedMode };
111
+ ? { bounds: { width, height }, title, mode: normalizedMode, semanticHookMask }
112
+ : { bounds: { width, height }, title, idPrefix, mode: normalizedMode, semanticHookMask };
88
113
  }
89
114
  function svgNumber(value) {
90
115
  return String(Number(value.toFixed(3)));
91
116
  }
92
117
  function fittedTextLength(value, maximum, advance) {
93
- return Math.max(1, Math.min(Array.from(value).length * advance, maximum));
118
+ return Math.max(1, Math.min(Math.ceil(mathLabelTextWidth(value, advance)), maximum));
94
119
  }
95
120
  function stableHash(value) {
96
121
  let hash = 2166136261;
@@ -103,6 +128,9 @@ function stableHash(value) {
103
128
  function isClassicalGate(component) {
104
129
  return CLASSICAL_GATE_KINDS.includes(component.kind);
105
130
  }
131
+ function isUmlComponent(component) {
132
+ return UML_COMPONENT_KINDS.includes(component.kind);
133
+ }
106
134
  function colorAttributes(color, extraClass = '') {
107
135
  const suffix = extraClass === '' ? '' : ` ${extraClass}`;
108
136
  const fill = extraClass.split(' ').includes('schematic-node-fill') ? ' fill="currentColor"' : '';
@@ -183,7 +211,7 @@ function iecGate(component, paint) {
183
211
  xor: '=1',
184
212
  not: '1'
185
213
  };
186
- return `<rect ${paint} x="-32" y="${-height / 2}" width="64" height="${height}" rx="2" /><text class="schematic-gate-symbol" fill="currentColor" stroke="none" x="0" y="4">${symbol[component.kind]}</text>${inversionBubbles(component, paint)}${gateStubs(component, paint)}`;
214
+ return `<rect ${paint} x="-32" y="${-height / 2}" width="64" height="${height}" rx="2" /><text class="schematic-gate-symbol" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="14">${symbol[component.kind]}</text>${inversionBubbles(component, paint)}${gateStubs(component, paint)}`;
187
215
  }
188
216
  function ieeeGate(component, paint) {
189
217
  const height = classicalGateHeight(component);
@@ -205,9 +233,9 @@ function ieeeGate(component, paint) {
205
233
  function quantumText(component) {
206
234
  const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined && value !== '');
207
235
  const rows = [component.label, ...details];
208
- const start = -((rows.length - 1) * 10) / 2 + 3;
236
+ const start = -((rows.length - 1) * 12) / 2 + 4;
209
237
  return rows
210
- .map((value, index) => `<text class="${index === 0 ? 'schematic-gate-symbol' : 'schematic-quantum-detail'}" fill="currentColor" stroke="none" x="0" y="${start + index * 10}" textLength="${fittedTextLength(value, 56, index === 0 ? 7 : 5)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(value)}</text>`)
238
+ .map((value, index) => `<text class="${index === 0 ? 'schematic-gate-symbol' : 'schematic-quantum-detail'}" fill="currentColor" stroke="none" x="0" y="${start + index * 12}" text-anchor="middle" font-size="${index === 0 ? 14 : 10}" textLength="${fittedTextLength(value, 56, index === 0 ? 7 : 5)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(value)}</text>`)
211
239
  .join('');
212
240
  }
213
241
  function diodeShape(component, paint) {
@@ -261,29 +289,94 @@ function icPinMarkup(component, side, paint) {
261
289
  const horizontalTextLength = fittedTextLength(pin, Math.max(8, component.bodyWidth / 2 - 10), 4);
262
290
  const verticalTextLength = fittedTextLength(pin, Math.max(8, component.bodyHeight / 2 - 10), 4);
263
291
  if (side === 'left') {
264
- return `<path ${paint} d="M ${x} ${y} H ${-component.bodyWidth / 2}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${-component.bodyWidth / 2 + 5}" y="${y + 3}" text-anchor="start" textLength="${horizontalTextLength}" lengthAdjust="spacingAndGlyphs">${escapeXml(pin)}</text>`;
292
+ return `<path ${paint} d="M ${x} ${y} H ${-component.bodyWidth / 2}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${-component.bodyWidth / 2 + 5}" y="${y + 3}" text-anchor="start" font-size="10" textLength="${horizontalTextLength}" lengthAdjust="spacingAndGlyphs">${escapeXml(pin)}</text>`;
265
293
  }
266
294
  if (side === 'right') {
267
- return `<path ${paint} d="M ${component.bodyWidth / 2} ${y} H ${x}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${component.bodyWidth / 2 - 5}" y="${y + 3}" text-anchor="end" textLength="${horizontalTextLength}" lengthAdjust="spacingAndGlyphs">${escapeXml(pin)}</text>`;
295
+ return `<path ${paint} d="M ${component.bodyWidth / 2} ${y} H ${x}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${component.bodyWidth / 2 - 5}" y="${y + 3}" text-anchor="end" font-size="10" textLength="${horizontalTextLength}" lengthAdjust="spacingAndGlyphs">${escapeXml(pin)}</text>`;
268
296
  }
269
297
  if (side === 'top') {
270
298
  const labelY = -component.bodyHeight / 2 + 5;
271
- return `<path ${paint} d="M ${x} ${y} V ${-component.bodyHeight / 2}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${x}" y="${labelY}" text-anchor="start" textLength="${verticalTextLength}" lengthAdjust="spacingAndGlyphs" transform="rotate(90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
299
+ return `<path ${paint} d="M ${x} ${y} V ${-component.bodyHeight / 2}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${x}" y="${labelY}" text-anchor="start" font-size="10" textLength="${verticalTextLength}" lengthAdjust="spacingAndGlyphs" transform="rotate(90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
272
300
  }
273
301
  const labelY = component.bodyHeight / 2 - 5;
274
- return `<path ${paint} d="M ${x} ${component.bodyHeight / 2} V ${y}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${x}" y="${labelY}" text-anchor="start" textLength="${verticalTextLength}" lengthAdjust="spacingAndGlyphs" transform="rotate(-90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
302
+ return `<path ${paint} d="M ${x} ${component.bodyHeight / 2} V ${y}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${x}" y="${labelY}" text-anchor="start" font-size="10" textLength="${verticalTextLength}" lengthAdjust="spacingAndGlyphs" transform="rotate(-90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
275
303
  })
276
304
  .join('');
277
305
  }
278
306
  function integratedCircuitShape(component, paint) {
279
307
  const left = -component.bodyWidth / 2;
280
308
  const top = -component.bodyHeight / 2;
281
- 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" textLength="${fittedTextLength(component.label, component.bodyWidth - 12, 7)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(component.label)}</text>`;
309
+ 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
+ }
311
+ 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>`;
313
+ }
314
+ function umlClassShape(component, paint) {
315
+ const left = -component.bodyWidth / 2;
316
+ const top = -component.bodyHeight / 2;
317
+ const headerHeight = 36 + (component.stereotype === undefined ? 0 : 14);
318
+ const attributeHeight = Math.max(24, component.attributes.length * 16 + 8);
319
+ const attributeSeparator = top + headerHeight;
320
+ const operationSeparator = attributeSeparator + attributeHeight;
321
+ const stereotype = component.stereotype === undefined
322
+ ? ''
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>`;
324
+ const nameY = top + (component.stereotype === undefined ? 23 : 32);
325
+ const attributes = component.attributes
326
+ .map((row, index) => umlRow(row, left + 8, attributeSeparator + 17 + index * 16))
327
+ .join('');
328
+ const operations = component.operations
329
+ .map((row, index) => umlRow(row, left + 8, operationSeparator + 17 + index * 16))
330
+ .join('');
331
+ return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" rx="2" /><path ${paint} d="M ${svgNumber(left)} ${svgNumber(attributeSeparator)} H ${svgNumber(-left)} M ${svgNumber(left)} ${svgNumber(operationSeparator)} H ${svgNumber(-left)}" />${stereotype}<text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="${svgNumber(nameY)}" text-anchor="middle" font-size="13" font-weight="600">${renderMathLabelTspans(component.label)}</text>${attributes}${operations}`;
332
+ }
333
+ function umlStateShape(component, paint) {
334
+ const left = -component.bodyWidth / 2;
335
+ const top = -component.bodyHeight / 2;
336
+ const separator = top + 32;
337
+ const rows = component.details
338
+ .map((row, index) => umlRow(row, left + 8, separator + 17 + index * 16))
339
+ .join('');
340
+ return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" rx="10" />${component.details.length === 0 ? '' : `<path ${paint} d="M ${svgNumber(left)} ${svgNumber(separator)} H ${svgNumber(-left)}" />`}<text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="${svgNumber(top + 21)}" text-anchor="middle" font-size="13" font-weight="600">${renderMathLabelTspans(component.label)}</text>${rows}`;
341
+ }
342
+ function umlComponentShape(component, paint, nodePaint) {
343
+ if (component.kind === 'class')
344
+ return umlClassShape(component, paint);
345
+ if (component.kind === 'state')
346
+ return umlStateShape(component, paint);
347
+ switch (component.kind) {
348
+ case 'actor':
349
+ return `<circle ${paint} cx="0" cy="-28" r="9" /><path ${paint} d="M 0 -19 V 12 M -20 -5 H 20 M 0 12 L -17 37 M 0 12 L 17 37" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="49" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
350
+ case 'usecase':
351
+ return `<ellipse ${paint} cx="0" cy="0" rx="${svgNumber(component.bodyWidth / 2)}" ry="${svgNumber(component.bodyHeight / 2)}" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="13">${renderMathLabelTspans(component.label)}</text>`;
352
+ case 'lifeline': {
353
+ const top = -component.bodyHeight / 2;
354
+ const left = -component.bodyWidth / 2;
355
+ return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="32" rx="2" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="${svgNumber(top + 21)}" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text><path ${paint} stroke-dasharray="6 5" d="M 0 ${svgNumber(top + 32)} V ${svgNumber(component.bodyHeight / 2)}" />`;
356
+ }
357
+ case 'note': {
358
+ const left = -component.bodyWidth / 2;
359
+ const top = -component.bodyHeight / 2;
360
+ const right = component.bodyWidth / 2;
361
+ return `<path ${paint} d="M ${svgNumber(left)} ${svgNumber(top)} H ${svgNumber(right - 14)} L ${svgNumber(right)} ${svgNumber(top + 14)} V ${svgNumber(-top)} H ${svgNumber(left)} Z M ${svgNumber(right - 14)} ${svgNumber(top)} V ${svgNumber(top + 14)} H ${svgNumber(right)}" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
362
+ }
363
+ case 'package': {
364
+ const left = -component.bodyWidth / 2;
365
+ const top = -component.bodyHeight / 2;
366
+ return `<path ${paint} d="M ${svgNumber(left)} ${svgNumber(top + 12)} V ${svgNumber(-top)} H ${svgNumber(-left)} V ${svgNumber(top)} H ${svgNumber(left + Math.min(54, component.bodyWidth / 2))} L ${svgNumber(left + Math.min(64, component.bodyWidth / 2 + 10))} ${svgNumber(top + 12)} Z" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="13">${renderMathLabelTspans(component.label)}</text>`;
367
+ }
368
+ case 'initial':
369
+ return `<circle ${nodePaint} cx="0" cy="0" r="10" />`;
370
+ case 'final':
371
+ return `<circle ${paint} cx="0" cy="0" r="11" /><circle ${nodePaint} cx="0" cy="0" r="6" />`;
372
+ }
282
373
  }
283
374
  function componentShape(component, paint = colorAttributes(component.color), nodePaint = colorAttributes(component.color, 'schematic-node-fill')) {
284
375
  if (isClassicalGate(component)) {
285
376
  return component.standard === 'iec' ? iecGate(component, paint) : ieeeGate(component, paint);
286
377
  }
378
+ if (isUmlComponent(component))
379
+ return umlComponentShape(component, paint, nodePaint);
287
380
  switch (component.kind) {
288
381
  case 'resistor':
289
382
  return `<path ${paint} d="M -42 0 H -26 L -20 -12 L -10 12 L 0 -12 L 10 12 L 20 -12 L 26 0 H 42" />`;
@@ -310,7 +403,10 @@ function componentShape(component, paint = colorAttributes(component.color), nod
310
403
  }
311
404
  }
312
405
  function reusableSymbolKey(component) {
313
- if (isClassicalGate(component) || component.kind === 'qgate' || component.kind === 'ic') {
406
+ if (isClassicalGate(component) ||
407
+ isUmlComponent(component) ||
408
+ component.kind === 'qgate' ||
409
+ component.kind === 'ic') {
314
410
  return undefined;
315
411
  }
316
412
  if (component.kind === 'diode')
@@ -332,16 +428,56 @@ function connectionMarkup(connection, routed, index, idPrefix, glowId) {
332
428
  const target = escapeXml(`${connection.to.componentId}.${connection.to.port}`);
333
429
  const traceId = `${idPrefix}-wire-${index}-vector`;
334
430
  const dataAttributes = ` data-wire-source="${source}" data-wire-target="${target}"`;
335
- const accessibility = ` tabindex="0" role="group" aria-label="Signal from ${source} to ${target}"`;
431
+ const relation = connection.relation ?? 'signal';
432
+ const accessibility = ` tabindex="0" role="group" aria-label="${escapeXml(relation)} from ${source} to ${target}"`;
336
433
  const vectorId = ` id="${traceId}"`;
337
434
  const markerAttributes = connectionMarkerAttributes(connection, idPrefix);
338
435
  const glow = `<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />`;
339
- return `<g class="schematic-wire"${dataAttributes}${accessibility}><path${vectorId} ${colorAttributes(connection.color, 'schematic-trace')} d="${routed.d}"${markerAttributes} />${glow}<circle ${colorAttributes(connection.color, 'schematic-node-fill')} cx="${svgNumber(end.x)}" cy="${svgNumber(end.y)}" r="3" /></g>`;
436
+ const endpoint = relation === 'signal' && connection.markerEnd === 'none'
437
+ ? `<circle ${colorAttributes(connection.color, 'schematic-node-fill')} cx="${svgNumber(end.x)}" cy="${svgNumber(end.y)}" r="3" />`
438
+ : '';
439
+ return `<g class="schematic-wire"${dataAttributes}${accessibility}><path${vectorId} ${colorAttributes(connection.color, 'schematic-trace')} d="${routed.d}"${markerAttributes} />${glow}${endpoint}${connectionLabelMarkup(connection, routed)}</g>`;
340
440
  }
341
441
  function connectionMarkerAttributes(connection, idPrefix) {
342
442
  const start = connection.markerStart;
343
443
  const end = connection.markerEnd;
344
- return `${start === 'none' ? '' : ` marker-start="url(#${idPrefix}-marker-${start})"`}${end === 'none' ? '' : ` marker-end="url(#${idPrefix}-marker-${end})"`}`;
444
+ return `${start === 'none' ? '' : ` marker-start="url(#${idPrefix}-marker-${start})"`}${end === 'none' ? '' : ` marker-end="url(#${idPrefix}-marker-${end})"`}${connection.dashed === true ? ' stroke-dasharray="7 5"' : ''}`;
445
+ }
446
+ function connectionLabelPoint(route) {
447
+ if (route.curve === 'bezier' && route.points.length >= 4) {
448
+ const a = route.points[0];
449
+ const b = route.points[1];
450
+ const c = route.points[2];
451
+ const d = route.points[3];
452
+ return {
453
+ x: (a.x + 3 * b.x + 3 * c.x + d.x) / 8,
454
+ y: (a.y + 3 * b.y + 3 * c.y + d.y) / 8
455
+ };
456
+ }
457
+ let total = 0;
458
+ for (let index = 1; index < route.points.length; index += 1) {
459
+ total +=
460
+ Math.abs(route.points[index].x - route.points[index - 1].x) +
461
+ Math.abs(route.points[index].y - route.points[index - 1].y);
462
+ }
463
+ let remaining = total / 2;
464
+ for (let index = 1; index < route.points.length; index += 1) {
465
+ const start = route.points[index - 1];
466
+ const end = route.points[index];
467
+ const length = Math.abs(end.x - start.x) + Math.abs(end.y - start.y);
468
+ if (remaining <= length) {
469
+ const ratio = length === 0 ? 0 : remaining / length;
470
+ return { x: start.x + (end.x - start.x) * ratio, y: start.y + (end.y - start.y) * ratio };
471
+ }
472
+ remaining -= length;
473
+ }
474
+ return route.points.at(-1);
475
+ }
476
+ function connectionLabelMarkup(connection, route) {
477
+ if (connection.label === undefined)
478
+ return '';
479
+ const point = connectionLabelPoint(route);
480
+ return `<text class="schematic-connection-label" fill="currentColor" stroke="none" x="${svgNumber(point.x)}" y="${svgNumber(point.y - 7)}" text-anchor="middle" font-size="11">${renderMathLabelTspans(connection.label)}</text>`;
345
481
  }
346
482
  function colorKey(color) {
347
483
  return `${color.kind}:${color.value}`;
@@ -353,23 +489,29 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
353
489
  const key = `${colorKey(connection.color)}|${markerAttributes}`;
354
490
  let batch = batches.get(key);
355
491
  if (batch === undefined) {
356
- batch = { color: connection.color, markerAttributes, traces: [], endpoints: [] };
492
+ batch = { color: connection.color, markerAttributes, traces: [], endpoints: [], labels: [] };
357
493
  batches.set(key, batch);
358
494
  }
359
495
  const routed = routes[index];
360
496
  const end = routed.points.at(-1);
361
497
  batch.traces.push(routed.d);
362
- batch.endpoints.push(`M ${svgNumber(end.x - 3)} ${svgNumber(end.y)} a 3 3 0 1 0 6 0 a 3 3 0 1 0 -6 0`);
498
+ if ((connection.relation ?? 'signal') === 'signal' && connection.markerEnd === 'none') {
499
+ batch.endpoints.push(`M ${svgNumber(end.x - 3)} ${svgNumber(end.y)} a 3 3 0 1 0 6 0 a 3 3 0 1 0 -6 0`);
500
+ }
501
+ const label = connectionLabelMarkup(connection, routed);
502
+ if (label !== '')
503
+ batch.labels.push(label);
363
504
  }
364
505
  return Array.from(batches.values(), (batch, index) => {
365
506
  const tracePaint = colorAttributes(batch.color, 'schematic-trace');
366
507
  const nodePaint = colorAttributes(batch.color, 'schematic-node-fill');
367
508
  const traceId = `${idPrefix}-wire-batch-${index}-vector`;
368
509
  const trace = `<path${embedVisuals ? ` id="${traceId}"` : ''} ${tracePaint} d="${batch.traces.join(' ')}"${batch.markerAttributes} />`;
369
- const endpoints = `<path ${nodePaint} d="${batch.endpoints.join(' ')}" />`;
510
+ const endpoints = batch.endpoints.length === 0 ? '' : `<path ${nodePaint} d="${batch.endpoints.join(' ')}" />`;
511
+ const labels = batch.labels.join('');
370
512
  if (!embedVisuals)
371
- return trace + endpoints;
372
- return `<g class="schematic-wire" tabindex="0" role="group" aria-label="${batch.traces.length} grouped signal trace${batch.traces.length === 1 ? '' : 's'}">${trace}<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />${endpoints}</g>`;
513
+ return trace + endpoints + labels;
514
+ return `<g class="schematic-wire" tabindex="0" role="group" aria-label="${batch.traces.length} grouped connection${batch.traces.length === 1 ? '' : 's'}">${trace}<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />${endpoints}${labels}</g>`;
373
515
  }).join('');
374
516
  }
375
517
  function componentMetadata(component) {
@@ -399,19 +541,18 @@ function componentPortMarkup(component) {
399
541
  })
400
542
  .join('');
401
543
  }
402
- function componentMarkup(component, index, idPrefix, glowId, mode, symbolId) {
544
+ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, portHooks, symbolId) {
403
545
  const label = escapeXml(component.label);
404
546
  const renderedLabel = renderMathLabelTspans(component.label);
405
547
  const anchors = componentTextAnchors(component);
406
548
  const id = escapeXml(component.id);
407
549
  const vectorId = `${idPrefix}-node-${index}-vector`;
408
550
  const styles = mode === 'embedded-css' || mode === 'full';
409
- const hooks = mode === 'full';
410
- const dataAttributes = hooks
551
+ const dataAttributes = nodeHooks
411
552
  ? ` data-node-id="${id}" data-node-kind="${component.kind}" data-node-label="${label}" data-component="${id}" data-kind="${component.kind}"`
412
553
  : '';
413
554
  let accessibility = '';
414
- if (styles || hooks) {
555
+ if (styles) {
415
556
  const metadata = componentMetadata(component);
416
557
  const ariaLabel = escapeXml(`${component.id}, ${component.kind}, ${component.label}${metadata === '' ? '' : `, ${metadata}`}`);
417
558
  accessibility = ` tabindex="0" role="group" aria-label="${ariaLabel}"`;
@@ -420,16 +561,21 @@ function componentMarkup(component, index, idPrefix, glowId, mode, symbolId) {
420
561
  const glow = styles
421
562
  ? `<use class="schematic-glow-layer" href="#${vectorId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />`
422
563
  : '';
423
- const hotspots = hooks ? componentPortMarkup(component) : '';
564
+ const hotspots = portHooks ? componentPortMarkup(component) : '';
424
565
  const vector = symbolId === undefined
425
566
  ? `<g${vectorIdAttribute} class="schematic-component-vector">${componentShape(component)}</g>`
426
567
  : `<use${vectorIdAttribute} ${colorAttributes(component.color, 'schematic-component-vector')} href="#${symbolId}" />`;
427
- return `<g class="schematic-component"${dataAttributes} transform="translate(${svgNumber(component.x)} ${svgNumber(component.y)})"${accessibility}>${vector}${glow}<text class="schematic-designator" fill="currentColor" stroke="none" x="0" y="${anchors.designatorY}" text-anchor="middle" textLength="${anchors.designatorWidth}" lengthAdjust="spacingAndGlyphs">${id}</text><text class="schematic-label" fill="currentColor" stroke="none" x="0" y="${anchors.labelY}" text-anchor="middle" textLength="${anchors.labelWidth}" lengthAdjust="spacingAndGlyphs">${renderedLabel}</text>${hotspots}</g>`;
568
+ const externalLabels = isUmlComponent(component)
569
+ ? ''
570
+ : `<text class="schematic-designator" fill="currentColor" stroke="none" x="0" y="${anchors.designatorY}" text-anchor="middle" font-size="12" textLength="${anchors.designatorWidth}" lengthAdjust="spacingAndGlyphs">${id}</text><text class="schematic-label" fill="currentColor" stroke="none" x="0" y="${anchors.labelY}" text-anchor="middle" font-size="12" textLength="${anchors.labelWidth}" lengthAdjust="spacingAndGlyphs">${renderedLabel}</text>`;
571
+ return `<g class="schematic-component"${dataAttributes} transform="translate(${svgNumber(component.x)} ${svgNumber(component.y)})"${accessibility}>${vector}${glow}${externalLabels}${hotspots}</g>`;
428
572
  }
429
573
  export function renderSchematic(document, options) {
430
574
  assertParsedSchematicDocument(document);
431
575
  const normalized = normalizeCompileOptions(options);
432
- validateDocumentGeometry(document, normalized);
576
+ const components = new Map(document.components.map((component) => [component.id, component]));
577
+ const routedConnections = routeConnections(document.connections, components, normalized.bounds);
578
+ validateDocumentGeometry(document, normalized, routedConnections);
433
579
  const signature = `${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`;
434
580
  const candidatePrefix = (normalized.idPrefix ?? `schematic-${stableHash(signature)}`).replace(/[^A-Za-z0-9_-]/g, '-');
435
581
  const safePrefix = /[A-Za-z0-9]/.test(candidatePrefix)
@@ -441,8 +587,9 @@ export function renderSchematic(document, options) {
441
587
  const glowId = `${safePrefix}-schematic-glow-filter`;
442
588
  const styles = normalized.mode === 'embedded-css' || normalized.mode === 'full';
443
589
  const hooks = normalized.mode === 'full';
444
- const components = new Map(document.components.map((component) => [component.id, component]));
445
- const routedConnections = routeConnections(document.connections, components);
590
+ const nodeHooks = hooks && (normalized.semanticHookMask & NODE_HOOK) !== 0;
591
+ const portHooks = hooks && (normalized.semanticHookMask & PORT_HOOK) !== 0;
592
+ const wireHooks = hooks && (normalized.semanticHookMask & WIRE_HOOK) !== 0;
446
593
  const reusableSymbols = new Map();
447
594
  for (const component of document.components) {
448
595
  const key = reusableSymbolKey(component);
@@ -452,22 +599,22 @@ export function renderSchematic(document, options) {
452
599
  }
453
600
  const title = escapeXml(normalized.title);
454
601
  const componentCount = document.components.length;
455
- const description = `${componentCount} component${componentCount === 1 ? '' : 's'} and ${document.connections.length} signal connection${document.connections.length === 1 ? '' : 's'}.`;
602
+ const description = `${componentCount} component${componentCount === 1 ? '' : 's'} and ${document.connections.length} connection${document.connections.length === 1 ? '' : 's'}.`;
456
603
  const writer = new BoundedSvgWriter();
457
- const hookStyles = hooks ? HOOK_SVG_STYLES : '';
604
+ const hookStyles = portHooks ? HOOK_SVG_STYLES : '';
458
605
  const interactionStyles = styles ? INTERACTIVE_SVG_STYLES : '';
459
606
  const glowFilter = styles
460
607
  ? `<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>`
461
608
  : '';
462
609
  const symbolDefinitions = Array.from(reusableSymbols.values(), ({ id, component }) => reusableSymbolDefinition(component, id)).join('');
463
610
  const usedMarkers = new Set(document.connections.flatMap((connection) => [connection.markerStart, connection.markerEnd]));
464
- const markerDefinitions = `${usedMarkers.has('arrow') ? `<marker id="${safePrefix}-marker-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 8 4 0 8Z" fill="context-stroke" /></marker>` : ''}${usedMarkers.has('dot') ? `<marker id="${safePrefix}-marker-dot" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="5" markerHeight="5"><circle cx="4" cy="4" r="3" fill="context-stroke" /></marker>` : ''}`;
611
+ const markerDefinitions = `${usedMarkers.has('arrow') ? `<marker id="${safePrefix}-marker-arrow" viewBox="0 0 8 8" refX="8" refY="4" markerWidth="8" markerHeight="8" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 0 8 4 0 8Z" fill="context-stroke" /></marker>` : ''}${usedMarkers.has('open-arrow') ? `<marker id="${safePrefix}-marker-open-arrow" viewBox="0 0 9 10" refX="9" refY="5" markerWidth="9" markerHeight="10" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 0 9 5 0 10" fill="none" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('dot') ? `<marker id="${safePrefix}-marker-dot" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="8" markerHeight="8" markerUnits="userSpaceOnUse"><circle cx="4" cy="4" r="3" fill="context-stroke" /></marker>` : ''}${usedMarkers.has('triangle') ? `<marker id="${safePrefix}-marker-triangle" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="12" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 1 11 6 0 11Z" fill="var(--schematic-surface,#fff)" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('diamond') ? `<marker id="${safePrefix}-marker-diamond" viewBox="0 0 13 12" refX="12" refY="6" markerWidth="13" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 6 6 1 12 6 6 11Z" fill="var(--schematic-surface,#fff)" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('diamond-filled') ? `<marker id="${safePrefix}-marker-diamond-filled" viewBox="0 0 13 12" refX="12" refY="6" markerWidth="13" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 6 6 1 12 6 6 11Z" fill="context-stroke" stroke="context-stroke" /></marker>` : ''}`;
465
612
  const embeddedDefinitions = styles
466
613
  ? `<style>${STATIC_SVG_STYLES}${hookStyles}${interactionStyles}</style><pattern id="${gridId}" width="20" height="20" patternUnits="userSpaceOnUse"><path class="schematic-grid-line" d="M20 0H0V20" /></pattern>${glowFilter}`
467
614
  : '';
468
615
  const definitions = `${embeddedDefinitions}${markerDefinitions}${symbolDefinitions}`;
469
616
  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">`);
470
- if (hooks) {
617
+ if (wireHooks) {
471
618
  for (const [index, connection] of document.connections.entries()) {
472
619
  writer.append(connectionMarkup(connection, routedConnections[index], index, safePrefix, glowId));
473
620
  }
@@ -478,7 +625,7 @@ export function renderSchematic(document, options) {
478
625
  for (const [index, component] of document.components.entries()) {
479
626
  const key = reusableSymbolKey(component);
480
627
  const symbolId = key === undefined ? undefined : reusableSymbols.get(key)?.id;
481
- writer.append(componentMarkup(component, index, safePrefix, glowId, normalized.mode, symbolId));
628
+ writer.append(componentMarkup(component, index, safePrefix, glowId, normalized.mode, nodeHooks, portHooks, symbolId));
482
629
  }
483
630
  writer.append(`</g></svg><figcaption>${title}</figcaption></figure>`);
484
631
  return writer.finish();
package/dist/types.d.ts CHANGED
@@ -35,8 +35,12 @@ export type ClassicalGateKind = (typeof CLASSICAL_GATE_KINDS)[number];
35
35
  export declare const QUANTUM_GATE_KINDS: readonly ["hadamard", "cnot", "qgate"];
36
36
  /** A supported native or polymorphic quantum gate keyword. */
37
37
  export type QuantumGateKind = (typeof QUANTUM_GATE_KINDS)[number];
38
+ /** First-class UML node keywords spanning structural and behavioral diagrams. */
39
+ export declare const UML_COMPONENT_KINDS: readonly ["class", "actor", "usecase", "state", "lifeline", "note", "package", "initial", "final"];
40
+ /** A supported UML node keyword. */
41
+ export type UmlComponentKind = (typeof UML_COMPONENT_KINDS)[number];
38
42
  /** Complete, collision-free component keyword registry. */
39
- export declare const COMPONENT_KINDS: readonly ["resistor", "capacitor", "inductor", "diode", "transistor", "port", "ground", "nand", "nor", "xor", "and", "or", "not", "hadamard", "cnot", "qgate", "ic"];
43
+ export declare const COMPONENT_KINDS: readonly ["resistor", "capacitor", "inductor", "diode", "transistor", "port", "ground", "nand", "nor", "xor", "and", "or", "not", "hadamard", "cnot", "qgate", "class", "actor", "usecase", "state", "lifeline", "note", "package", "initial", "final", "ic"];
40
44
  /** Any component keyword that can begin a declaration. */
41
45
  export type ComponentKind = (typeof COMPONENT_KINDS)[number];
42
46
  /** Built-in semantic color tokens that hosts can theme through CSS. */
@@ -169,8 +173,53 @@ export interface IcComponent extends ComponentBase {
169
173
  }
170
174
  /** Descriptive alias for a parsed integrated-circuit block. */
171
175
  export type IntegratedCircuitComponent = IcComponent;
176
+ /** Parsed UML class with independently sized attribute and operation compartments. */
177
+ export interface UmlClassComponent extends ComponentBase {
178
+ /** UML class node discriminant. */
179
+ kind: 'class';
180
+ /** Optional stereotype displayed above the class name. */
181
+ stereotype?: string;
182
+ /** Attribute declarations in source order. */
183
+ attributes: readonly string[];
184
+ /** Operation declarations in source order. */
185
+ operations: readonly string[];
186
+ /** Deterministic box width derived from all visible rows. */
187
+ bodyWidth: number;
188
+ /** Deterministic three-compartment box height. */
189
+ bodyHeight: number;
190
+ }
191
+ /** Parsed UML state with optional behavior rows. */
192
+ export interface UmlStateComponent extends ComponentBase {
193
+ kind: 'state';
194
+ /** Entry, do, exit, or author-defined behavior rows. */
195
+ details: readonly string[];
196
+ bodyWidth: number;
197
+ bodyHeight: number;
198
+ }
199
+ /** Parsed UML ellipse, note, package, or sequence lifeline. */
200
+ export interface UmlSizedComponent extends ComponentBase {
201
+ kind: 'usecase' | 'lifeline' | 'note' | 'package';
202
+ bodyWidth: number;
203
+ bodyHeight: number;
204
+ }
205
+ /** Parsed UML actor figure. */
206
+ export interface UmlActorComponent extends ComponentBase {
207
+ kind: 'actor';
208
+ }
209
+ /** Parsed UML initial or final pseudostate. */
210
+ export interface UmlPseudostateComponent extends ComponentBase {
211
+ kind: 'initial' | 'final';
212
+ }
213
+ /** Every UML component accepted by the compiler. */
214
+ export type UmlComponent = UmlClassComponent | UmlStateComponent | UmlSizedComponent | UmlActorComponent | UmlPseudostateComponent;
172
215
  /** Discriminated union of every component node accepted by the renderer. */
173
- export type SchematicComponent = PassiveComponent | DiodeComponent | TransistorComponent | PortComponent | GroundComponent | ClassicalGateComponent | QuantumGateComponent | IcComponent;
216
+ export type SchematicComponent = PassiveComponent | DiodeComponent | TransistorComponent | PortComponent | GroundComponent | ClassicalGateComponent | QuantumGateComponent | IcComponent | UmlComponent;
217
+ /** UML relationship semantics used to derive line style and endpoint markers. */
218
+ export declare const UML_RELATION_KINDS: readonly ["association", "dependency", "generalization", "realization", "aggregation", "composition", "message", "transition", "include", "extend"];
219
+ /** A supported UML relationship. */
220
+ export type UmlRelationKind = (typeof UML_RELATION_KINDS)[number];
221
+ /** Electrical signal or a first-class UML relationship. */
222
+ export type SchematicRelationKind = 'signal' | UmlRelationKind;
174
223
  /** Address of one component terminal in a connection declaration. */
175
224
  export interface SchematicEndpoint {
176
225
  /** Document-local component identifier. */
@@ -192,6 +241,12 @@ export interface SchematicConnection {
192
241
  markerStart: SchematicSignalMarker;
193
242
  /** Optional marker drawn at the destination terminal. */
194
243
  markerEnd: SchematicSignalMarker;
244
+ /** Electrical or UML relationship semantics. */
245
+ relation?: SchematicRelationKind;
246
+ /** Optional text centered beside the routed connector. */
247
+ label?: string;
248
+ /** Whether the trace uses the UML dependency dash pattern. */
249
+ dashed?: boolean;
195
250
  /** One-based source line used for routing diagnostics. */
196
251
  line: number;
197
252
  }
@@ -206,8 +261,12 @@ export interface SchematicDocument {
206
261
  export declare const SCHEMD_OUTPUT_MODES: readonly ["default", "embedded-css", "full"];
207
262
  /** Static, CSS-enhanced, or fully attributed SVG output mode. */
208
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];
209
268
  /** Marker primitives that can terminate or originate a signal trace. */
210
- export declare const SCHEMATIC_SIGNAL_MARKERS: readonly ["none", "arrow", "dot"];
269
+ export declare const SCHEMATIC_SIGNAL_MARKERS: readonly ["none", "arrow", "open-arrow", "dot", "triangle", "diamond", "diamond-filled"];
211
270
  /** A validated connection marker selection. */
212
271
  export type SchematicSignalMarker = (typeof SCHEMATIC_SIGNAL_MARKERS)[number];
213
272
  /** Per-render options extending the fence's intrinsic layout contract. */
@@ -216,17 +275,8 @@ export interface CompileSchematicOptions extends SchematicFence {
216
275
  idPrefix?: string;
217
276
  /** Markup and interaction budget for the generated SVG. */
218
277
  mode?: SchemdOutputMode;
219
- }
220
- /** Configuration accepted by the type-only Marked extension factory. */
221
- export interface SchematicMarkedOptions {
222
- /** Accessible title used when the fence omits one. */
223
- defaultTitle?: string;
224
- /** Fixed output mode for every schemd fence in a Markdown pass. */
225
- mode?: SchemdOutputMode;
226
- /** Synchronous mode resolver for hosts with request-scoped rendering state. */
227
- resolveMode?: () => SchemdOutputMode;
228
- /** Optional safe fallback renderer for bounded syntax failures. */
229
- onError?: (error: SchematicSyntaxError, source: string) => string;
278
+ /** Full-mode metadata to emit. Defaults to nodes, ports, and wires. */
279
+ semanticHooks?: readonly SchematicSemanticHook[];
230
280
  }
231
281
  /**
232
282
  * Deterministic syntax or geometry failure with optional source location.
package/dist/types.js CHANGED
@@ -5,16 +5,49 @@ export const TRANSISTOR_TYPES = ['npn', 'pnp', 'nmos', 'pmos'];
5
5
  export const GROUND_STYLES = ['chassis', 'earth', 'signal'];
6
6
  export const CLASSICAL_GATE_KINDS = ['nand', 'nor', 'xor', 'and', 'or', 'not'];
7
7
  export const QUANTUM_GATE_KINDS = ['hadamard', 'cnot', 'qgate'];
8
+ export const UML_COMPONENT_KINDS = [
9
+ 'class',
10
+ 'actor',
11
+ 'usecase',
12
+ 'state',
13
+ 'lifeline',
14
+ 'note',
15
+ 'package',
16
+ 'initial',
17
+ 'final'
18
+ ];
8
19
  export const COMPONENT_KINDS = [
9
20
  ...PASSIVE_KINDS,
10
21
  ...ANALOG_KINDS,
11
22
  ...CLASSICAL_GATE_KINDS,
12
23
  ...QUANTUM_GATE_KINDS,
24
+ ...UML_COMPONENT_KINDS,
13
25
  'ic'
14
26
  ];
15
27
  export const SEMANTIC_COLORS = ['amber', 'blue', 'cyan', 'purple', 'slate', 'emerald'];
28
+ export const UML_RELATION_KINDS = [
29
+ 'association',
30
+ 'dependency',
31
+ 'generalization',
32
+ 'realization',
33
+ 'aggregation',
34
+ 'composition',
35
+ 'message',
36
+ 'transition',
37
+ 'include',
38
+ 'extend'
39
+ ];
16
40
  export const SCHEMD_OUTPUT_MODES = ['default', 'embedded-css', 'full'];
17
- export const SCHEMATIC_SIGNAL_MARKERS = ['none', 'arrow', 'dot'];
41
+ export const SCHEMD_SEMANTIC_HOOKS = ['nodes', 'ports', 'wires'];
42
+ export const SCHEMATIC_SIGNAL_MARKERS = [
43
+ 'none',
44
+ 'arrow',
45
+ 'open-arrow',
46
+ 'dot',
47
+ 'triangle',
48
+ 'diamond',
49
+ 'diamond-filled'
50
+ ];
18
51
  export class SchematicSyntaxError extends Error {
19
52
  line;
20
53
  constructor(message, line) {