@schemd/core 0.2.1 → 0.3.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/CHANGELOG.md +64 -0
- package/README.md +84 -32
- package/dist/compiler.d.ts +39 -0
- package/dist/compiler.js +17 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/layout.d.ts +60 -1
- package/dist/layout.js +536 -65
- package/dist/math-label.d.ts +5 -3
- package/dist/math-label.js +14 -28
- package/dist/parser.js +409 -40
- package/dist/renderer.js +377 -45
- package/dist/types.d.ts +124 -19
- package/dist/types.js +167 -4
- package/package.json +83 -78
- package/assets/brand/README.md +0 -6
- package/assets/brand/schemd-logo.svg +0 -29
- package/assets/brand/schemd-mark.svg +0 -23
package/dist/parser.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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';
|
|
1
|
+
import { ADDER_TYPES, AMPLIFIER_TYPES, ANALOG_KINDS, BUFFER_TYPES, BUS_TYPES, CLASSICAL_GATE_KINDS, COMPONENT_KINDS, DIGITAL_COMPONENT_KINDS, DIODE_TYPES, ELECTRICAL_COMPONENT_KINDS, FLIPFLOP_TYPES, GROUND_STYLES, LOAD_TYPES, LOGIC_STATES, METER_TYPES, MUX_TYPES, NAMED_QUANTUM_GATE_KINDS, PASSIVE_KINDS, POWER_TYPES, PROTECTION_TYPES, QUANTUM_GATE_KINDS, QUANTUM_SPECIAL_KINDS, RESONATOR_TYPES, SCHEMATIC_ORIENTATIONS, SCHEMATIC_SIGNAL_KINDS, SEMANTIC_COLORS, SCHEMATIC_SIGNAL_MARKERS, SOURCE_TYPES, SWITCH_TYPES, 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
4
|
import { cacheParsedSchematicRoutes } from './route-cache.js';
|
|
5
5
|
import { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_SOURCE_CHARACTERS } from './limits.js';
|
|
6
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+(.+)$/;
|
|
7
|
-
const CONNECTION_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_
|
|
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+(.+)$/;
|
|
8
8
|
const SCHEMD_FENCE_PATTERN = /^schemd\s+bounds="(\d+)x(\d+)"(?:\s+title="([^"]+)")?\s*$/i;
|
|
9
9
|
const NUMBER_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
|
|
10
10
|
const ANGLE_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:deg|grad|rad|turn)?$/i;
|
|
@@ -32,7 +32,11 @@ function freezeParsedDocument(document) {
|
|
|
32
32
|
Object.freeze(component.pins.bottom);
|
|
33
33
|
Object.freeze(component.pins);
|
|
34
34
|
}
|
|
35
|
-
if (component.kind === 'class'
|
|
35
|
+
if (component.kind === 'class' ||
|
|
36
|
+
component.kind === 'interface' ||
|
|
37
|
+
component.kind === 'enumeration' ||
|
|
38
|
+
component.kind === 'datatype' ||
|
|
39
|
+
component.kind === 'object') {
|
|
36
40
|
Object.freeze(component.attributes);
|
|
37
41
|
Object.freeze(component.operations);
|
|
38
42
|
}
|
|
@@ -275,10 +279,26 @@ function parseEnumOption(value, fallback, values, name, line) {
|
|
|
275
279
|
if (value === undefined)
|
|
276
280
|
return fallback;
|
|
277
281
|
if (!includesValue(values, value)) {
|
|
278
|
-
throw new SchematicSyntaxError(
|
|
282
|
+
throw new SchematicSyntaxError(`Option ${name} must be one of: ${values.join(', ')}.`, line);
|
|
279
283
|
}
|
|
280
284
|
return value;
|
|
281
285
|
}
|
|
286
|
+
function parseOrientation(attributes, line) {
|
|
287
|
+
const value = attributes.get('orientation');
|
|
288
|
+
return value === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
orientation: parseEnumOption(value, 'right', SCHEMATIC_ORIENTATIONS, 'orientation', line)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function parseWidth(value, fallback, line) {
|
|
295
|
+
if (value === undefined)
|
|
296
|
+
return fallback;
|
|
297
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 256) {
|
|
298
|
+
throw new SchematicSyntaxError('width must be an integer from 1 through 256.', line);
|
|
299
|
+
}
|
|
300
|
+
return Number(value);
|
|
301
|
+
}
|
|
282
302
|
function parseIcPins(attributes, line) {
|
|
283
303
|
const registered = new Set();
|
|
284
304
|
const parseSide = (side) => {
|
|
@@ -372,39 +392,81 @@ function parseComponent(match, line) {
|
|
|
372
392
|
const attributes = parseAttributes(tail.options, line);
|
|
373
393
|
const common = commonComponent(match, tail.color === '' ? 'slate' : tail.color, line);
|
|
374
394
|
if (includesValue(PASSIVE_KINDS, kind)) {
|
|
375
|
-
assertOnlyAttributes(attributes, [], line);
|
|
376
|
-
|
|
395
|
+
assertOnlyAttributes(attributes, ['type', 'orientation'], line);
|
|
396
|
+
const allowed = kind === 'resistor'
|
|
397
|
+
? ['fixed', 'variable', 'rheostat', 'potentiometer', 'thermistor', 'ldr']
|
|
398
|
+
: kind === 'capacitor'
|
|
399
|
+
? ['fixed', 'variable', 'polarized']
|
|
400
|
+
: ['fixed', 'coupled', 'transformer'];
|
|
401
|
+
const passiveType = parseEnumOption(attributes.get('type'), 'fixed', allowed, 'type', line);
|
|
402
|
+
return passiveType === 'fixed'
|
|
403
|
+
? { kind, ...common, ...parseOrientation(attributes, line) }
|
|
404
|
+
: { kind, ...common, passiveType, ...parseOrientation(attributes, line) };
|
|
377
405
|
}
|
|
378
406
|
if (includesValue(ANALOG_KINDS, kind)) {
|
|
379
407
|
switch (kind) {
|
|
380
408
|
case 'diode':
|
|
381
|
-
assertOnlyAttributes(attributes, ['type'], line);
|
|
409
|
+
assertOnlyAttributes(attributes, ['type', 'orientation'], line);
|
|
382
410
|
return {
|
|
383
411
|
kind,
|
|
384
412
|
...common,
|
|
385
|
-
diodeType: parseEnumOption(attributes.get('type'), 'standard', DIODE_TYPES, 'type', line)
|
|
413
|
+
diodeType: parseEnumOption(attributes.get('type'), 'standard', DIODE_TYPES, 'type', line),
|
|
414
|
+
...parseOrientation(attributes, line)
|
|
386
415
|
};
|
|
387
416
|
case 'transistor':
|
|
388
|
-
assertOnlyAttributes(attributes, ['type'], line);
|
|
417
|
+
assertOnlyAttributes(attributes, ['type', 'orientation'], line);
|
|
389
418
|
return {
|
|
390
419
|
kind,
|
|
391
420
|
...common,
|
|
392
|
-
transistorType: parseEnumOption(attributes.get('type'), 'npn', TRANSISTOR_TYPES, 'type', line)
|
|
421
|
+
transistorType: parseEnumOption(attributes.get('type'), 'npn', TRANSISTOR_TYPES, 'type', line),
|
|
422
|
+
...parseOrientation(attributes, line)
|
|
393
423
|
};
|
|
394
|
-
case 'port':
|
|
395
|
-
assertOnlyAttributes(attributes, [], line);
|
|
396
|
-
|
|
424
|
+
case 'port': {
|
|
425
|
+
assertOnlyAttributes(attributes, ['width', 'orientation'], line);
|
|
426
|
+
const width = parseWidth(attributes.get('width'), 1, line);
|
|
427
|
+
return width === 1
|
|
428
|
+
? { kind, ...common, ...parseOrientation(attributes, line) }
|
|
429
|
+
: { kind, ...common, width, ...parseOrientation(attributes, line) };
|
|
430
|
+
}
|
|
397
431
|
case 'ground':
|
|
398
|
-
assertOnlyAttributes(attributes, ['style'], line);
|
|
432
|
+
assertOnlyAttributes(attributes, ['style', 'orientation'], line);
|
|
399
433
|
return {
|
|
400
434
|
kind,
|
|
401
435
|
...common,
|
|
402
|
-
groundStyle: parseEnumOption(attributes.get('style'), 'signal', GROUND_STYLES, 'style', line)
|
|
436
|
+
groundStyle: parseEnumOption(attributes.get('style'), 'signal', GROUND_STYLES, 'style', line),
|
|
437
|
+
...parseOrientation(attributes, line)
|
|
403
438
|
};
|
|
404
439
|
}
|
|
405
440
|
}
|
|
441
|
+
if (includesValue(ELECTRICAL_COMPONENT_KINDS, kind)) {
|
|
442
|
+
if (kind === 'junction' || kind === 'testpoint') {
|
|
443
|
+
assertOnlyAttributes(attributes, [], line);
|
|
444
|
+
return { kind, ...common };
|
|
445
|
+
}
|
|
446
|
+
if (kind === 'connector') {
|
|
447
|
+
assertOnlyAttributes(attributes, ['orientation'], line);
|
|
448
|
+
return { kind, ...common, ...parseOrientation(attributes, line) };
|
|
449
|
+
}
|
|
450
|
+
assertOnlyAttributes(attributes, ['type', 'orientation'], line);
|
|
451
|
+
const variant = kind === 'source'
|
|
452
|
+
? parseEnumOption(attributes.get('type'), 'voltage-dc', SOURCE_TYPES, 'type', line)
|
|
453
|
+
: kind === 'power'
|
|
454
|
+
? parseEnumOption(attributes.get('type'), 'vcc', POWER_TYPES, 'type', line)
|
|
455
|
+
: kind === 'switch'
|
|
456
|
+
? parseEnumOption(attributes.get('type'), 'spst', SWITCH_TYPES, 'type', line)
|
|
457
|
+
: kind === 'protection'
|
|
458
|
+
? parseEnumOption(attributes.get('type'), 'fuse', PROTECTION_TYPES, 'type', line)
|
|
459
|
+
: kind === 'amplifier'
|
|
460
|
+
? parseEnumOption(attributes.get('type'), 'opamp', AMPLIFIER_TYPES, 'type', line)
|
|
461
|
+
: kind === 'resonator'
|
|
462
|
+
? parseEnumOption(attributes.get('type'), 'crystal', RESONATOR_TYPES, 'type', line)
|
|
463
|
+
: kind === 'meter'
|
|
464
|
+
? parseEnumOption(attributes.get('type'), 'voltmeter', METER_TYPES, 'type', line)
|
|
465
|
+
: parseEnumOption(attributes.get('type'), 'lamp', LOAD_TYPES, 'type', line);
|
|
466
|
+
return { kind, ...common, variant, ...parseOrientation(attributes, line) };
|
|
467
|
+
}
|
|
406
468
|
if (includesValue(CLASSICAL_GATE_KINDS, kind)) {
|
|
407
|
-
assertOnlyAttributes(attributes, ['inputs', 'outputs', 'standard'], line);
|
|
469
|
+
assertOnlyAttributes(attributes, ['inputs', 'outputs', 'standard', 'orientation'], line);
|
|
408
470
|
const standard = attributes.get('standard') ?? 'ieee';
|
|
409
471
|
if (standard !== 'ieee' && standard !== 'iec') {
|
|
410
472
|
throw new SchematicSyntaxError('standard must be ieee or iec.', line);
|
|
@@ -414,26 +476,93 @@ function parseComponent(match, line) {
|
|
|
414
476
|
...common,
|
|
415
477
|
inputs: parseCount(attributes.get('inputs'), kind === 'not' ? 1 : 2, 'inputs', line),
|
|
416
478
|
outputs: parseCount(attributes.get('outputs'), 1, 'outputs', line),
|
|
417
|
-
standard
|
|
479
|
+
standard,
|
|
480
|
+
...parseOrientation(attributes, line)
|
|
418
481
|
};
|
|
419
482
|
}
|
|
483
|
+
if (includesValue(DIGITAL_COMPONENT_KINDS, kind)) {
|
|
484
|
+
assertOnlyAttributes(attributes, ['type', 'inputs', 'outputs', 'width', 'orientation'], line);
|
|
485
|
+
const defaults = {
|
|
486
|
+
buffer: [1, 1], logic: [0, 1], clock: [0, 1], flipflop: [3, 2], mux: [2, 1],
|
|
487
|
+
encoder: [4, 2], decoder: [2, 4], register: [3, 2], counter: [3, 2], adder: [2, 2],
|
|
488
|
+
comparator: [2, 3], bus: [1, 2]
|
|
489
|
+
};
|
|
490
|
+
const [defaultInputs, defaultOutputs] = defaults[kind];
|
|
491
|
+
let inputs = parseCount(attributes.get('inputs'), defaultInputs === 0 ? 1 : defaultInputs, 'inputs', line);
|
|
492
|
+
let outputs = parseCount(attributes.get('outputs'), defaultOutputs, 'outputs', line);
|
|
493
|
+
if ((kind === 'logic' || kind === 'clock') && attributes.has('inputs')) {
|
|
494
|
+
throw new SchematicSyntaxError(`${kind} does not accept inputs.`, line);
|
|
495
|
+
}
|
|
496
|
+
const width = parseWidth(attributes.get('width'), kind === 'bus' || kind === 'register' ? 8 : 1, line);
|
|
497
|
+
if (attributes.has('width') && kind !== 'bus' && kind !== 'register') {
|
|
498
|
+
throw new SchematicSyntaxError(`Option width is not supported for ${kind}.`, line);
|
|
499
|
+
}
|
|
500
|
+
if (kind === 'bus' && width < 2) {
|
|
501
|
+
throw new SchematicSyntaxError('Bus width must be at least 2.', line);
|
|
502
|
+
}
|
|
503
|
+
const variant = kind === 'buffer'
|
|
504
|
+
? parseEnumOption(attributes.get('type'), 'plain', BUFFER_TYPES, 'type', line)
|
|
505
|
+
: kind === 'logic'
|
|
506
|
+
? parseEnumOption(attributes.get('type'), 'low', LOGIC_STATES, 'type', line)
|
|
507
|
+
: kind === 'flipflop'
|
|
508
|
+
? parseEnumOption(attributes.get('type'), 'd', FLIPFLOP_TYPES, 'type', line)
|
|
509
|
+
: kind === 'mux'
|
|
510
|
+
? parseEnumOption(attributes.get('type'), 'mux', MUX_TYPES, 'type', line)
|
|
511
|
+
: kind === 'adder'
|
|
512
|
+
? parseEnumOption(attributes.get('type'), 'half', ADDER_TYPES, 'type', line)
|
|
513
|
+
: kind === 'bus'
|
|
514
|
+
? parseEnumOption(attributes.get('type'), 'splitter', BUS_TYPES, 'type', line)
|
|
515
|
+
: undefined;
|
|
516
|
+
if (variant === undefined && attributes.has('type')) {
|
|
517
|
+
throw new SchematicSyntaxError(`Option type is not supported for ${kind}.`, line);
|
|
518
|
+
}
|
|
519
|
+
if (kind === 'adder' && variant === 'full' && !attributes.has('inputs'))
|
|
520
|
+
inputs = 3;
|
|
521
|
+
if (kind === 'mux' && variant === 'demux') {
|
|
522
|
+
if (!attributes.has('inputs'))
|
|
523
|
+
inputs = 1;
|
|
524
|
+
if (!attributes.has('outputs'))
|
|
525
|
+
outputs = 2;
|
|
526
|
+
}
|
|
527
|
+
if (kind === 'bus' && variant === 'joiner' && !attributes.has('outputs'))
|
|
528
|
+
outputs = 1;
|
|
529
|
+
if ((kind === 'buffer' && (inputs !== 1 || outputs !== 1)) || ((kind === 'logic' || kind === 'clock') && outputs !== 1)) {
|
|
530
|
+
throw new SchematicSyntaxError(`${kind} has fixed terminal counts.`, line);
|
|
531
|
+
}
|
|
532
|
+
const bodyHeight = Math.max(52, Math.max(inputs, outputs) * 16 + 20);
|
|
533
|
+
const digital = {
|
|
534
|
+
kind, ...common, inputs: kind === 'logic' || kind === 'clock' ? 0 : inputs, outputs, width,
|
|
535
|
+
bodyWidth: kind === 'buffer' || kind === 'logic' || kind === 'clock' ? 56 : 84,
|
|
536
|
+
bodyHeight,
|
|
537
|
+
...parseOrientation(attributes, line)
|
|
538
|
+
};
|
|
539
|
+
if (variant !== undefined)
|
|
540
|
+
digital.variant = variant;
|
|
541
|
+
return digital;
|
|
542
|
+
}
|
|
420
543
|
if (kind === 'ic') {
|
|
421
|
-
assertOnlyAttributes(attributes, ['left', 'right', 'top', 'bottom'], line);
|
|
544
|
+
assertOnlyAttributes(attributes, ['left', 'right', 'top', 'bottom', 'orientation'], line);
|
|
422
545
|
const pins = parseIcPins(attributes, line);
|
|
423
546
|
return {
|
|
424
547
|
kind,
|
|
425
548
|
...common,
|
|
426
549
|
pins,
|
|
427
|
-
...integratedCircuitDimensions(pins)
|
|
550
|
+
...integratedCircuitDimensions(pins),
|
|
551
|
+
...parseOrientation(attributes, line)
|
|
428
552
|
};
|
|
429
553
|
}
|
|
430
554
|
if (includesValue(UML_COMPONENT_KINDS, kind)) {
|
|
431
555
|
switch (kind) {
|
|
432
|
-
case 'class':
|
|
556
|
+
case 'class':
|
|
557
|
+
case 'interface':
|
|
558
|
+
case 'enumeration':
|
|
559
|
+
case 'datatype':
|
|
560
|
+
case 'object': {
|
|
433
561
|
assertOnlyAttributes(attributes, ['attributes', 'operations', 'stereotype', 'width'], line);
|
|
434
562
|
const classAttributes = parseUmlRows(attributes.get('attributes'), 'attributes', line);
|
|
435
563
|
const operations = parseUmlRows(attributes.get('operations'), 'operations', line);
|
|
436
|
-
const stereotype = attributes.get('stereotype')
|
|
564
|
+
const stereotype = attributes.get('stereotype') ??
|
|
565
|
+
(kind === 'interface' || kind === 'enumeration' || kind === 'datatype' ? kind : undefined);
|
|
437
566
|
if (stereotype !== undefined && (stereotype === '' || stereotype.length > 128)) {
|
|
438
567
|
throw new SchematicSyntaxError('stereotype must contain 1 through 128 characters.', line);
|
|
439
568
|
}
|
|
@@ -467,13 +596,33 @@ function parseComponent(match, line) {
|
|
|
467
596
|
bodyHeight: 40 + details.length * UML_ROW_HEIGHT
|
|
468
597
|
};
|
|
469
598
|
}
|
|
599
|
+
case 'history':
|
|
600
|
+
assertOnlyAttributes(attributes, ['type'], line);
|
|
601
|
+
return {
|
|
602
|
+
kind,
|
|
603
|
+
...common,
|
|
604
|
+
variant: parseEnumOption(attributes.get('type'), 'shallow', ['shallow', 'deep'], 'type', line)
|
|
605
|
+
};
|
|
470
606
|
case 'usecase':
|
|
471
607
|
case 'lifeline':
|
|
472
608
|
case 'note':
|
|
473
|
-
case 'package':
|
|
609
|
+
case 'package':
|
|
610
|
+
case 'component':
|
|
611
|
+
case 'artifact':
|
|
612
|
+
case 'node':
|
|
613
|
+
case 'device':
|
|
614
|
+
case 'execution':
|
|
615
|
+
case 'system':
|
|
616
|
+
case 'action':
|
|
617
|
+
case 'object-node':
|
|
618
|
+
case 'partition':
|
|
619
|
+
case 'activation':
|
|
620
|
+
case 'fragment':
|
|
621
|
+
case 'interaction':
|
|
622
|
+
case 'region': {
|
|
474
623
|
assertOnlyAttributes(attributes, ['width', 'height'], line);
|
|
475
624
|
const defaultWidth = Math.max(kind === 'usecase' ? 112 : 96, mathLabelTextWidth(common.label, 8) + 28);
|
|
476
|
-
const defaultHeight = kind === 'lifeline' ? 180 : kind === 'usecase' ? 56 : 64;
|
|
625
|
+
const defaultHeight = kind === 'lifeline' ? 180 : kind === 'activation' ? 96 : kind === 'usecase' ? 56 : 64;
|
|
477
626
|
return {
|
|
478
627
|
kind,
|
|
479
628
|
...common,
|
|
@@ -482,25 +631,87 @@ function parseComponent(match, line) {
|
|
|
482
631
|
};
|
|
483
632
|
}
|
|
484
633
|
case 'actor':
|
|
634
|
+
case 'provided-interface':
|
|
635
|
+
case 'required-interface':
|
|
636
|
+
case 'component-port':
|
|
637
|
+
case 'decision':
|
|
638
|
+
case 'merge':
|
|
639
|
+
case 'fork':
|
|
640
|
+
case 'join':
|
|
641
|
+
case 'activity-final':
|
|
642
|
+
case 'flow-final':
|
|
643
|
+
case 'send-signal':
|
|
644
|
+
case 'receive-signal':
|
|
645
|
+
case 'destruction':
|
|
646
|
+
case 'gate':
|
|
647
|
+
case 'found':
|
|
648
|
+
case 'lost':
|
|
649
|
+
case 'choice':
|
|
650
|
+
case 'state-junction':
|
|
651
|
+
case 'entry':
|
|
652
|
+
case 'exit':
|
|
653
|
+
case 'terminate':
|
|
485
654
|
case 'initial':
|
|
486
655
|
case 'final':
|
|
487
656
|
assertOnlyAttributes(attributes, [], line);
|
|
488
657
|
return { kind, ...common };
|
|
489
658
|
}
|
|
490
659
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
660
|
+
if (includesValue(QUANTUM_GATE_KINDS, kind)) {
|
|
661
|
+
const detailed = kind === 'qgate' || includesValue(NAMED_QUANTUM_GATE_KINDS, kind);
|
|
662
|
+
assertOnlyAttributes(attributes, detailed ? ['parameter', 'matrix', 'phase', 'orientation'] : ['orientation'], line);
|
|
663
|
+
const component = { kind, ...common, ...parseOrientation(attributes, line) };
|
|
664
|
+
if (detailed) {
|
|
665
|
+
const parameter = attributes.get('parameter');
|
|
666
|
+
const matrix = attributes.get('matrix');
|
|
667
|
+
const phase = attributes.get('phase');
|
|
668
|
+
if (parameter !== undefined)
|
|
669
|
+
component.parameter = parameter;
|
|
670
|
+
if (matrix !== undefined)
|
|
671
|
+
component.matrix = matrix;
|
|
672
|
+
if (phase !== undefined)
|
|
673
|
+
component.phase = phase;
|
|
674
|
+
}
|
|
675
|
+
return component;
|
|
676
|
+
}
|
|
677
|
+
if (includesValue(QUANTUM_SPECIAL_KINDS, kind)) {
|
|
678
|
+
assertOnlyAttributes(attributes, ['control', 'operator', 'controls', 'targets', 'wires', 'width', 'orientation'], line);
|
|
679
|
+
if (attributes.has('control') && kind !== 'control' && kind !== 'controlled') {
|
|
680
|
+
throw new SchematicSyntaxError(`Option control is not supported for ${kind}.`, line);
|
|
681
|
+
}
|
|
682
|
+
if (attributes.has('operator') && kind !== 'controlled') {
|
|
683
|
+
throw new SchematicSyntaxError(`Option operator is not supported for ${kind}.`, line);
|
|
684
|
+
}
|
|
685
|
+
if ((attributes.has('controls') || attributes.has('targets')) && !['controlled', 'cz', 'cphase', 'toffoli', 'swap'].includes(kind)) {
|
|
686
|
+
throw new SchematicSyntaxError(`Control/target counts are not supported for ${kind}.`, line);
|
|
687
|
+
}
|
|
688
|
+
if (attributes.has('wires') && kind !== 'barrier' && kind !== 'delay') {
|
|
689
|
+
throw new SchematicSyntaxError(`Option wires is not supported for ${kind}.`, line);
|
|
690
|
+
}
|
|
691
|
+
if (attributes.has('width') && kind !== 'classical-register') {
|
|
692
|
+
throw new SchematicSyntaxError(`Option width is not supported for ${kind}.`, line);
|
|
693
|
+
}
|
|
694
|
+
const controlType = parseEnumOption(attributes.get('control'), 'positive', ['positive', 'negative', 'classical'], 'control', line);
|
|
695
|
+
const controlled = kind === 'controlled' || kind === 'cz' || kind === 'cphase' || kind === 'toffoli';
|
|
696
|
+
const component = {
|
|
697
|
+
kind, ...common,
|
|
698
|
+
controls: parseCount(attributes.get('controls'), kind === 'toffoli' ? 2 : controlled ? 1 : 0, 'controls', line),
|
|
699
|
+
targets: parseCount(attributes.get('targets'), kind === 'swap' ? 2 : controlled ? 1 : 0, 'targets', line),
|
|
700
|
+
wires: parseCount(attributes.get('wires'), kind === 'barrier' || kind === 'delay' ? 2 : 1, 'wires', line),
|
|
701
|
+
width: parseWidth(attributes.get('width'), kind === 'classical-register' ? 8 : 1, line),
|
|
702
|
+
...parseOrientation(attributes, line)
|
|
703
|
+
};
|
|
704
|
+
if (kind === 'classical-register' && component.width < 2) {
|
|
705
|
+
throw new SchematicSyntaxError('Classical register width must be at least 2.', line);
|
|
706
|
+
}
|
|
707
|
+
if (kind === 'control' || kind === 'controlled')
|
|
708
|
+
component.controlType = controlType;
|
|
709
|
+
const operator = attributes.get('operator');
|
|
710
|
+
if (operator !== undefined)
|
|
711
|
+
component.operator = operator;
|
|
712
|
+
return component;
|
|
713
|
+
}
|
|
714
|
+
throw new SchematicSyntaxError(`Unsupported component kind ${kind}.`, line);
|
|
504
715
|
}
|
|
505
716
|
function parseEndpoint(componentId, port) {
|
|
506
717
|
return { componentId, port };
|
|
@@ -538,8 +749,10 @@ function parseConnectionOptions(raw, line) {
|
|
|
538
749
|
let relation = 'signal';
|
|
539
750
|
let label;
|
|
540
751
|
let dashed = false;
|
|
752
|
+
let signalKind;
|
|
753
|
+
let width;
|
|
541
754
|
if (raw === undefined || raw.trim() === '') {
|
|
542
|
-
return { curve, markerStart, markerEnd, relation, label, dashed };
|
|
755
|
+
return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, width };
|
|
543
756
|
}
|
|
544
757
|
const seen = new Set();
|
|
545
758
|
for (const token of connectionOptionTokens(raw.trim(), line)) {
|
|
@@ -567,6 +780,14 @@ function parseConnectionOptions(raw, line) {
|
|
|
567
780
|
seen.add('relation');
|
|
568
781
|
continue;
|
|
569
782
|
}
|
|
783
|
+
if (includesValue(SCHEMATIC_SIGNAL_KINDS, token)) {
|
|
784
|
+
if (seen.has('signal')) {
|
|
785
|
+
throw new SchematicSyntaxError('Connection signal kind can only be declared once.', line);
|
|
786
|
+
}
|
|
787
|
+
signalKind = token;
|
|
788
|
+
seen.add('signal');
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
570
791
|
if (token === 'arrow' || token === 'dot') {
|
|
571
792
|
if (seen.has('marker-end')) {
|
|
572
793
|
throw new SchematicSyntaxError('Connection marker-end can only be declared once.', line);
|
|
@@ -575,7 +796,7 @@ function parseConnectionOptions(raw, line) {
|
|
|
575
796
|
seen.add('marker-end');
|
|
576
797
|
continue;
|
|
577
798
|
}
|
|
578
|
-
const match = token.match(/^(marker-start|marker-end|relation|label)=(.*)$/);
|
|
799
|
+
const match = token.match(/^(marker-start|marker-end|relation|label|signal|width)=(.*)$/);
|
|
579
800
|
if (!match) {
|
|
580
801
|
throw new SchematicSyntaxError('Unsupported connection routing, marker, relation, label, or stroke option.', line);
|
|
581
802
|
}
|
|
@@ -602,6 +823,15 @@ function parseConnectionOptions(raw, line) {
|
|
|
602
823
|
}
|
|
603
824
|
label = optionValue;
|
|
604
825
|
}
|
|
826
|
+
else if (option === 'signal') {
|
|
827
|
+
if (!includesValue(SCHEMATIC_SIGNAL_KINDS, optionValue)) {
|
|
828
|
+
throw new SchematicSyntaxError(`signal must be one of: ${SCHEMATIC_SIGNAL_KINDS.join(', ')}.`, line);
|
|
829
|
+
}
|
|
830
|
+
signalKind = optionValue;
|
|
831
|
+
}
|
|
832
|
+
else if (option === 'width') {
|
|
833
|
+
width = parseWidth(optionValue, 1, line);
|
|
834
|
+
}
|
|
605
835
|
else {
|
|
606
836
|
const marker = parseSignalMarker(optionValue, option, line);
|
|
607
837
|
if (option === 'marker-start')
|
|
@@ -622,19 +852,25 @@ function parseConnectionOptions(raw, line) {
|
|
|
622
852
|
markerEnd = 'triangle';
|
|
623
853
|
else if (relation === 'dependency' ||
|
|
624
854
|
relation === 'message' ||
|
|
855
|
+
relation === 'asynchronous' ||
|
|
856
|
+
relation === 'return' ||
|
|
857
|
+
relation === 'control-flow' ||
|
|
858
|
+
relation === 'object-flow' ||
|
|
625
859
|
relation === 'transition' ||
|
|
626
860
|
relation === 'include' ||
|
|
627
861
|
relation === 'extend') {
|
|
628
862
|
markerEnd = 'open-arrow';
|
|
629
863
|
}
|
|
864
|
+
else if (relation === 'synchronous')
|
|
865
|
+
markerEnd = 'arrow';
|
|
630
866
|
}
|
|
631
867
|
if (!seen.has('stroke-style')) {
|
|
632
|
-
dashed = ['dependency', 'realization', 'include', 'extend'].includes(relation);
|
|
868
|
+
dashed = ['dependency', 'realization', 'return', 'include', 'extend'].includes(relation);
|
|
633
869
|
}
|
|
634
870
|
if (label === undefined && (relation === 'include' || relation === 'extend')) {
|
|
635
871
|
label = `«${relation}»`;
|
|
636
872
|
}
|
|
637
|
-
return { curve, markerStart, markerEnd, relation, label, dashed };
|
|
873
|
+
return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, width };
|
|
638
874
|
}
|
|
639
875
|
function parseConnection(match, line) {
|
|
640
876
|
const tail = splitDeclarationTail(match[5], line);
|
|
@@ -652,6 +888,10 @@ function parseConnection(match, line) {
|
|
|
652
888
|
};
|
|
653
889
|
if (options.label !== undefined)
|
|
654
890
|
connection.label = options.label;
|
|
891
|
+
if (options.signalKind !== undefined)
|
|
892
|
+
connection.signalKind = options.signalKind;
|
|
893
|
+
if (options.width !== undefined)
|
|
894
|
+
connection.width = options.width;
|
|
655
895
|
return connection;
|
|
656
896
|
}
|
|
657
897
|
function validateComponent(component, fence) {
|
|
@@ -676,6 +916,59 @@ function validGatePort(component, port) {
|
|
|
676
916
|
function isClassicalGate(component) {
|
|
677
917
|
return CLASSICAL_GATE_KINDS.includes(component.kind);
|
|
678
918
|
}
|
|
919
|
+
function isDigitalComponent(component) {
|
|
920
|
+
return DIGITAL_COMPONENT_KINDS.includes(component.kind);
|
|
921
|
+
}
|
|
922
|
+
function isQuantumSpecial(component) {
|
|
923
|
+
return QUANTUM_SPECIAL_KINDS.includes(component.kind);
|
|
924
|
+
}
|
|
925
|
+
function validIndexedPort(port, inputs, outputs) {
|
|
926
|
+
if (port === 'in')
|
|
927
|
+
return inputs > 0;
|
|
928
|
+
if (port === 'out')
|
|
929
|
+
return outputs > 0;
|
|
930
|
+
const match = port.match(/^(in|out)([1-9]\d*)$/);
|
|
931
|
+
if (match === null)
|
|
932
|
+
return false;
|
|
933
|
+
const count = match[1] === 'in' ? inputs : outputs;
|
|
934
|
+
return Number(match[2]) <= count;
|
|
935
|
+
}
|
|
936
|
+
function validDigitalPort(component, port) {
|
|
937
|
+
if (validIndexedPort(port, component.inputs, component.outputs))
|
|
938
|
+
return true;
|
|
939
|
+
switch (component.kind) {
|
|
940
|
+
case 'buffer':
|
|
941
|
+
return component.variant?.startsWith('tristate') === true && port === 'enable';
|
|
942
|
+
case 'flipflop':
|
|
943
|
+
return ['d', 'j', 'k', 's', 'r', 't', 'clock', 'enable', 'preset', 'clear', 'q', 'nq'].includes(port);
|
|
944
|
+
case 'mux':
|
|
945
|
+
return port === 'select' || port === 'enable';
|
|
946
|
+
case 'register':
|
|
947
|
+
case 'counter':
|
|
948
|
+
return ['clock', 'enable', 'preset', 'clear'].includes(port);
|
|
949
|
+
case 'comparator':
|
|
950
|
+
return ['gt', 'eq', 'lt'].includes(port);
|
|
951
|
+
case 'bus':
|
|
952
|
+
return ['bus', 'tap'].includes(port);
|
|
953
|
+
default:
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function validQuantumSpecialPort(component, port) {
|
|
958
|
+
if (component.kind === 'prepare')
|
|
959
|
+
return port === 'out';
|
|
960
|
+
if (component.kind === 'control')
|
|
961
|
+
return ['in', 'out', 'control'].includes(port);
|
|
962
|
+
if (component.kind === 'measure')
|
|
963
|
+
return ['in', 'out', 'classical'].includes(port);
|
|
964
|
+
if (component.kind === 'classical-bit' || component.kind === 'classical-register') {
|
|
965
|
+
return port === 'in' || port === 'out';
|
|
966
|
+
}
|
|
967
|
+
const tracks = Math.max(component.wires, component.controls + component.targets);
|
|
968
|
+
return validIndexedPort(port, tracks, tracks) ||
|
|
969
|
+
(/^control[1-9]\d*$/.test(port) && Number(port.slice(7)) <= component.controls) ||
|
|
970
|
+
(/^target[1-9]\d*$/.test(port) && Number(port.slice(6)) <= component.targets);
|
|
971
|
+
}
|
|
679
972
|
function validIcPort(component, port) {
|
|
680
973
|
const sides = Object.values(component.pins);
|
|
681
974
|
if (port === 'in') {
|
|
@@ -698,6 +991,12 @@ function validateEndpoint(endpoint, components, line) {
|
|
|
698
991
|
if (isClassicalGate(component)) {
|
|
699
992
|
valid = validGatePort(component, endpoint.port);
|
|
700
993
|
}
|
|
994
|
+
else if (isDigitalComponent(component)) {
|
|
995
|
+
valid = validDigitalPort(component, endpoint.port);
|
|
996
|
+
}
|
|
997
|
+
else if (isQuantumSpecial(component)) {
|
|
998
|
+
valid = validQuantumSpecialPort(component, endpoint.port);
|
|
999
|
+
}
|
|
701
1000
|
else if (includesValue(UML_COMPONENT_KINDS, component.kind)) {
|
|
702
1001
|
valid = ['in', 'out', 'left', 'right', 'top', 'bottom'].includes(endpoint.port);
|
|
703
1002
|
if (!valid && component.kind === 'lifeline') {
|
|
@@ -734,6 +1033,19 @@ function validateEndpoint(endpoint, components, line) {
|
|
|
734
1033
|
case 'port':
|
|
735
1034
|
case 'hadamard':
|
|
736
1035
|
case 'qgate':
|
|
1036
|
+
case 'xgate':
|
|
1037
|
+
case 'ygate':
|
|
1038
|
+
case 'zgate':
|
|
1039
|
+
case 'sgate':
|
|
1040
|
+
case 'sdg':
|
|
1041
|
+
case 'tgate':
|
|
1042
|
+
case 'tdg':
|
|
1043
|
+
case 'sx':
|
|
1044
|
+
case 'phase':
|
|
1045
|
+
case 'rx':
|
|
1046
|
+
case 'ry':
|
|
1047
|
+
case 'rz':
|
|
1048
|
+
case 'ugate':
|
|
737
1049
|
valid = endpoint.port === 'in' || endpoint.port === 'out';
|
|
738
1050
|
break;
|
|
739
1051
|
case 'ground':
|
|
@@ -745,12 +1057,68 @@ function validateEndpoint(endpoint, components, line) {
|
|
|
745
1057
|
case 'ic':
|
|
746
1058
|
valid = validIcPort(component, endpoint.port);
|
|
747
1059
|
break;
|
|
1060
|
+
case 'junction':
|
|
1061
|
+
case 'testpoint':
|
|
1062
|
+
valid = ['in', 'out', 'node'].includes(endpoint.port);
|
|
1063
|
+
break;
|
|
1064
|
+
case 'connector':
|
|
1065
|
+
valid = endpoint.port === 'in' || endpoint.port === 'out';
|
|
1066
|
+
break;
|
|
1067
|
+
case 'source':
|
|
1068
|
+
valid = ['in', 'out', 'positive', 'negative'].includes(endpoint.port) ||
|
|
1069
|
+
(['vcvs', 'vccs', 'ccvs', 'cccs'].includes(String(component.variant)) && ['control-positive', 'control-negative'].includes(endpoint.port));
|
|
1070
|
+
break;
|
|
1071
|
+
case 'power':
|
|
1072
|
+
valid = endpoint.port === 'in';
|
|
1073
|
+
break;
|
|
1074
|
+
case 'switch':
|
|
1075
|
+
valid = component.variant === 'spdt'
|
|
1076
|
+
? ['in', 'out', 'common', 'normally-open', 'normally-closed'].includes(endpoint.port)
|
|
1077
|
+
: component.variant === 'relay'
|
|
1078
|
+
? ['in', 'out', 'coil1', 'coil2'].includes(endpoint.port)
|
|
1079
|
+
: ['in', 'out'].includes(endpoint.port);
|
|
1080
|
+
break;
|
|
1081
|
+
case 'amplifier':
|
|
1082
|
+
valid = ['in', 'positive', 'negative', 'out', 'v+', 'v-'].includes(endpoint.port);
|
|
1083
|
+
break;
|
|
1084
|
+
case 'protection':
|
|
1085
|
+
case 'resonator':
|
|
1086
|
+
case 'meter':
|
|
1087
|
+
case 'load':
|
|
1088
|
+
valid = ['in', 'out'].includes(endpoint.port);
|
|
1089
|
+
break;
|
|
748
1090
|
}
|
|
749
1091
|
}
|
|
750
1092
|
if (!valid) {
|
|
751
1093
|
throw new SchematicSyntaxError(`Port ${endpoint.componentId}.${endpoint.port} is invalid for ${component.kind}.`, line);
|
|
752
1094
|
}
|
|
753
1095
|
}
|
|
1096
|
+
function endpointWidth(endpoint, components) {
|
|
1097
|
+
const component = components.get(endpoint.componentId);
|
|
1098
|
+
if (component.kind === 'port')
|
|
1099
|
+
return component.width ?? 1;
|
|
1100
|
+
if (component.kind === 'classical-register')
|
|
1101
|
+
return component.width;
|
|
1102
|
+
if (isDigitalComponent(component)) {
|
|
1103
|
+
if (component.kind === 'register' && (endpoint.port === 'in' || endpoint.port === 'out')) {
|
|
1104
|
+
return component.width;
|
|
1105
|
+
}
|
|
1106
|
+
if (component.kind === 'bus' && endpoint.port === 'bus')
|
|
1107
|
+
return component.width;
|
|
1108
|
+
}
|
|
1109
|
+
return 1;
|
|
1110
|
+
}
|
|
1111
|
+
function validateConnectionWidth(connection, components) {
|
|
1112
|
+
const sourceWidth = endpointWidth(connection.from, components);
|
|
1113
|
+
const targetWidth = endpointWidth(connection.to, components);
|
|
1114
|
+
const width = connection.width ?? 1;
|
|
1115
|
+
if ((sourceWidth > 1 || targetWidth > 1) && connection.width === undefined) {
|
|
1116
|
+
throw new SchematicSyntaxError('Bus connections require an explicit width option.', connection.line);
|
|
1117
|
+
}
|
|
1118
|
+
if (sourceWidth !== targetWidth || width !== sourceWidth) {
|
|
1119
|
+
throw new SchematicSyntaxError(`Connection width ${width} is incompatible with ${sourceWidth}-bit source and ${targetWidth}-bit target ports.`, connection.line);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
754
1122
|
export function parseSchematicFence(info, defaultTitle = 'Engineering schematic') {
|
|
755
1123
|
if (info === undefined || !/^schemd(?:\s|$)/i.test(info.trim())) {
|
|
756
1124
|
return undefined;
|
|
@@ -816,6 +1184,7 @@ export function parseSchematic(source, fence) {
|
|
|
816
1184
|
for (const connection of connections) {
|
|
817
1185
|
validateEndpoint(connection.from, componentsById, connection.line);
|
|
818
1186
|
validateEndpoint(connection.to, componentsById, connection.line);
|
|
1187
|
+
validateConnectionWidth(connection, componentsById);
|
|
819
1188
|
}
|
|
820
1189
|
const document = { components, connections };
|
|
821
1190
|
const routes = validateDocumentGeometry(document, fence);
|