@openpowershift/logic-diagram-language 0.1.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/dist/examples.d.ts +2 -0
  4. package/dist/examples.js +469 -0
  5. package/dist/export-image.d.ts +10 -0
  6. package/dist/export-image.js +47 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/index.html +13 -0
  9. package/dist/index.js +18 -0
  10. package/dist/parser/ast.d.ts +118 -0
  11. package/dist/parser/ast.js +79 -0
  12. package/dist/parser/index.d.ts +3 -0
  13. package/dist/parser/index.js +2 -0
  14. package/dist/parser/parser.d.ts +2 -0
  15. package/dist/parser/parser.js +635 -0
  16. package/dist/renderer/astar-router.d.ts +16 -0
  17. package/dist/renderer/astar-router.js +532 -0
  18. package/dist/renderer/gates.d.ts +19 -0
  19. package/dist/renderer/gates.js +92 -0
  20. package/dist/renderer/graph.d.ts +31 -0
  21. package/dist/renderer/graph.js +403 -0
  22. package/dist/renderer/layout.d.ts +76 -0
  23. package/dist/renderer/layout.js +3121 -0
  24. package/dist/renderer/math-renderer.d.ts +16 -0
  25. package/dist/renderer/math-renderer.js +119 -0
  26. package/dist/renderer/svg-renderer.d.ts +3 -0
  27. package/dist/renderer/svg-renderer.js +599 -0
  28. package/dist/renderer/wires.d.ts +5 -0
  29. package/dist/renderer/wires.js +12 -0
  30. package/dist/theme/themes.d.ts +58 -0
  31. package/dist/theme/themes.js +104 -0
  32. package/docs/api.adoc +196 -0
  33. package/docs/ldl-for-llms.md +163 -0
  34. package/docs/user-guide.adoc +318 -0
  35. package/package.json +78 -0
  36. package/spec/render.sh +22 -0
  37. package/spec/sections/attributes.adoc +80 -0
  38. package/spec/sections/connections.adoc +39 -0
  39. package/spec/sections/examples.adoc +212 -0
  40. package/spec/sections/expressions.adoc +182 -0
  41. package/spec/sections/file-extension.adoc +5 -0
  42. package/spec/sections/function-blocks.adoc +120 -0
  43. package/spec/sections/grammar.adoc +64 -0
  44. package/spec/sections/hyperlinks.adoc +18 -0
  45. package/spec/sections/introduction.adoc +16 -0
  46. package/spec/sections/layout-rules.adoc +491 -0
  47. package/spec/sections/lexical-conventions.adoc +68 -0
  48. package/spec/sections/objects.adoc +31 -0
  49. package/spec/sections/options.adoc +146 -0
  50. package/spec/sections/ports.adoc +77 -0
  51. package/spec/sections/rendering-contract.adoc +11 -0
  52. package/spec/sections/revision-history.adoc +9 -0
  53. package/spec/sections/styling.adoc +83 -0
  54. package/spec/sections/svg-symbol-specification.adoc +149 -0
  55. package/spec/sections/symbol-definitions.adoc +143 -0
  56. package/spec/sections/templates.adoc +31 -0
  57. package/spec/spec.adoc +49 -0
@@ -0,0 +1,635 @@
1
+ const BLOCK_TYPES = new Set(['TIMER', 'SR', 'RISING', 'FALLING', 'COMPARE', 'FB']);
2
+ const KEYWORDS = new Set([
3
+ 'and', 'or', 'not', 'nand', 'nor', 'xor', 'xnor',
4
+ 'symbol', 'end', 'port', 'input', 'output', 'bidi',
5
+ 'attribute', 'import', 'template', 'connect',
6
+ 'style', 'stylesheet', 'link', 'as', 'true', 'false',
7
+ 'option',
8
+ ]);
9
+ function tokenize(source) {
10
+ const tokens = [];
11
+ const errors = [];
12
+ let pos = 0;
13
+ let line = 1;
14
+ let col = 1;
15
+ // Detect STYLE blocks at line start and skip the raw CSS (which contains characters the
16
+ // tokenizer can't handle: #, {, }, :, ;, .). A STYLE block starts with `STYLE` on its own
17
+ // line and ends with `END` on its own line. We emit a single STYLE token, then a raw CSS
18
+ // STRING token containing the CSS body, then skip to END and emit END.
19
+ const checkStyleBlock = () => {
20
+ // Must be at the start of a line (col === 1 or only whitespace before pos on this line).
21
+ let p = pos;
22
+ while (p > 0 && source[p - 1] === ' ')
23
+ p--;
24
+ if (p > 0 && source[p - 1] !== '\n')
25
+ return false;
26
+ // Check if the line starts with STYLE (case-insensitive) followed by whitespace/newline.
27
+ const rest = source.slice(pos);
28
+ const m = rest.match(/^STYLE\s*[\n\r]/i);
29
+ if (!m)
30
+ return false;
31
+ // Emit STYLE token.
32
+ tokens.push({ type: 'KEYWORD', value: 'STYLE', line, column: col, offset: pos });
33
+ const styleEnd = pos + m[0].length;
34
+ // Advance past STYLE + newline.
35
+ pos = styleEnd;
36
+ line++;
37
+ col = 1;
38
+ // Find the next standalone END or END STYLE (on its own line).
39
+ const endMatch = source.slice(pos).match(/\n\s*END(\s+STYLE)?\s*(\n|$)/i);
40
+ let cssEnd;
41
+ if (endMatch) {
42
+ cssEnd = pos + endMatch.index;
43
+ }
44
+ else {
45
+ cssEnd = source.length;
46
+ }
47
+ // Emit a STRING token containing the raw CSS.
48
+ const cssText = source.slice(pos, cssEnd).trim();
49
+ if (cssText) {
50
+ tokens.push({ type: 'STRING', value: cssText, line, column: 1, offset: pos });
51
+ }
52
+ // Advance to END.
53
+ if (endMatch) {
54
+ const endStart = pos + endMatch.index + 1; // skip the \n before END
55
+ // Count newlines in the CSS text for line tracking.
56
+ const cssRaw = source.slice(pos, endStart);
57
+ const newlines = (cssRaw.match(/\n/g) || []).length;
58
+ line += newlines;
59
+ pos = endStart;
60
+ col = 1;
61
+ // Skip whitespace before END.
62
+ while (pos < source.length && source[pos] === ' ') {
63
+ pos++;
64
+ col++;
65
+ }
66
+ // Emit END token.
67
+ tokens.push({ type: 'KEYWORD', value: 'END', line, column: col, offset: pos });
68
+ pos += 3;
69
+ col += 3;
70
+ // If END is followed by STYLE (i.e. "END STYLE"), skip past STYLE too so the parser
71
+ // doesn't see a second STYLE keyword and parse an empty style block.
72
+ const rest2 = source.slice(pos);
73
+ const styleAfter = rest2.match(/^\s+STYLE\b/i);
74
+ if (styleAfter) {
75
+ pos += styleAfter[0].length;
76
+ col += styleAfter[0].length;
77
+ }
78
+ }
79
+ else {
80
+ pos = source.length;
81
+ }
82
+ return true;
83
+ };
84
+ while (pos < source.length) {
85
+ const ch = source[pos];
86
+ // Check for STYLE block before any other tokenization (CSS content has invalid chars).
87
+ if (checkStyleBlock())
88
+ continue;
89
+ if (ch === '\n') {
90
+ line++;
91
+ col = 1;
92
+ pos++;
93
+ continue;
94
+ }
95
+ if (ch === '\r') {
96
+ pos++;
97
+ if (source[pos] === '\n')
98
+ pos++;
99
+ line++;
100
+ col = 1;
101
+ continue;
102
+ }
103
+ if (ch === ' ' || ch === '\t') {
104
+ pos++;
105
+ col++;
106
+ continue;
107
+ }
108
+ if (source.slice(pos, pos + 2) === '//') {
109
+ const end = source.indexOf('\n', pos);
110
+ pos = end === -1 ? source.length : end;
111
+ col = 1;
112
+ continue;
113
+ }
114
+ if (source.slice(pos, pos + 2) === '/*') {
115
+ const end = source.indexOf('*/', pos + 2);
116
+ if (end === -1) {
117
+ errors.push({ message: 'Unterminated block comment', line, column: col, offset: pos });
118
+ pos = source.length;
119
+ }
120
+ else {
121
+ const comment = source.slice(pos, end + 2);
122
+ const newlines = (comment.match(/\n/g) || []).length;
123
+ line += newlines;
124
+ pos = end + 2;
125
+ col = 1;
126
+ }
127
+ continue;
128
+ }
129
+ if (ch === '"') {
130
+ const start = pos;
131
+ const startLine = line;
132
+ const startCol = col;
133
+ pos++;
134
+ let value = '';
135
+ while (pos < source.length && source[pos] !== '"') {
136
+ if (source[pos] === '\\' && pos + 1 < source.length && source[pos + 1] === '"') {
137
+ value += '"';
138
+ pos += 2;
139
+ }
140
+ else {
141
+ value += source[pos];
142
+ pos += 1;
143
+ }
144
+ col++;
145
+ }
146
+ if (pos >= source.length) {
147
+ errors.push({ message: 'Unterminated string', line: startLine, column: startCol, offset: start });
148
+ }
149
+ else {
150
+ pos++;
151
+ col++;
152
+ }
153
+ tokens.push({ type: 'STRING', value, line: startLine, column: startCol, offset: start });
154
+ continue;
155
+ }
156
+ if (/[0-9]/.test(ch)) {
157
+ const start = pos;
158
+ const startLine = line;
159
+ const startCol = col;
160
+ let num = '';
161
+ while (pos < source.length && /[0-9]/.test(source[pos])) {
162
+ num += source[pos];
163
+ pos++;
164
+ col++;
165
+ }
166
+ if (pos < source.length && source[pos] === '.') {
167
+ num += '.';
168
+ pos++;
169
+ col++;
170
+ while (pos < source.length && /[0-9]/.test(source[pos])) {
171
+ num += source[pos];
172
+ pos++;
173
+ col++;
174
+ }
175
+ }
176
+ const rest = source.slice(pos);
177
+ const durationMatch = rest.match(/^(ms|cycles|cycle|cyc|s(?![a-zA-Z])|m(?![a-zA-Z]))/);
178
+ if (durationMatch) {
179
+ pos += durationMatch[0].length;
180
+ col += durationMatch[0].length;
181
+ tokens.push({ type: 'DURATION', value: num + durationMatch[0], line: startLine, column: startCol, offset: start });
182
+ }
183
+ else {
184
+ tokens.push({ type: 'NUMBER', value: num, line: startLine, column: startCol, offset: start });
185
+ }
186
+ continue;
187
+ }
188
+ if (/[a-zA-Z_]/.test(ch)) {
189
+ const start = pos;
190
+ const startLine = line;
191
+ const startCol = col;
192
+ let ident = '';
193
+ while (pos < source.length && /[a-zA-Z0-9_]/.test(source[pos])) {
194
+ ident += source[pos];
195
+ pos++;
196
+ col++;
197
+ }
198
+ const lower = ident.toLowerCase();
199
+ if (KEYWORDS.has(lower)) {
200
+ if (lower === 'true' || lower === 'false') {
201
+ tokens.push({ type: 'KEYWORD', value: ident.toLowerCase(), line: startLine, column: startCol, offset: start });
202
+ }
203
+ else {
204
+ tokens.push({ type: 'KEYWORD', value: ident.toUpperCase(), line: startLine, column: startCol, offset: start });
205
+ }
206
+ }
207
+ else if (/^[A-Z]/.test(ident) && /^[A-Z0-9_]+$/.test(ident)) {
208
+ tokens.push({ type: 'SYMBOL_NAME', value: ident, line: startLine, column: startCol, offset: start });
209
+ }
210
+ else {
211
+ tokens.push({ type: 'IDENT', value: ident, line: startLine, column: startCol, offset: start });
212
+ }
213
+ continue;
214
+ }
215
+ if ('=.()#{},[]'.includes(ch)) {
216
+ tokens.push({ type: 'OP', value: ch, line, column: col, offset: pos });
217
+ pos++;
218
+ col++;
219
+ continue;
220
+ }
221
+ errors.push({ message: `Unexpected character: '${ch}'`, line, column: col, offset: pos });
222
+ pos++;
223
+ col++;
224
+ }
225
+ tokens.push({ type: 'EOF', value: '', line, column: col, offset: pos });
226
+ return { tokens, errors };
227
+ }
228
+ class Parser {
229
+ constructor(tokens, errors, source) {
230
+ this.tokens = tokens;
231
+ this.pos = 0;
232
+ this.errors = [...errors];
233
+ this.source = source;
234
+ }
235
+ peek() {
236
+ return this.tokens[this.pos] ?? this.tokens[this.tokens.length - 1];
237
+ }
238
+ peekAt(offset) {
239
+ return this.tokens[this.pos + offset] ?? this.tokens[this.tokens.length - 1];
240
+ }
241
+ advance() {
242
+ const token = this.tokens[this.pos];
243
+ this.pos++;
244
+ return token;
245
+ }
246
+ expect(type, value) {
247
+ const token = this.peek();
248
+ if (value !== undefined) {
249
+ if (token.type === type && (token.value === value || token.value.toUpperCase() === value.toUpperCase())) {
250
+ return this.advance();
251
+ }
252
+ }
253
+ else {
254
+ if (token.type === type) {
255
+ return this.advance();
256
+ }
257
+ }
258
+ this.errors.push({
259
+ message: `Expected ${value ?? type} but got '${token.value}' (${token.type})`,
260
+ line: token.line,
261
+ column: token.column,
262
+ offset: token.offset,
263
+ });
264
+ return null;
265
+ }
266
+ match(type, value) {
267
+ const token = this.peek();
268
+ if (value !== undefined) {
269
+ if (token.type === type && (token.value === value || token.value.toUpperCase() === value.toUpperCase())) {
270
+ return this.advance();
271
+ }
272
+ }
273
+ else {
274
+ if (token.type === type) {
275
+ return this.advance();
276
+ }
277
+ }
278
+ return null;
279
+ }
280
+ isKeyword(kw) {
281
+ const token = this.peek();
282
+ return token.type === 'KEYWORD' && token.value.toUpperCase() === kw.toUpperCase();
283
+ }
284
+ parseDiagram() {
285
+ const outputs = [];
286
+ const objects = [];
287
+ const portMeta = [];
288
+ const attributes = [];
289
+ const connections = [];
290
+ const styles = [];
291
+ const options = [];
292
+ while (this.peek().type !== 'EOF') {
293
+ if (this.isKeyword('CONNECT')) {
294
+ this.advance();
295
+ const conn = this.parseConnect();
296
+ if (conn)
297
+ connections.push(conn);
298
+ }
299
+ else if (this.isKeyword('OPTION')) {
300
+ this.advance();
301
+ const nameToken = this.peek();
302
+ const name = this.advance()?.value ?? '';
303
+ this.expect('OP', '=');
304
+ // Accumulate every token on the value's line, so list/bracket values work too —
305
+ // e.g. `COMPACTNESS = 70,70` or `COMPACTNESS = [60,60]`.
306
+ const valueToken = this.peek();
307
+ let value = this.advance()?.value ?? '';
308
+ while (this.peek() && this.peek().type !== 'EOF' && this.peek().line === valueToken?.line) {
309
+ value += this.advance().value;
310
+ }
311
+ options.push({ name, value, pos: { line: nameToken.line, column: nameToken.column, offset: nameToken.offset } });
312
+ }
313
+ else if (this.isKeyword('STYLE')) {
314
+ this.advance();
315
+ // The tokenizer emitted the CSS body as a single STRING token (raw text between STYLE
316
+ // and END). If no STRING follows (empty STYLE block), push empty CSS.
317
+ const cssToken = this.peek();
318
+ if (cssToken.type === 'STRING') {
319
+ styles.push({ css: this.advance().value });
320
+ }
321
+ else {
322
+ styles.push({ css: '' });
323
+ }
324
+ // Consume the END keyword that closes the STYLE block.
325
+ this.match('KEYWORD', 'END');
326
+ }
327
+ else if (this.isKeyword('STYLESHEET')) {
328
+ this.advance();
329
+ this.expect('STRING');
330
+ }
331
+ else if (this.isKeyword('IMPORT')) {
332
+ this.advance();
333
+ this.match('KEYWORD', 'TEMPLATE');
334
+ this.expect('STRING');
335
+ this.match('KEYWORD', 'AS');
336
+ this.advance();
337
+ }
338
+ else if (this.isKeyword('SYMBOL')) {
339
+ this.advance();
340
+ while (this.peek().type !== 'EOF' && !this.isKeyword('END')) {
341
+ this.advance();
342
+ }
343
+ this.match('KEYWORD', 'END');
344
+ }
345
+ else {
346
+ const la1 = this.peek();
347
+ const la2 = this.peekAt(1);
348
+ const la3 = this.peekAt(2);
349
+ if (la2 && la2.value === '#') {
350
+ const obj = this.parseObjectDecl();
351
+ if (obj)
352
+ objects.push(obj);
353
+ }
354
+ else if (la2 && la2.value === '.' && la3) {
355
+ const propName = la3.value;
356
+ if (propName === 'Name' || propName === 'Description' || propName === 'Style' || propName.toUpperCase() === 'OUT') {
357
+ const meta = this.parsePortMeta();
358
+ if (meta)
359
+ portMeta.push(meta);
360
+ }
361
+ else {
362
+ const attr = this.parseAttributeOrPort();
363
+ if (attr)
364
+ attributes.push(attr);
365
+ }
366
+ }
367
+ else if (la2 && la2.value === '=' && la1.type !== 'EOF') {
368
+ const expr = this.parseExpression();
369
+ if (expr)
370
+ outputs.push(expr);
371
+ }
372
+ else {
373
+ this.advance();
374
+ }
375
+ }
376
+ }
377
+ return { outputs, objects, portMeta, attributes, connections, styles, options };
378
+ }
379
+ parsePortMeta() {
380
+ const nameToken = this.peek();
381
+ const identifier = this.advance()?.value ?? '';
382
+ this.expect('OP', '.');
383
+ const propName = this.advance()?.value ?? '';
384
+ this.expect('OP', '=');
385
+ const value = this.peek().type === 'STRING' ? this.advance()?.value ?? '' : this.advance()?.value ?? '';
386
+ let property = 'Name';
387
+ if (propName === 'Description')
388
+ property = 'Description';
389
+ else if (propName === 'Style')
390
+ property = 'Style';
391
+ else if (propName.toUpperCase() === 'OUT')
392
+ property = 'Out';
393
+ return {
394
+ identifier,
395
+ property,
396
+ value,
397
+ pos: { line: nameToken.line, column: nameToken.column, offset: nameToken.offset },
398
+ };
399
+ }
400
+ parseExpression() {
401
+ const nameToken = this.peek();
402
+ const name = this.advance()?.value ?? '';
403
+ if (!this.expect('OP', '='))
404
+ return null;
405
+ const expression = this.parseOrExpr();
406
+ return { name, expression, pos: { line: nameToken.line, column: nameToken.column, offset: nameToken.offset } };
407
+ }
408
+ parseOrExpr() {
409
+ let left = this.parseAndExpr();
410
+ while (this.isKeyword('OR')) {
411
+ this.advance();
412
+ const right = this.parseAndExpr();
413
+ left = { kind: 'gate', gateType: 'OR', inputs: [left, right] };
414
+ }
415
+ return left;
416
+ }
417
+ parseAndExpr() {
418
+ let left = this.parseNotExpr();
419
+ while (this.isKeyword('AND')) {
420
+ this.advance();
421
+ const right = this.parseNotExpr();
422
+ left = { kind: 'gate', gateType: 'AND', inputs: [left, right] };
423
+ }
424
+ return left;
425
+ }
426
+ parseNotExpr() {
427
+ if (this.isKeyword('NOT') && this.peekAt(1).value === '#') {
428
+ // NOT#ID(A) — function-call form with an instance id. Falls through to parsePrimary's
429
+ // AND/OR/NOT#ID handling, but intercept here so parseNotExpr doesn't consume the NOT.
430
+ return this.parsePrimary();
431
+ }
432
+ if (this.isKeyword('NOT')) {
433
+ this.advance();
434
+ const inner = this.parseNotExpr();
435
+ return { kind: 'gate', gateType: 'NOT', inputs: [inner] };
436
+ }
437
+ return this.parsePrimary();
438
+ }
439
+ parsePrimary() {
440
+ if (this.match('OP', '(')) {
441
+ const expr = this.parseOrExpr();
442
+ this.expect('OP', ')');
443
+ return expr;
444
+ }
445
+ const token = this.peek();
446
+ // AND#ID(...) / OR#ID(...) / NOT#ID(...) — function-call form with an instance id so the
447
+ // gate can carry .Name / .Description without the pass-through-intermediate trick.
448
+ if (token.type === 'KEYWORD' && (token.value === 'AND' || token.value === 'OR' || token.value === 'NOT')
449
+ && this.peekAt(1).value === '#') {
450
+ const gateType = this.advance().value;
451
+ this.advance(); // '#'
452
+ const id = this.advance()?.value ?? undefined;
453
+ this.expect('OP', '(');
454
+ const inputs = [];
455
+ if (this.peek().value !== ')') {
456
+ do {
457
+ inputs.push(this.parseOrExpr());
458
+ } while (this.match('OP', ','));
459
+ }
460
+ this.expect('OP', ')');
461
+ return { kind: 'gate', gateType, id, inputs };
462
+ }
463
+ if (token.type === 'SYMBOL_NAME') {
464
+ const symbolName = this.advance().value;
465
+ // Optional instance id is part of the name: BLOCK#id(...) or SYMBOL_NAME#ID.
466
+ const id = this.match('OP', '#') ? this.advance()?.value : undefined;
467
+ // A known block type followed by '(' is a function-block call.
468
+ if (BLOCK_TYPES.has(symbolName) && this.peek().value === '(') {
469
+ return this.parseBlockCall(symbolName, id);
470
+ }
471
+ const portName = this.match('OP', '.') ? this.advance()?.value : undefined;
472
+ if (id !== undefined || portName !== undefined) {
473
+ return { kind: 'symbolRef', symbolName, id, portName };
474
+ }
475
+ return { kind: 'port', name: symbolName };
476
+ }
477
+ if (token.type === 'IDENT') {
478
+ const name = this.advance().value;
479
+ return { kind: 'port', name };
480
+ }
481
+ this.errors.push({
482
+ message: `Unexpected token: '${token.value}' (${token.type})`,
483
+ line: token.line,
484
+ column: token.column,
485
+ offset: token.offset,
486
+ });
487
+ this.advance();
488
+ return { kind: 'port', name: '__error__' };
489
+ }
490
+ // Parse a SEL function-block call: BLOCK#id( arg, arg, NAME=value, ... ).port
491
+ // Arguments are signal expressions; durations/numbers are positional settings (PU then DO
492
+ // for a timer); `NAME=value` items are named settings (PU, DO, DOMINANT, ...).
493
+ parseBlockCall(blockType, id) {
494
+ this.expect('OP', '(');
495
+ const inputs = [];
496
+ const inputLabels = [];
497
+ const params = {};
498
+ let posNum = 0;
499
+ if (this.peek().value !== ')') {
500
+ do {
501
+ const t = this.peek();
502
+ const next = this.peekAt(1);
503
+ if ((t.type === 'IDENT' || t.type === 'SYMBOL_NAME') && next.value === '=') {
504
+ const key = this.advance().value;
505
+ this.advance(); // '='
506
+ if (blockType === 'FB') {
507
+ // A generic block's named argument is a labelled input port, not a setting.
508
+ inputs.push(this.parseOrExpr());
509
+ inputLabels.push(key);
510
+ }
511
+ else {
512
+ params[key.toUpperCase()] = this.advance()?.value ?? '';
513
+ }
514
+ }
515
+ else if ((t.type === 'DURATION' || t.type === 'NUMBER') && blockType !== 'FB') {
516
+ const v = this.advance().value;
517
+ if (blockType === 'TIMER')
518
+ params[posNum === 0 ? 'PU' : 'DO'] = v;
519
+ else
520
+ params[`P${posNum}`] = v;
521
+ posNum++;
522
+ }
523
+ else {
524
+ inputs.push(this.parseOrExpr());
525
+ inputLabels.push(undefined);
526
+ }
527
+ } while (this.match('OP', ','));
528
+ }
529
+ this.expect('OP', ')');
530
+ const port = this.match('OP', '.') ? this.advance()?.value : undefined;
531
+ return { kind: 'block', blockType, id, inputs, inputLabels, params, port };
532
+ }
533
+ parseObjectDecl() {
534
+ const nameToken = this.peek();
535
+ const symbolName = this.advance()?.value ?? '';
536
+ let id;
537
+ if (this.match('OP', '#')) {
538
+ id = this.advance()?.value;
539
+ }
540
+ return { symbolName, id, pos: { line: nameToken.line, column: nameToken.column, offset: nameToken.offset } };
541
+ }
542
+ parseAttributeOrPort() {
543
+ const nameToken = this.peek();
544
+ const objRef = this.advance()?.value ?? '';
545
+ let id;
546
+ if (this.peek().value === '#') {
547
+ this.advance();
548
+ id = this.advance()?.value;
549
+ }
550
+ this.expect('OP', '.');
551
+ const attrName = this.advance()?.value ?? '';
552
+ this.expect('OP', '=');
553
+ let value = '';
554
+ const valToken = this.peek();
555
+ if (valToken.type === 'STRING') {
556
+ value = this.advance()?.value ?? '';
557
+ }
558
+ else if (valToken.type === 'DURATION' || valToken.type === 'NUMBER' || valToken.type === 'IDENT') {
559
+ value = this.advance()?.value ?? '';
560
+ }
561
+ else if (valToken.type === 'KEYWORD' && (valToken.value === 'TRUE' || valToken.value === 'FALSE')) {
562
+ value = this.advance()?.value ?? '';
563
+ }
564
+ else {
565
+ const rhs = this.parseOrExpr();
566
+ value = rhs !== null ? '(expression)' : '';
567
+ }
568
+ return {
569
+ objectRef: objRef,
570
+ id,
571
+ attributeName: attrName,
572
+ value,
573
+ pos: { line: nameToken.line, column: nameToken.column, offset: nameToken.offset },
574
+ };
575
+ }
576
+ parseConnect() {
577
+ const fromObj = this.advance()?.value ?? '';
578
+ let fromId;
579
+ if (this.match('OP', '#')) {
580
+ fromId = this.advance()?.value;
581
+ }
582
+ this.expect('OP', '.');
583
+ const fromPort = this.advance()?.value ?? '';
584
+ const toObj = this.advance()?.value ?? '';
585
+ let toId;
586
+ if (this.match('OP', '#')) {
587
+ toId = this.advance()?.value;
588
+ }
589
+ this.expect('OP', '.');
590
+ const toPort = this.advance()?.value ?? '';
591
+ return { fromObject: fromObj, fromId, fromPort, toObject: toObj, toId, toPort };
592
+ }
593
+ parseStyleBlock() {
594
+ // Extract raw CSS source between STYLE and END STYLE, bypassing the tokenizer (which
595
+ // mangles CSS selectors like #G1, .ldl-fill, { fill: #fff3cd; } into individual tokens).
596
+ // The STYLE keyword has already been consumed; find the next END keyword and extract
597
+ // everything in the source between the end of this line and the END line.
598
+ const styleToken = this.tokens[Math.max(0, this.pos - 1)];
599
+ const startOffset = styleToken.offset + styleToken.value.length;
600
+ // Find the end of the STYLE line so CSS starts on the next line.
601
+ const lineEnd = this.source.indexOf('\n', startOffset);
602
+ const cssStart = lineEnd === -1 ? this.source.length : lineEnd + 1;
603
+ // Find the next standalone END keyword (on its own line, case-insensitive).
604
+ const endMatch = this.source.slice(cssStart).match(/\n\s*END\s*(\n|$)/i);
605
+ let cssEnd;
606
+ if (endMatch) {
607
+ cssEnd = cssStart + endMatch.index;
608
+ }
609
+ else {
610
+ cssEnd = this.source.length;
611
+ }
612
+ const css = this.source.slice(cssStart, cssEnd).trim();
613
+ // Skip tokens until past the END keyword that closes the STYLE block.
614
+ while (this.peek().type !== 'EOF' && !this.isKeyword('END')) {
615
+ this.advance();
616
+ }
617
+ this.match('KEYWORD', 'END');
618
+ return css;
619
+ }
620
+ }
621
+ export function parse(source) {
622
+ const { tokens, errors: lexErrors } = tokenize(source);
623
+ if (lexErrors.length > 0) {
624
+ return {
625
+ diagram: { outputs: [], objects: [], portMeta: [], attributes: [], connections: [], styles: [], options: [] },
626
+ errors: lexErrors,
627
+ };
628
+ }
629
+ const parser = new Parser(tokens, lexErrors, source);
630
+ const diagram = parser.parseDiagram();
631
+ return {
632
+ diagram,
633
+ errors: parser.errors,
634
+ };
635
+ }
@@ -0,0 +1,16 @@
1
+ export interface Vec2 {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ export interface GateObstacle {
6
+ x: number;
7
+ y: number;
8
+ w: number;
9
+ h: number;
10
+ id: string;
11
+ }
12
+ export interface RoutedSegment {
13
+ points: Vec2[];
14
+ fromId: string;
15
+ }
16
+ export declare function routeWireAStar(sourceX: number, sourceY: number, destX: number, destY: number, obstacles: GateObstacle[], sourceGateX: number, sourceGateY: number, sourceGateW: number, sourceGateH: number, destGateX: number, destGateY: number, destGateW: number, destGateH: number, destIsGate: boolean, routedSegments: RoutedSegment[], canvasW: number, canvasH: number, sameSourceFromId?: string): Vec2[];