@schemd/core 0.1.1 → 0.1.2
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 +23 -82
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/layout.d.ts +14 -4
- package/dist/layout.js +510 -138
- package/dist/limits.d.ts +3 -0
- package/dist/limits.js +2 -0
- package/dist/math-label.d.ts +20 -3
- package/dist/math-label.js +149 -56
- package/dist/parser.js +248 -27
- package/dist/renderer.js +147 -27
- package/dist/types.d.ts +58 -3
- package/dist/types.js +33 -1
- package/package.json +33 -3
package/dist/parser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ANALOG_KINDS, CLASSICAL_GATE_KINDS, COMPONENT_KINDS, DIODE_TYPES, GROUND_STYLES, PASSIVE_KINDS, SEMANTIC_COLORS, SCHEMATIC_SIGNAL_MARKERS, TRANSISTOR_TYPES, SchematicSyntaxError } from './types.js';
|
|
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
|
+
import { mathLabelTextWidth } from './math-label.js';
|
|
3
4
|
import { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_SOURCE_CHARACTERS } from './limits.js';
|
|
4
5
|
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+(.+)$/;
|
|
5
6
|
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+(.+)$/;
|
|
@@ -15,6 +16,9 @@ const IC_MINIMUM_HEIGHT = 64;
|
|
|
15
16
|
const IC_HORIZONTAL_PIN_SPACING = 22;
|
|
16
17
|
const IC_VERTICAL_PIN_SPACING = 18;
|
|
17
18
|
const IC_BODY_PADDING = 24;
|
|
19
|
+
const MAX_UML_ROWS = 64;
|
|
20
|
+
const MAX_UML_ROW_LENGTH = 256;
|
|
21
|
+
const UML_ROW_HEIGHT = 16;
|
|
18
22
|
const MAX_FENCE_TITLE_LENGTH = 512;
|
|
19
23
|
const parsedDocuments = new WeakSet();
|
|
20
24
|
function freezeParsedDocument(document) {
|
|
@@ -27,6 +31,12 @@ function freezeParsedDocument(document) {
|
|
|
27
31
|
Object.freeze(component.pins.bottom);
|
|
28
32
|
Object.freeze(component.pins);
|
|
29
33
|
}
|
|
34
|
+
if (component.kind === 'class') {
|
|
35
|
+
Object.freeze(component.attributes);
|
|
36
|
+
Object.freeze(component.operations);
|
|
37
|
+
}
|
|
38
|
+
if (component.kind === 'state')
|
|
39
|
+
Object.freeze(component.details);
|
|
30
40
|
Object.freeze(component);
|
|
31
41
|
}
|
|
32
42
|
for (const connection of document.connections) {
|
|
@@ -141,29 +151,51 @@ function parseAttributes(raw, line) {
|
|
|
141
151
|
let cursor = 0;
|
|
142
152
|
while (cursor < source.length) {
|
|
143
153
|
if (cursor > 0) {
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
if (!/\s/.test(source[cursor])) {
|
|
155
|
+
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
156
|
+
}
|
|
157
|
+
while (cursor < source.length && /\s/.test(source[cursor]))
|
|
158
|
+
cursor += 1;
|
|
159
|
+
if (cursor === source.length) {
|
|
146
160
|
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
147
|
-
|
|
161
|
+
}
|
|
148
162
|
}
|
|
149
|
-
const
|
|
150
|
-
|
|
163
|
+
const keyStart = cursor;
|
|
164
|
+
const firstCode = source.charCodeAt(cursor);
|
|
165
|
+
if (firstCode < 97 || firstCode > 122) {
|
|
151
166
|
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
152
167
|
}
|
|
153
|
-
|
|
154
|
-
cursor
|
|
168
|
+
cursor += 1;
|
|
169
|
+
while (cursor < source.length) {
|
|
170
|
+
const code = source.charCodeAt(cursor);
|
|
171
|
+
if (!((code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 45))
|
|
172
|
+
break;
|
|
173
|
+
cursor += 1;
|
|
174
|
+
}
|
|
175
|
+
if (source[cursor] !== '=') {
|
|
176
|
+
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
177
|
+
}
|
|
178
|
+
const key = source.slice(keyStart, cursor);
|
|
179
|
+
cursor += 1;
|
|
155
180
|
let value;
|
|
156
181
|
if (source[cursor] === '"') {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
const valueStart = ++cursor;
|
|
183
|
+
while (cursor < source.length && source[cursor] !== '"')
|
|
184
|
+
cursor += 1;
|
|
185
|
+
if (cursor === source.length) {
|
|
186
|
+
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
187
|
+
}
|
|
188
|
+
value = source.slice(valueStart, cursor);
|
|
189
|
+
cursor += 1;
|
|
160
190
|
}
|
|
161
191
|
else {
|
|
162
|
-
const
|
|
163
|
-
|
|
192
|
+
const valueStart = cursor;
|
|
193
|
+
while (cursor < source.length && !/\s/.test(source[cursor]))
|
|
194
|
+
cursor += 1;
|
|
195
|
+
if (cursor === valueStart) {
|
|
164
196
|
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
}
|
|
198
|
+
value = source.slice(valueStart, cursor);
|
|
167
199
|
}
|
|
168
200
|
if (attributes.has(key))
|
|
169
201
|
throw new SchematicSyntaxError(`Duplicate option ${key}.`, line);
|
|
@@ -287,6 +319,38 @@ function integratedCircuitDimensions(pins) {
|
|
|
287
319
|
bodyHeight: Math.max(IC_MINIMUM_HEIGHT, Math.max(pins.left.length, pins.right.length) * IC_VERTICAL_PIN_SPACING + IC_BODY_PADDING)
|
|
288
320
|
};
|
|
289
321
|
}
|
|
322
|
+
function parseUmlRows(value, name, line) {
|
|
323
|
+
if (value === undefined || value.trim() === '')
|
|
324
|
+
return [];
|
|
325
|
+
const rows = value.split(';').map((row) => row.trim());
|
|
326
|
+
if (rows.length > MAX_UML_ROWS) {
|
|
327
|
+
throw new SchematicSyntaxError(`${name} supports at most ${MAX_UML_ROWS} rows.`, line);
|
|
328
|
+
}
|
|
329
|
+
for (const row of rows) {
|
|
330
|
+
if (row === '' || row.length > MAX_UML_ROW_LENGTH) {
|
|
331
|
+
throw new SchematicSyntaxError(`${name} rows must contain 1 through ${MAX_UML_ROW_LENGTH} characters.`, line);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return rows;
|
|
335
|
+
}
|
|
336
|
+
function parseUmlDimension(value, fallback, name, line) {
|
|
337
|
+
if (value === undefined)
|
|
338
|
+
return fallback;
|
|
339
|
+
if (!/^\d+(?:\.\d+)?$/.test(value)) {
|
|
340
|
+
throw new SchematicSyntaxError(`${name} must be a finite number from 24 through 2048.`, line);
|
|
341
|
+
}
|
|
342
|
+
const dimension = Number(value);
|
|
343
|
+
if (dimension < 24 || dimension > 2048) {
|
|
344
|
+
throw new SchematicSyntaxError(`${name} must be a finite number from 24 through 2048.`, line);
|
|
345
|
+
}
|
|
346
|
+
return dimension;
|
|
347
|
+
}
|
|
348
|
+
function widestUmlRow(rows) {
|
|
349
|
+
let width = 0;
|
|
350
|
+
for (const row of rows)
|
|
351
|
+
width = Math.max(width, mathLabelTextWidth(row, 8));
|
|
352
|
+
return width;
|
|
353
|
+
}
|
|
290
354
|
function commonComponent(match, color, line) {
|
|
291
355
|
return {
|
|
292
356
|
id: match[2],
|
|
@@ -362,6 +426,67 @@ function parseComponent(match, line) {
|
|
|
362
426
|
...integratedCircuitDimensions(pins)
|
|
363
427
|
};
|
|
364
428
|
}
|
|
429
|
+
if (includesValue(UML_COMPONENT_KINDS, kind)) {
|
|
430
|
+
switch (kind) {
|
|
431
|
+
case 'class': {
|
|
432
|
+
assertOnlyAttributes(attributes, ['attributes', 'operations', 'stereotype', 'width'], line);
|
|
433
|
+
const classAttributes = parseUmlRows(attributes.get('attributes'), 'attributes', line);
|
|
434
|
+
const operations = parseUmlRows(attributes.get('operations'), 'operations', line);
|
|
435
|
+
const stereotype = attributes.get('stereotype');
|
|
436
|
+
if (stereotype !== undefined && (stereotype === '' || stereotype.length > 128)) {
|
|
437
|
+
throw new SchematicSyntaxError('stereotype must contain 1 through 128 characters.', line);
|
|
438
|
+
}
|
|
439
|
+
const calculatedWidth = Math.max(120, widestUmlRow([common.label, stereotype ?? '', ...classAttributes, ...operations]) + 24);
|
|
440
|
+
const bodyWidth = Math.max(calculatedWidth, parseUmlDimension(attributes.get('width'), calculatedWidth, 'width', line));
|
|
441
|
+
const bodyHeight = 36 +
|
|
442
|
+
Math.max(24, classAttributes.length * UML_ROW_HEIGHT + 8) +
|
|
443
|
+
Math.max(24, operations.length * UML_ROW_HEIGHT + 8) +
|
|
444
|
+
(stereotype === undefined ? 0 : 14);
|
|
445
|
+
const component = {
|
|
446
|
+
kind,
|
|
447
|
+
...common,
|
|
448
|
+
attributes: classAttributes,
|
|
449
|
+
operations,
|
|
450
|
+
bodyWidth,
|
|
451
|
+
bodyHeight
|
|
452
|
+
};
|
|
453
|
+
if (stereotype !== undefined)
|
|
454
|
+
component.stereotype = stereotype;
|
|
455
|
+
return component;
|
|
456
|
+
}
|
|
457
|
+
case 'state': {
|
|
458
|
+
assertOnlyAttributes(attributes, ['details', 'width'], line);
|
|
459
|
+
const details = parseUmlRows(attributes.get('details'), 'details', line);
|
|
460
|
+
const calculatedWidth = Math.max(112, widestUmlRow([common.label, ...details]) + 28);
|
|
461
|
+
return {
|
|
462
|
+
kind,
|
|
463
|
+
...common,
|
|
464
|
+
details,
|
|
465
|
+
bodyWidth: Math.max(calculatedWidth, parseUmlDimension(attributes.get('width'), calculatedWidth, 'width', line)),
|
|
466
|
+
bodyHeight: 40 + details.length * UML_ROW_HEIGHT
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
case 'usecase':
|
|
470
|
+
case 'lifeline':
|
|
471
|
+
case 'note':
|
|
472
|
+
case 'package': {
|
|
473
|
+
assertOnlyAttributes(attributes, ['width', 'height'], line);
|
|
474
|
+
const defaultWidth = Math.max(kind === 'usecase' ? 112 : 96, mathLabelTextWidth(common.label, 8) + 28);
|
|
475
|
+
const defaultHeight = kind === 'lifeline' ? 180 : kind === 'usecase' ? 56 : 64;
|
|
476
|
+
return {
|
|
477
|
+
kind,
|
|
478
|
+
...common,
|
|
479
|
+
bodyWidth: Math.max(defaultWidth, parseUmlDimension(attributes.get('width'), defaultWidth, 'width', line)),
|
|
480
|
+
bodyHeight: Math.max(defaultHeight, parseUmlDimension(attributes.get('height'), defaultHeight, 'height', line))
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
case 'actor':
|
|
484
|
+
case 'initial':
|
|
485
|
+
case 'final':
|
|
486
|
+
assertOnlyAttributes(attributes, [], line);
|
|
487
|
+
return { kind, ...common };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
365
490
|
const quantumKind = kind;
|
|
366
491
|
assertOnlyAttributes(attributes, quantumKind === 'qgate' ? ['parameter', 'matrix', 'phase'] : [], line);
|
|
367
492
|
const component = { kind: quantumKind, ...common };
|
|
@@ -379,6 +504,26 @@ function parseComponent(match, line) {
|
|
|
379
504
|
function parseEndpoint(componentId, port) {
|
|
380
505
|
return { componentId, port };
|
|
381
506
|
}
|
|
507
|
+
function connectionOptionTokens(raw, line) {
|
|
508
|
+
const tokens = [];
|
|
509
|
+
let tokenStart = -1;
|
|
510
|
+
let quoteOpen = false;
|
|
511
|
+
for (let index = 0; index <= raw.length; index += 1) {
|
|
512
|
+
const character = raw[index];
|
|
513
|
+
if (character === '"')
|
|
514
|
+
quoteOpen = !quoteOpen;
|
|
515
|
+
if ((character === undefined || (/\s/.test(character) && !quoteOpen)) && tokenStart >= 0) {
|
|
516
|
+
tokens.push(raw.slice(tokenStart, index));
|
|
517
|
+
tokenStart = -1;
|
|
518
|
+
}
|
|
519
|
+
else if (character !== undefined && !/\s/.test(character) && tokenStart < 0) {
|
|
520
|
+
tokenStart = index;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (quoteOpen)
|
|
524
|
+
throw new SchematicSyntaxError('Malformed quoted connection option.', line);
|
|
525
|
+
return tokens;
|
|
526
|
+
}
|
|
382
527
|
function parseSignalMarker(value, option, line) {
|
|
383
528
|
if (!includesValue(SCHEMATIC_SIGNAL_MARKERS, value)) {
|
|
384
529
|
throw new SchematicSyntaxError(`${option} must be one of: ${SCHEMATIC_SIGNAL_MARKERS.join(', ')}.`, line);
|
|
@@ -389,10 +534,14 @@ function parseConnectionOptions(raw, line) {
|
|
|
389
534
|
let curve = 'line';
|
|
390
535
|
let markerStart = 'none';
|
|
391
536
|
let markerEnd = 'none';
|
|
392
|
-
|
|
393
|
-
|
|
537
|
+
let relation = 'signal';
|
|
538
|
+
let label;
|
|
539
|
+
let dashed = false;
|
|
540
|
+
if (raw === undefined || raw.trim() === '') {
|
|
541
|
+
return { curve, markerStart, markerEnd, relation, label, dashed };
|
|
542
|
+
}
|
|
394
543
|
const seen = new Set();
|
|
395
|
-
for (const token of raw.trim()
|
|
544
|
+
for (const token of connectionOptionTokens(raw.trim(), line)) {
|
|
396
545
|
if (token === 'line' || token === 'bezier' || token === 'ortho') {
|
|
397
546
|
if (seen.has('curve')) {
|
|
398
547
|
throw new SchematicSyntaxError('Connection routing can only be declared once.', line);
|
|
@@ -401,6 +550,22 @@ function parseConnectionOptions(raw, line) {
|
|
|
401
550
|
seen.add('curve');
|
|
402
551
|
continue;
|
|
403
552
|
}
|
|
553
|
+
if (token === 'dashed' || token === 'solid') {
|
|
554
|
+
if (seen.has('stroke-style')) {
|
|
555
|
+
throw new SchematicSyntaxError('Connection stroke style can only be declared once.', line);
|
|
556
|
+
}
|
|
557
|
+
dashed = token === 'dashed';
|
|
558
|
+
seen.add('stroke-style');
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (includesValue(UML_RELATION_KINDS, token)) {
|
|
562
|
+
if (seen.has('relation')) {
|
|
563
|
+
throw new SchematicSyntaxError('Connection relation can only be declared once.', line);
|
|
564
|
+
}
|
|
565
|
+
relation = token;
|
|
566
|
+
seen.add('relation');
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
404
569
|
if (token === 'arrow' || token === 'dot') {
|
|
405
570
|
if (seen.has('marker-end')) {
|
|
406
571
|
throw new SchematicSyntaxError('Connection marker-end can only be declared once.', line);
|
|
@@ -409,35 +574,84 @@ function parseConnectionOptions(raw, line) {
|
|
|
409
574
|
seen.add('marker-end');
|
|
410
575
|
continue;
|
|
411
576
|
}
|
|
412
|
-
const match = token.match(/^(marker-start|marker-end)=(
|
|
577
|
+
const match = token.match(/^(marker-start|marker-end|relation|label)=(.*)$/);
|
|
413
578
|
if (!match) {
|
|
414
|
-
throw new SchematicSyntaxError('
|
|
579
|
+
throw new SchematicSyntaxError('Unsupported connection routing, marker, relation, label, or stroke option.', line);
|
|
415
580
|
}
|
|
416
581
|
const option = match[1];
|
|
417
582
|
if (seen.has(option)) {
|
|
418
583
|
throw new SchematicSyntaxError(`Connection ${option} can only be declared once.`, line);
|
|
419
584
|
}
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
585
|
+
let optionValue = match[2];
|
|
586
|
+
if (optionValue.startsWith('"') || optionValue.endsWith('"')) {
|
|
587
|
+
if (!(optionValue.length >= 2 && optionValue.startsWith('"') && optionValue.endsWith('"'))) {
|
|
588
|
+
throw new SchematicSyntaxError(`Malformed connection ${option}.`, line);
|
|
589
|
+
}
|
|
590
|
+
optionValue = optionValue.slice(1, -1);
|
|
591
|
+
}
|
|
592
|
+
if (option === 'relation') {
|
|
593
|
+
if (!includesValue(UML_RELATION_KINDS, optionValue)) {
|
|
594
|
+
throw new SchematicSyntaxError(`relation must be one of: ${UML_RELATION_KINDS.join(', ')}.`, line);
|
|
595
|
+
}
|
|
596
|
+
relation = optionValue;
|
|
597
|
+
}
|
|
598
|
+
else if (option === 'label') {
|
|
599
|
+
if (optionValue === '' || optionValue.length > 256) {
|
|
600
|
+
throw new SchematicSyntaxError('Connection labels require 1 through 256 characters.', line);
|
|
601
|
+
}
|
|
602
|
+
label = optionValue;
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
const marker = parseSignalMarker(optionValue, option, line);
|
|
606
|
+
if (option === 'marker-start')
|
|
607
|
+
markerStart = marker;
|
|
608
|
+
else
|
|
609
|
+
markerEnd = marker;
|
|
610
|
+
}
|
|
425
611
|
seen.add(option);
|
|
426
612
|
}
|
|
427
|
-
|
|
613
|
+
if (!seen.has('marker-start')) {
|
|
614
|
+
if (relation === 'aggregation')
|
|
615
|
+
markerStart = 'diamond';
|
|
616
|
+
else if (relation === 'composition')
|
|
617
|
+
markerStart = 'diamond-filled';
|
|
618
|
+
}
|
|
619
|
+
if (!seen.has('marker-end')) {
|
|
620
|
+
if (relation === 'generalization' || relation === 'realization')
|
|
621
|
+
markerEnd = 'triangle';
|
|
622
|
+
else if (relation === 'dependency' ||
|
|
623
|
+
relation === 'message' ||
|
|
624
|
+
relation === 'transition' ||
|
|
625
|
+
relation === 'include' ||
|
|
626
|
+
relation === 'extend') {
|
|
627
|
+
markerEnd = 'open-arrow';
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (!seen.has('stroke-style')) {
|
|
631
|
+
dashed = ['dependency', 'realization', 'include', 'extend'].includes(relation);
|
|
632
|
+
}
|
|
633
|
+
if (label === undefined && (relation === 'include' || relation === 'extend')) {
|
|
634
|
+
label = `«${relation}»`;
|
|
635
|
+
}
|
|
636
|
+
return { curve, markerStart, markerEnd, relation, label, dashed };
|
|
428
637
|
}
|
|
429
638
|
function parseConnection(match, line) {
|
|
430
639
|
const tail = splitDeclarationTail(match[5], line);
|
|
431
640
|
const options = parseConnectionOptions(tail.options, line);
|
|
432
|
-
|
|
641
|
+
const connection = {
|
|
433
642
|
from: parseEndpoint(match[1], match[2]),
|
|
434
643
|
to: parseEndpoint(match[3], match[4]),
|
|
435
644
|
color: parseSchematicColor(tail.color, line),
|
|
436
645
|
curve: options.curve,
|
|
437
646
|
markerStart: options.markerStart,
|
|
438
647
|
markerEnd: options.markerEnd,
|
|
648
|
+
relation: options.relation,
|
|
649
|
+
dashed: options.dashed,
|
|
439
650
|
line
|
|
440
651
|
};
|
|
652
|
+
if (options.label !== undefined)
|
|
653
|
+
connection.label = options.label;
|
|
654
|
+
return connection;
|
|
441
655
|
}
|
|
442
656
|
function validateComponent(component, fence) {
|
|
443
657
|
if (component.x < 0 ||
|
|
@@ -483,6 +697,13 @@ function validateEndpoint(endpoint, components, line) {
|
|
|
483
697
|
if (isClassicalGate(component)) {
|
|
484
698
|
valid = validGatePort(component, endpoint.port);
|
|
485
699
|
}
|
|
700
|
+
else if (includesValue(UML_COMPONENT_KINDS, component.kind)) {
|
|
701
|
+
valid = ['in', 'out', 'left', 'right', 'top', 'bottom'].includes(endpoint.port);
|
|
702
|
+
if (!valid && component.kind === 'lifeline') {
|
|
703
|
+
const match = endpoint.port.match(/^(left|right)(\d+)$/);
|
|
704
|
+
valid = match !== null && Number(match[2]) <= component.bodyHeight;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
486
707
|
else {
|
|
487
708
|
switch (component.kind) {
|
|
488
709
|
case 'resistor':
|