@polagram/core 0.0.3 → 0.0.4

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 (40) hide show
  1. package/dist/index.d.ts +104 -6
  2. package/dist/polagram-core.js +2689 -2157
  3. package/dist/polagram-core.umd.cjs +20 -14
  4. package/dist/src/api.d.ts +12 -3
  5. package/dist/src/api.js +26 -3
  6. package/dist/src/config/schema.d.ts +16 -0
  7. package/dist/src/config/schema.js +5 -1
  8. package/dist/src/generator/generators/plantuml.d.ts +17 -0
  9. package/dist/src/generator/generators/plantuml.js +131 -0
  10. package/dist/src/generator/generators/plantuml.test.d.ts +1 -0
  11. package/dist/src/generator/generators/plantuml.test.js +143 -0
  12. package/dist/src/index.d.ts +4 -0
  13. package/dist/src/index.js +4 -0
  14. package/dist/src/parser/base/lexer.d.ts +3 -3
  15. package/dist/src/parser/base/parser.d.ts +9 -9
  16. package/dist/src/parser/base/token.d.ts +18 -0
  17. package/dist/src/parser/base/token.js +1 -0
  18. package/dist/src/parser/base/tokens.d.ts +8 -0
  19. package/dist/src/parser/base/tokens.js +1 -0
  20. package/dist/src/parser/format-detector.d.ts +55 -0
  21. package/dist/src/parser/format-detector.js +98 -0
  22. package/dist/src/parser/index.d.ts +1 -0
  23. package/dist/src/parser/index.js +4 -0
  24. package/dist/src/parser/languages/mermaid/lexer.d.ts +1 -1
  25. package/dist/src/parser/languages/mermaid/parser.d.ts +2 -1
  26. package/dist/src/parser/languages/plantuml/index.d.ts +4 -0
  27. package/dist/src/parser/languages/plantuml/index.js +11 -0
  28. package/dist/src/parser/languages/plantuml/lexer.d.ts +15 -0
  29. package/dist/src/parser/languages/plantuml/lexer.js +143 -0
  30. package/dist/src/parser/languages/plantuml/parser.d.ts +23 -0
  31. package/dist/src/parser/languages/plantuml/parser.js +481 -0
  32. package/dist/src/parser/languages/plantuml/parser.test.d.ts +1 -0
  33. package/dist/src/parser/languages/plantuml/parser.test.js +236 -0
  34. package/dist/src/parser/languages/plantuml/tokens.d.ts +9 -0
  35. package/dist/src/parser/languages/plantuml/tokens.js +1 -0
  36. package/dist/src/transformer/orchestration/engine.test.js +12 -1
  37. package/dist/src/transformer/selector/matcher.test.js +17 -0
  38. package/dist/src/transformer/traverse/walker.test.js +67 -4
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +10 -9
@@ -0,0 +1,481 @@
1
+ import { BaseParser } from '../../base/parser';
2
+ export class Parser extends BaseParser {
3
+ constructor(lexer) {
4
+ super(lexer);
5
+ }
6
+ /**
7
+ * Type-safe token type checker.
8
+ * Helps TypeScript understand token type after advance() calls.
9
+ */
10
+ isTokenType(type) {
11
+ return this.currToken.type === type;
12
+ }
13
+ parse() {
14
+ const root = {
15
+ kind: 'root',
16
+ meta: { version: '1.0.0', source: 'plantuml' },
17
+ participants: [],
18
+ groups: [],
19
+ events: []
20
+ };
21
+ while (this.currToken.type !== 'EOF') {
22
+ if (this.currToken.type === 'START_UML') {
23
+ this.advance();
24
+ continue;
25
+ }
26
+ if (this.currToken.type === 'END_UML') {
27
+ this.advance();
28
+ continue;
29
+ }
30
+ if (this.currToken.type === 'TITLE') {
31
+ this.advance(); // eat title
32
+ root.meta.title = this.readRestOfLine().trim();
33
+ continue;
34
+ }
35
+ if (['PARTICIPANT', 'ACTOR', 'DATABASE'].includes(this.currToken.type)) {
36
+ this.parseParticipant(root);
37
+ continue;
38
+ }
39
+ // Implicit message/participant detection
40
+ // A -> B : text
41
+ // Identifier/String -> Arrow...
42
+ if (this.isParticipantToken(this.currToken)) {
43
+ const probMsg = this.parseMessage(root);
44
+ if (probMsg) {
45
+ root.events.push(probMsg);
46
+ continue;
47
+ }
48
+ }
49
+ if (this.currToken.type === 'ACTIVATE' || this.currToken.type === 'DEACTIVATE') {
50
+ const act = this.parseActivation(root);
51
+ if (act)
52
+ root.events.push(act);
53
+ continue;
54
+ }
55
+ if (this.currToken.type === 'NOTE') {
56
+ const note = this.parseNote(root);
57
+ if (note)
58
+ root.events.push(note);
59
+ continue;
60
+ }
61
+ if (['ALT', 'OPT', 'LOOP'].includes(this.currToken.type)) {
62
+ const fragment = this.parseFragment(root);
63
+ if (fragment)
64
+ root.events.push(fragment);
65
+ continue;
66
+ }
67
+ // Handle standalone 'end' if it appears outside (shouldn't if parsed recursively, but safeguard)
68
+ if (this.currToken.type === 'END') {
69
+ // If we are at root, 'end' might be closing a fragment.
70
+ // But parseFragment consumes until end.
71
+ // If we see it here, it's unmatched or nested logic needed.
72
+ // For simple recursive descent, we return to caller.
73
+ return root;
74
+ }
75
+ if (this.currToken.type === 'BOX') {
76
+ const group = this.parseGroup(root);
77
+ if (group)
78
+ root.groups.push(group);
79
+ continue;
80
+ }
81
+ this.advance();
82
+ }
83
+ return root;
84
+ }
85
+ parseGroup(root) {
86
+ this.advance(); // eat box
87
+ let name = '';
88
+ let backgroundColor;
89
+ // box "Title" #Color
90
+ if (this.currToken.type === 'STRING') {
91
+ name = this.currToken.literal;
92
+ this.advance();
93
+ }
94
+ // Check for color (starts with # usually, but lexer might tokenize it as UNKNOWN or need handling)
95
+ // PlantUML #Color is just text heavily.
96
+ // My lexer tokenizes # as UNKNOWN?
97
+ // Let's check lexer. It has no case for '#'.
98
+ // So it returns UNKNOWN.
99
+ if (this.currToken.type === 'UNKNOWN' && this.currToken.literal === '#') {
100
+ // Read color
101
+ // #LightBlue
102
+ // We need to read identifiers after #?
103
+ // Currently I don't have good color support in lexer.
104
+ // Quick hack: assume we are at #. Read next identifier.
105
+ this.advance(); // eat #
106
+ if (this.isTokenType('IDENTIFIER')) {
107
+ backgroundColor = '#' + this.currToken.literal;
108
+ this.advance();
109
+ }
110
+ }
111
+ const participantIds = [];
112
+ // Parse content until 'end box' (or just 'end')
113
+ while (this.currToken.type !== 'EOF') {
114
+ if (this.currToken.type === 'END') {
115
+ this.advance(); // eat end
116
+ if (this.isTokenType('BOX')) {
117
+ this.advance(); // eat box
118
+ }
119
+ break;
120
+ }
121
+ // We expect participant declarations inside box usually.
122
+ if (['PARTICIPANT', 'ACTOR', 'DATABASE'].includes(this.currToken.type)) {
123
+ // We need to capture the ID of the participant created.
124
+ // parseParticipant pushes to root.participants.
125
+ // We can check root.participants.length before and after? Or return ID from parseParticipant.
126
+ const lenBefore = root.participants.length;
127
+ this.parseParticipant(root);
128
+ const lenAfter = root.participants.length;
129
+ if (lenAfter > lenBefore) {
130
+ participantIds.push(root.participants[lenAfter - 1].id);
131
+ }
132
+ continue;
133
+ }
134
+ // If implicit participant? 'A'
135
+ if (this.currToken.type === 'IDENTIFIER') {
136
+ // Check if it is a participant decl without keyword? (PlantUML allows it)
137
+ // OR check if it is start of message?
138
+ // If start of message, participants might be already capable of being in group?
139
+ // Usually checking 'participant A' is safe.
140
+ // But implicit participants in box:
141
+ // box "Foo"
142
+ // A
143
+ // end box
144
+ // This declares A in box.
145
+ // But A -> B inside box?
146
+ // The message is an event. The participants are in the box?
147
+ // Only if they are first declared here.
148
+ // For MVP: Support explicit 'participant' inside box, OR just parse statements.
149
+ // If parseStatement returns null (participant decl), we need to capture ID.
150
+ // Let's rely on explicit participant keywords for now as per test case.
151
+ this.advance(); // skip other things
152
+ continue;
153
+ }
154
+ this.advance();
155
+ }
156
+ return {
157
+ kind: 'group',
158
+ id: 'group_' + (root.groups.length + 1),
159
+ name,
160
+ type: 'box',
161
+ participantIds,
162
+ style: backgroundColor ? { backgroundColor } : undefined
163
+ };
164
+ }
165
+ parseFragment(root) {
166
+ const kind = 'fragment';
167
+ const operator = this.currToken.literal.toLowerCase(); // alt, opt, loop
168
+ this.advance(); // eat keyword
169
+ const condition = this.readRestOfLine().trim();
170
+ const branches = [];
171
+ let currentEvents = [];
172
+ const currentBranch = { condition, events: currentEvents };
173
+ branches.push(currentBranch);
174
+ // We need to parse block content until ELSE or END
175
+ while (this.currToken.type !== 'EOF') {
176
+ if (this.currToken.type === 'END') {
177
+ this.advance(); // eat end
178
+ // Check if it is 'end box' or just 'end'?
179
+ // PlantUML has 'end' for fragments.
180
+ // Also 'end note', 'end box'.
181
+ // For now assume 'end' closes fragment.
182
+ break;
183
+ }
184
+ if (this.currToken.type === 'ELSE') {
185
+ this.advance(); // eat else
186
+ // New branch
187
+ const elseCond = this.readRestOfLine().trim();
188
+ currentEvents = [];
189
+ branches.push({ condition: elseCond, events: currentEvents });
190
+ continue;
191
+ }
192
+ // Parse single line event or nested structure
193
+ // We can reuse the main loop logic effectively if we refactor 'parseBlock'
194
+ // For now, let's duplicate the switch logic or call a recursive 'parseStatement'
195
+ // Simulating parseStatement step:
196
+ if (this.currToken.type === 'NEWLINE') {
197
+ this.advance();
198
+ continue;
199
+ }
200
+ // Recursively call a helper that processes ONE statement
201
+ const event = this.parseStatement(root); // We need this helper!
202
+ if (event) {
203
+ currentEvents.push(event);
204
+ }
205
+ else {
206
+ // If not returned an event (e.g. participant decl), we might still advance?
207
+ // parseStatement should handle everything inside block.
208
+ // But 'parseStatement' needs to be extracted from parse().
209
+ }
210
+ }
211
+ return {
212
+ kind,
213
+ id: 'frag_' + (root.events.length + 1),
214
+ operator,
215
+ branches
216
+ };
217
+ }
218
+ // Refactor parse() to use parseStatement
219
+ parseStatement(root) {
220
+ if (['PARTICIPANT', 'ACTOR', 'DATABASE'].includes(this.currToken.type)) {
221
+ this.parseParticipant(root);
222
+ return null; // Not an event
223
+ }
224
+ if (this.isParticipantToken(this.currToken)) {
225
+ const probMsg = this.parseMessage(root);
226
+ if (probMsg)
227
+ return probMsg;
228
+ }
229
+ if (this.currToken.type === 'ACTIVATE' || this.currToken.type === 'DEACTIVATE') {
230
+ return this.parseActivation(root);
231
+ }
232
+ if (this.currToken.type === 'NOTE') {
233
+ return this.parseNote(root);
234
+ }
235
+ if (['ALT', 'OPT', 'LOOP'].includes(this.currToken.type)) {
236
+ return this.parseFragment(root);
237
+ }
238
+ this.advance();
239
+ return null;
240
+ }
241
+ parseNote(root) {
242
+ this.advance(); // eat note
243
+ let position = 'over'; // default
244
+ // note left of A
245
+ // note right of A
246
+ // note over A
247
+ if (this.currToken.type === 'LEFT') {
248
+ position = 'left';
249
+ this.advance();
250
+ }
251
+ else if (this.currToken.type === 'RIGHT') {
252
+ position = 'right';
253
+ this.advance();
254
+ }
255
+ else if (this.currToken.type === 'OVER') {
256
+ position = 'over';
257
+ this.advance();
258
+ }
259
+ if (this.currToken.type === 'OF') {
260
+ this.advance();
261
+ }
262
+ const participantIds = [];
263
+ while (this.isParticipantToken(this.currToken)) {
264
+ participantIds.push(this.currToken.literal);
265
+ this.ensureParticipant(root, this.currToken.literal);
266
+ this.advance();
267
+ if (this.currToken.type === 'COMMA') {
268
+ this.advance();
269
+ }
270
+ else {
271
+ break;
272
+ }
273
+ }
274
+ let text = '';
275
+ if (this.currToken.type === 'COLON') {
276
+ this.advance();
277
+ text = this.readRestOfLine().trim();
278
+ }
279
+ else {
280
+ // Multi-line note
281
+ if (this.currToken.type === 'NEWLINE') {
282
+ this.advance();
283
+ }
284
+ const start = this.currToken.start;
285
+ let end = start;
286
+ while (this.currToken.type !== 'EOF') {
287
+ if (this.currToken.type === 'END' && this.peekToken.type === 'NOTE') {
288
+ end = this.currToken.start;
289
+ this.advance(); // eat end
290
+ this.advance(); // eat note
291
+ break;
292
+ }
293
+ this.advance();
294
+ }
295
+ const input = this.lexer.getInput();
296
+ text = input.slice(start, end).trim();
297
+ }
298
+ return {
299
+ kind: 'note',
300
+ id: 'note_' + (root.events.length + 1),
301
+ position,
302
+ participantIds,
303
+ text
304
+ };
305
+ }
306
+ parseActivation(root) {
307
+ const action = this.currToken.type === 'ACTIVATE' ? 'activate' : 'deactivate';
308
+ this.advance(); // eat keyword
309
+ let participantId = '';
310
+ if (this.isParticipantToken(this.currToken)) {
311
+ participantId = this.currToken.literal;
312
+ this.ensureParticipant(root, participantId);
313
+ this.advance();
314
+ }
315
+ else {
316
+ return null; // Error
317
+ }
318
+ return {
319
+ kind: 'activation',
320
+ participantId,
321
+ action
322
+ };
323
+ }
324
+ isParticipantToken(tok) {
325
+ return tok.type === 'IDENTIFIER' || tok.type === 'STRING';
326
+ }
327
+ parseMessage(root) {
328
+ if (this.peekToken.type !== 'ARROW') {
329
+ // Maybe it's just a participant declaration implied? 'A' on its own line?
330
+ // PlantUML 'A' is valid. It creates participant A.
331
+ // But here we look for message.
332
+ return null;
333
+ }
334
+ const fromId = this.currToken.literal; // simple ID for now. If quoted string, use it as ID/Name.
335
+ this.ensureParticipant(root, fromId);
336
+ this.advance(); // eat from
337
+ const arrow = this.currToken.literal; // -> or -->
338
+ this.advance(); // eat arrow
339
+ if (!this.isParticipantToken(this.currToken)) {
340
+ return null; // Error?
341
+ }
342
+ const toId = this.currToken.literal;
343
+ this.ensureParticipant(root, toId);
344
+ this.advance(); // eat to
345
+ let text = '';
346
+ if (this.currToken.type === 'COLON') {
347
+ this.advance(); // eat colon
348
+ text = this.readRestOfLine().trim();
349
+ }
350
+ // Resolve arrow style
351
+ let type = 'sync';
352
+ let style = { line: 'solid', head: 'arrow' };
353
+ if (arrow === '-->') {
354
+ type = 'reply';
355
+ style = { line: 'dotted', head: 'arrow' };
356
+ }
357
+ else if (arrow === '->') {
358
+ type = 'sync';
359
+ style = { line: 'solid', head: 'arrow' };
360
+ }
361
+ return {
362
+ kind: 'message',
363
+ id: 'msg_' + (root.events.length + 1), // Simple ID generation
364
+ from: fromId,
365
+ to: toId,
366
+ text,
367
+ type,
368
+ style
369
+ };
370
+ }
371
+ ensureParticipant(root, id) {
372
+ if (!root.participants.find(p => p.id === id)) {
373
+ root.participants.push({
374
+ id,
375
+ name: id,
376
+ type: 'participant'
377
+ });
378
+ }
379
+ }
380
+ parseParticipant(root) {
381
+ const typeStr = this.currToken.type; // ACTOR, DATABASE, PARTICIPANT
382
+ let type = 'participant'; // Default
383
+ if (typeStr === 'ACTOR')
384
+ type = 'actor';
385
+ if (typeStr === 'DATABASE')
386
+ type = 'database';
387
+ this.advance(); // eat keyword
388
+ // console.log('DEBUG: parseParticipant token:', this.currToken.type, this.currToken.literal);
389
+ let name = '';
390
+ let id = '';
391
+ // Name/ID
392
+ if (this.currToken.type === 'STRING' || this.currToken.type === 'IDENTIFIER') {
393
+ name = this.currToken.literal;
394
+ id = name; // Default ID is name (unless as is used)
395
+ // If name has spaces (quoted), ID usually needs alias to be usable without quotes?
396
+ // PlantUML: participant "Long Name" as A
397
+ // ID = A, Name = "Long Name"
398
+ // PlantUML: participant A
399
+ // ID = A, Name = A
400
+ // But strict PlantUML uses the Alias as the ID for arrows.
401
+ this.advance();
402
+ }
403
+ if (this.currToken.type === 'AS') {
404
+ this.advance(); // eat as
405
+ if (this.isTokenType('IDENTIFIER')) {
406
+ id = this.currToken.literal; // "Long Name" as Svc -> Svc is ID
407
+ this.advance();
408
+ }
409
+ }
410
+ else {
411
+ // If "Long Name" is given without 'as', usually we treat name as ID if safe?
412
+ // But usually we sanitize.
413
+ // For now follow logic: A as B -> ID=B, Name=A.
414
+ // If just A -> ID=A, Name=A.
415
+ // If "A B" -> ID="A B", Name="A B".
416
+ // In Core AST, ID is the references key.
417
+ // If name was quoted "Service Wrapper", without ID, it is hard to reference.
418
+ // Unless we reference using Quotes?
419
+ // Let's assume input is valid alias for now.
420
+ // Wait, Step 93 test case:
421
+ // participant "Service Wrapper" as Svc
422
+ // name="Service Wrapper", id="Svc"
423
+ }
424
+ // If we found 'as', id was updated.
425
+ // If we didn't find 'as' (e.g. actor User), id = User, name = User.
426
+ // But wait:
427
+ // case: participant "Service Wrapper" as Svc
428
+ // 1. Keyword participant.
429
+ // 2. String "Service Wrapper". name = "Service Wrapper", id="Service Wrapper".
430
+ // 3. AS.
431
+ // 4. Identifier Svc. id="Svc".
432
+ // correct.
433
+ root.participants.push({
434
+ id,
435
+ name,
436
+ type
437
+ });
438
+ }
439
+ readRestOfLine() {
440
+ // We need to sync/consume tokens until NEWLINE
441
+ // But since we want raw text, we should ask lexer.
442
+ // However, we effectively already consumed 'current token' if we are here?
443
+ // Usually we call readRestOfLine AFTER consuming the label (e.g. COLON).
444
+ // So currToken should be the first token of the text?
445
+ // But lexer might have already tokenized it into multiple tokens.
446
+ // If we simply call lexer.readRestOfLine(), it continues from CURRENT lexer position.
447
+ // currToken is the token *already read*.
448
+ // peekToken is the next one.
449
+ // Parser is usually one step behind or ahead?
450
+ // BaseParser: this.currToken, this.peekToken.
451
+ // Parsing process:
452
+ // 1. nextToken called for currToken.
453
+ // 2. nextToken called for peekToken.
454
+ // So Lexer is at position AFTER peekToken.
455
+ // If we want "rest of line from currToken", we are in trouble because Lexer is far ahead.
456
+ // Alternative:
457
+ // Reconstruct text from tokens until NEWLINE.
458
+ // But tokens don't enforce whitespace rules strictly?
459
+ // We capture literal.
460
+ // Wait, `Token` has `start` and `end`?
461
+ // Yes: { type, literal, start, end }
462
+ // We can use the start of currToken and end of the last token before NEWLINE to slice from source?
463
+ // We don't have easy access to source in strict BaseParser (it's in Lexer).
464
+ // But we can access `(this.lexer as Lexer).input`.
465
+ if (this.currToken.type === 'NEWLINE' || this.currToken.type === 'EOF')
466
+ return '';
467
+ const start = this.currToken.start;
468
+ let end = this.currToken.end;
469
+ while (!this.isTokenType('NEWLINE') && !this.isTokenType('EOF')) {
470
+ end = this.currToken.end;
471
+ this.advance();
472
+ }
473
+ // We advanced past the last text token. currToken is now NEWLINE.
474
+ // The previous token ended at `end`.
475
+ // We need access to input.
476
+ const input = this.lexer.getInput(); // BaseLexer usually carries input?
477
+ // BaseLexer: protected input: string;
478
+ // We might need to make it public accessor or cast.
479
+ return input.slice(start, end).trim();
480
+ }
481
+ }