@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,236 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Lexer } from './lexer';
3
+ import { Parser } from './parser';
4
+ describe('PlantUML Parser', () => {
5
+ const parse = (input) => {
6
+ const lexer = new Lexer(input);
7
+ const parser = new Parser(lexer);
8
+ return parser.parse();
9
+ };
10
+ describe('Basic Document Structure', () => {
11
+ it('should parse @startuml/@enduml block', () => {
12
+ const input = `
13
+ @startuml
14
+ @enduml
15
+ `;
16
+ const ast = parse(input);
17
+ expect(ast.kind).toBe('root');
18
+ expect(ast.meta.source).toBe('plantuml');
19
+ });
20
+ it('should parse title', () => {
21
+ const input = `
22
+ @startuml
23
+ title My Diagram
24
+ @enduml
25
+ `;
26
+ const ast = parse(input);
27
+ expect(ast.meta.title).toBe('My Diagram');
28
+ });
29
+ });
30
+ describe('Participants & Types', () => {
31
+ it('should parse various participant types', () => {
32
+ const input = `
33
+ @startuml
34
+ actor User
35
+ database DB
36
+ participant "Service Wrapper" as Svc
37
+ @enduml
38
+ `;
39
+ const ast = parse(input);
40
+ expect(ast.participants).toHaveLength(3);
41
+ expect(ast.participants[0]).toMatchObject({ id: 'User', name: 'User', type: 'actor' });
42
+ expect(ast.participants[1]).toMatchObject({ id: 'DB', name: 'DB', type: 'database' });
43
+ expect(ast.participants[2]).toMatchObject({ id: 'Svc', name: 'Service Wrapper', type: 'participant' });
44
+ });
45
+ });
46
+ describe('Messages & Arrows', () => {
47
+ it('should parse sync and reply messages', () => {
48
+ const input = `
49
+ @startuml
50
+ A -> B : Sync Call
51
+ B --> A : Reply
52
+ A -> A : Internal
53
+ @enduml
54
+ `;
55
+ const ast = parse(input);
56
+ expect(ast.events).toHaveLength(3);
57
+ expect(ast.events[0]).toMatchObject({
58
+ kind: 'message',
59
+ from: 'A',
60
+ to: 'B',
61
+ text: 'Sync Call',
62
+ type: 'sync',
63
+ style: { line: 'solid', head: 'arrow' }
64
+ });
65
+ expect(ast.events[1]).toMatchObject({
66
+ kind: 'message',
67
+ from: 'B',
68
+ to: 'A',
69
+ text: 'Reply',
70
+ type: 'reply',
71
+ style: { line: 'dotted', head: 'arrow' } // PlantUML --> is dotted arrow
72
+ });
73
+ expect(ast.events[2]).toMatchObject({
74
+ kind: 'message',
75
+ from: 'A',
76
+ to: 'A',
77
+ text: 'Internal'
78
+ });
79
+ });
80
+ });
81
+ describe('Lifecycle', () => {
82
+ it('should parse activate and deactivate', () => {
83
+ const input = `
84
+ @startuml
85
+ activate A
86
+ A -> B
87
+ deactivate A
88
+ @enduml
89
+ `;
90
+ const ast = parse(input);
91
+ expect(ast.events).toHaveLength(3);
92
+ expect(ast.events[0]).toMatchObject({ kind: 'activation', participantId: 'A', action: 'activate' });
93
+ expect(ast.events[1]).toMatchObject({ kind: 'message', from: 'A', to: 'B' });
94
+ expect(ast.events[2]).toMatchObject({ kind: 'activation', participantId: 'A', action: 'deactivate' });
95
+ });
96
+ });
97
+ describe('Notes', () => {
98
+ it('should parse notes with position', () => {
99
+ const input = `
100
+ @startuml
101
+ note left of A : Note Left
102
+ note right of A : Note Right
103
+ note over A : Note Over
104
+ note over A, B : Shared Note
105
+ @enduml
106
+ `;
107
+ const ast = parse(input);
108
+ expect(ast.events).toHaveLength(4);
109
+ expect(ast.events[0]).toMatchObject({ kind: 'note', position: 'left', participantIds: ['A'], text: 'Note Left' });
110
+ expect(ast.events[1]).toMatchObject({ kind: 'note', position: 'right', participantIds: ['A'], text: 'Note Right' });
111
+ expect(ast.events[2]).toMatchObject({ kind: 'note', position: 'over', participantIds: ['A'], text: 'Note Over' });
112
+ expect(ast.events[3]).toMatchObject({ kind: 'note', position: 'over', participantIds: ['A', 'B'], text: 'Shared Note' });
113
+ });
114
+ });
115
+ describe('Fragments', () => {
116
+ it('should parse alt/else/end', () => {
117
+ const input = `
118
+ @startuml
119
+ alt success
120
+ A -> B : OK
121
+ else failure
122
+ A -> B : Fail
123
+ end
124
+ @enduml
125
+ `;
126
+ const ast = parse(input);
127
+ expect(ast.events).toHaveLength(1);
128
+ const fragment = ast.events[0];
129
+ expect(fragment.kind).toBe('fragment');
130
+ expect(fragment.operator).toBe('alt');
131
+ expect(fragment.branches).toHaveLength(2);
132
+ expect(fragment.branches[0].condition).toBe('success');
133
+ expect(fragment.branches[0].events).toHaveLength(1);
134
+ expect(fragment.branches[1].condition).toBe('failure');
135
+ expect(fragment.branches[1].events).toHaveLength(1);
136
+ });
137
+ it('should parse opt/loop', () => {
138
+ const input = `
139
+ @startuml
140
+ opt maybe
141
+ A -> B : Maybe
142
+ end
143
+ loop forever
144
+ A -> B : Repeat
145
+ end
146
+ @enduml
147
+ `;
148
+ const ast = parse(input);
149
+ expect(ast.events).toHaveLength(2);
150
+ expect(ast.events[0].kind).toBe('fragment');
151
+ expect(ast.events[0].operator).toBe('opt');
152
+ expect(ast.events[1].kind).toBe('fragment');
153
+ expect(ast.events[1].operator).toBe('loop');
154
+ });
155
+ });
156
+ describe('Grouping', () => {
157
+ it('should parse box with participants', () => {
158
+ const input = `
159
+ @startuml
160
+ box "Internal Service" #LightBlue
161
+ participant A
162
+ participant B
163
+ end box
164
+ participant C
165
+ @enduml
166
+ `;
167
+ const ast = parse(input);
168
+ expect(ast.groups).toHaveLength(1);
169
+ const box = ast.groups[0];
170
+ expect(box.kind).toBe('group');
171
+ expect(box.name).toBe('Internal Service');
172
+ expect(box.style?.backgroundColor).toBe('#LightBlue');
173
+ expect(box.participantIds).toEqual(['A', 'B']);
174
+ expect(ast.participants).toHaveLength(3);
175
+ });
176
+ });
177
+ describe('Edge Cases', () => {
178
+ it('should ignore single line comments', () => {
179
+ const input = `
180
+ @startuml
181
+ ' This is a comment
182
+ A -> B : Hello ' Another comment? No, this is part of string usually?
183
+ ' Comment line
184
+ @enduml
185
+ `;
186
+ const ast = parse(input);
187
+ expect(ast.events).toHaveLength(1);
188
+ expect(ast.events[0]).toMatchObject({ text: "Hello ' Another comment? No, this is part of string usually?" });
189
+ });
190
+ it('should parse nested fragments', () => {
191
+ const input = `
192
+ @startuml
193
+ alt outer
194
+ A -> B : Outer
195
+ loop inner
196
+ A -> B : Inner
197
+ end
198
+ end
199
+ @enduml
200
+ `;
201
+ const ast = parse(input);
202
+ expect(ast.events).toHaveLength(1);
203
+ const outer = ast.events[0];
204
+ expect(outer.operator).toBe('alt');
205
+ const outerEvents = outer.branches[0].events;
206
+ expect(outerEvents).toHaveLength(2);
207
+ expect(outerEvents[0].kind).toBe('message');
208
+ const inner = outerEvents[1];
209
+ expect(inner.kind).toBe('fragment');
210
+ expect(inner.operator).toBe('loop');
211
+ expect(inner.branches[0].events).toHaveLength(1);
212
+ });
213
+ it('should parse multi-line notes', () => {
214
+ const input = `
215
+ @startuml
216
+ note left of A
217
+ Line 1
218
+ Line 2
219
+ end note
220
+ @enduml
221
+ `;
222
+ const ast = parse(input);
223
+ expect(ast.events).toHaveLength(1);
224
+ expect(ast.events[0]).toMatchObject({
225
+ kind: 'note',
226
+ position: 'left',
227
+ participantIds: ['A'],
228
+ // text should contain lines. trim() might remove leading/trailing.
229
+ // We probably want to preserve internal newlines.
230
+ });
231
+ const text = ast.events[0].text;
232
+ expect(text).toContain('Line 1');
233
+ expect(text).toContain('Line 2');
234
+ });
235
+ });
236
+ });
@@ -0,0 +1,9 @@
1
+ export type TokenType = 'START_UML' | 'END_UML' | 'NEWLINE' | 'EOF' | 'UNKNOWN' | 'IDENTIFIER' | 'STRING' | 'TITLE' | 'PARTICIPANT' | 'ACTOR' | 'DATABASE' | 'ACTIVATE' | 'DEACTIVATE' | 'NOTE' | 'OF' | 'LEFT' | 'RIGHT' | 'OVER' | 'ALT' | 'OPT' | 'LOOP' | 'ELSE' | 'END' | 'BOX' | 'AS' | 'ARROW' | 'COLON' | 'COMMA';
2
+ export interface Token {
3
+ type: TokenType;
4
+ literal: string;
5
+ line: number;
6
+ column: number;
7
+ start: number;
8
+ end: number;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
  import { TransformationEngine } from './engine';
3
3
  describe('TransformationEngine (Pipeline Integration)', () => {
4
4
  const createAst = (participants, events) => ({
@@ -38,4 +38,15 @@ describe('TransformationEngine (Pipeline Integration)', () => {
38
38
  // 3. Cleaner (Unused) should have removed C and D
39
39
  expect(result.participants.map(p => p.id).sort()).toEqual(['A0', 'B0']);
40
40
  });
41
+ it('handles unknown actions gracefully (identity)', () => {
42
+ const root = createAst([pA], [msgAB]);
43
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
44
+ // @ts-ignore - purposefully passing invalid action
45
+ const result = new TransformationEngine().transform(root, [{ action: 'invalid' }]);
46
+ expect(result).toEqual(root); // Logic remains identity, but object reference changes due to CoW cleaners
47
+ // Actually structure cleaner typically returns new object.
48
+ // But main point is it didn't crash.
49
+ expect(warnSpy).toHaveBeenCalledWith('Unknown action: invalid');
50
+ warnSpy.mockRestore();
51
+ });
41
52
  });
@@ -50,4 +50,21 @@ describe('Matcher', () => {
50
50
  expect(matcher.matchMessage(msg, { kind: 'message', to: 'db' })).toBe(false);
51
51
  });
52
52
  });
53
+ describe('Branch Matching', () => {
54
+ const branch = { id: 'b1', condition: 'valid', events: [] };
55
+ it('matches by operator', () => {
56
+ // Operator matching is against the PARENT fragment's operator
57
+ expect(matcher.matchBranch(branch, { kind: 'fragment', operator: 'alt' }, 'alt')).toBe(true);
58
+ expect(matcher.matchBranch(branch, { kind: 'fragment', operator: 'loop' }, 'alt')).toBe(false);
59
+ expect(matcher.matchBranch(branch, { kind: 'fragment', operator: ['alt', 'opt'] }, 'alt')).toBe(true);
60
+ });
61
+ it('matches by condition', () => {
62
+ expect(matcher.matchBranch(branch, { kind: 'fragment', condition: 'valid' }, 'alt')).toBe(true);
63
+ expect(matcher.matchBranch(branch, { kind: 'fragment', condition: 'invalid' }, 'alt')).toBe(false);
64
+ });
65
+ it('returns false if branch has no condition but selector requires one', () => {
66
+ const noCondBranch = { id: 'b2', events: [] };
67
+ expect(matcher.matchBranch(noCondBranch, { kind: 'fragment', condition: 'needed' }, 'alt')).toBe(false);
68
+ });
69
+ });
53
70
  });
@@ -2,6 +2,33 @@ import { describe, expect, it } from 'vitest';
2
2
  import { Walker } from './walker';
3
3
  class IdentityWalker extends Walker {
4
4
  }
5
+ class FilteringWalker extends Walker {
6
+ // Removes messages starting with 'DROP'
7
+ visitEvent(node) {
8
+ if (node.kind === 'message' && node.text.startsWith('DROP')) {
9
+ return [];
10
+ }
11
+ return super.visitEvent(node);
12
+ }
13
+ }
14
+ class MutatingWalker extends Walker {
15
+ // Uppercases message text
16
+ visitEvent(node) {
17
+ if (node.kind === 'message') {
18
+ return [{ ...node, text: node.text.toUpperCase() }];
19
+ }
20
+ return super.visitEvent(node);
21
+ }
22
+ }
23
+ class ExpandingWalker extends Walker {
24
+ // Duplicates messages
25
+ visitEvent(node) {
26
+ if (node.kind === 'message') {
27
+ return [node, { ...node, id: node.id + '_copy', text: node.text + ' (Copy)' }];
28
+ }
29
+ return super.visitEvent(node);
30
+ }
31
+ }
5
32
  describe('Walker (Base Traversal)', () => {
6
33
  const createAst = (events) => ({
7
34
  kind: 'root',
@@ -11,7 +38,7 @@ describe('Walker (Base Traversal)', () => {
11
38
  events
12
39
  });
13
40
  const msg = {
14
- kind: 'message', id: 'm1', text: 'M', from: 'A', to: 'B',
41
+ kind: 'message', id: 'm1', text: 'hello', from: 'A', to: 'B',
15
42
  type: 'sync', style: { line: 'solid', head: 'arrow' }
16
43
  };
17
44
  it('returns events as is by default', () => {
@@ -20,7 +47,7 @@ describe('Walker (Base Traversal)', () => {
20
47
  expect(result.events).toHaveLength(1);
21
48
  expect(result.events[0].id).toBe('m1');
22
49
  });
23
- it('recursively traverses fragments (Deep Identiy)', () => {
50
+ it('recursively traverses fragments (Deep Identity)', () => {
24
51
  const fragment = {
25
52
  kind: 'fragment', id: 'f1', operator: 'alt',
26
53
  branches: [
@@ -33,7 +60,7 @@ describe('Walker (Base Traversal)', () => {
33
60
  expect(resFrag.branches[0].events).toHaveLength(1);
34
61
  expect(resFrag.branches[0].events[0].id).toBe('m1');
35
62
  });
36
- it('does not mutate original AST (simple check)', () => {
63
+ it('does not mutate original AST', () => {
37
64
  const fragment = {
38
65
  kind: 'fragment', id: 'f1', operator: 'alt',
39
66
  branches: [
@@ -41,8 +68,44 @@ describe('Walker (Base Traversal)', () => {
41
68
  ]
42
69
  };
43
70
  const root = createAst([fragment]);
44
- const result = new IdentityWalker().transform(root); // Should create new ref if we were being strict about CoW, but standard map returns new array.
71
+ const result = new IdentityWalker().transform(root);
45
72
  expect(result).not.toBe(root); // Root object is new
46
73
  expect(result.events).not.toBe(root.events); // Events array is new (map)
47
74
  });
75
+ describe('Subclass Behaviors', () => {
76
+ it('supports filtering (removing nodes)', () => {
77
+ const dropMsg = { ...msg, id: 'm2', text: 'DROP me' };
78
+ const root = createAst([msg, dropMsg]);
79
+ const result = new FilteringWalker().transform(root);
80
+ expect(result.events).toHaveLength(1);
81
+ expect(result.events[0].id).toBe('m1');
82
+ });
83
+ it('supports mutation (modifying nodes)', () => {
84
+ const root = createAst([msg]);
85
+ const result = new MutatingWalker().transform(root);
86
+ expect(result.events[0].text).toBe('HELLO');
87
+ });
88
+ it('supports expansion (1->N nodes)', () => {
89
+ const root = createAst([msg]);
90
+ const result = new ExpandingWalker().transform(root);
91
+ expect(result.events).toHaveLength(2);
92
+ expect(result.events[0].text).toBe('hello');
93
+ expect(result.events[1].text).toBe('hello (Copy)');
94
+ });
95
+ it('handles nested mutations/filtering', () => {
96
+ const dropMsg = { ...msg, id: 'm2', text: 'DROP nested' };
97
+ const fragment = {
98
+ kind: 'fragment', id: 'f1', operator: 'loop',
99
+ branches: [
100
+ { id: 'b1', events: [msg, dropMsg] }
101
+ ]
102
+ };
103
+ const root = createAst([fragment]);
104
+ // Should filter inside the fragment
105
+ const result = new FilteringWalker().transform(root);
106
+ const resFrag = result.events[0];
107
+ expect(resFrag.branches[0].events).toHaveLength(1);
108
+ expect(resFrag.branches[0].events[0].text).toBe('hello');
109
+ });
110
+ });
48
111
  });