@schemd/core 0.1.0 → 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/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
- const separator = source.slice(cursor).match(/^\s+/);
145
- if (!separator)
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
- cursor += separator[0].length;
161
+ }
148
162
  }
149
- const keyMatch = source.slice(cursor).match(/^([a-z][a-z0-9-]*)=/);
150
- if (!keyMatch) {
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
- const key = keyMatch[1];
154
- cursor += keyMatch[0].length;
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 closingQuote = source.indexOf('"', cursor + 1);
158
- value = source.slice(cursor + 1, closingQuote);
159
- cursor = closingQuote + 1;
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 valueMatch = source.slice(cursor).match(/^[^\s]+/);
163
- if (!valueMatch)
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
- value = valueMatch[0];
166
- cursor += value.length;
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
- if (raw === undefined || raw.trim() === '')
393
- return { curve, markerStart, markerEnd };
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().split(/\s+/)) {
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)=(\S+)$/);
577
+ const match = token.match(/^(marker-start|marker-end|relation|label)=(.*)$/);
413
578
  if (!match) {
414
- throw new SchematicSyntaxError('Connection routing options support line, bezier, ortho, arrow, dot, marker-start, and marker-end.', line);
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
- const marker = parseSignalMarker(match[2], option, line);
421
- if (option === 'marker-start')
422
- markerStart = marker;
423
- else
424
- markerEnd = marker;
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
- return { curve, markerStart, markerEnd };
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
- return {
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':