@polagram/core 0.0.4 → 0.1.1
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/index.d.ts +102 -113
- package/dist/polagram-core.js +1524 -1392
- package/dist/polagram-core.umd.cjs +23 -22
- package/package.json +3 -2
- package/dist/src/api.d.ts +0 -84
- package/dist/src/api.js +0 -183
- package/dist/src/ast/ast.test.d.ts +0 -1
- package/dist/src/ast/ast.test.js +0 -146
- package/dist/src/ast/index.d.ts +0 -119
- package/dist/src/ast/index.js +0 -2
- package/dist/src/config/index.d.ts +0 -1
- package/dist/src/config/index.js +0 -1
- package/dist/src/config/schema.d.ts +0 -198
- package/dist/src/config/schema.js +0 -82
- package/dist/src/config/schema.test.d.ts +0 -1
- package/dist/src/config/schema.test.js +0 -94
- package/dist/src/generator/base/walker.d.ts +0 -19
- package/dist/src/generator/base/walker.js +0 -56
- package/dist/src/generator/base/walker.test.d.ts +0 -1
- package/dist/src/generator/base/walker.test.js +0 -49
- package/dist/src/generator/generators/mermaid.d.ts +0 -24
- package/dist/src/generator/generators/mermaid.js +0 -140
- package/dist/src/generator/generators/mermaid.test.d.ts +0 -1
- package/dist/src/generator/generators/mermaid.test.js +0 -70
- package/dist/src/generator/generators/plantuml.d.ts +0 -17
- package/dist/src/generator/generators/plantuml.js +0 -131
- package/dist/src/generator/generators/plantuml.test.d.ts +0 -1
- package/dist/src/generator/generators/plantuml.test.js +0 -143
- package/dist/src/generator/interface.d.ts +0 -17
- package/dist/src/generator/interface.js +0 -1
- package/dist/src/index.d.ts +0 -13
- package/dist/src/index.js +0 -21
- package/dist/src/parser/base/lexer.d.ts +0 -18
- package/dist/src/parser/base/lexer.js +0 -95
- package/dist/src/parser/base/lexer.test.d.ts +0 -1
- package/dist/src/parser/base/lexer.test.js +0 -53
- package/dist/src/parser/base/parser.d.ts +0 -14
- package/dist/src/parser/base/parser.js +0 -43
- package/dist/src/parser/base/parser.test.d.ts +0 -1
- package/dist/src/parser/base/parser.test.js +0 -90
- package/dist/src/parser/base/token.d.ts +0 -18
- package/dist/src/parser/base/token.js +0 -1
- package/dist/src/parser/base/tokens.d.ts +0 -8
- package/dist/src/parser/base/tokens.js +0 -1
- package/dist/src/parser/format-detector.d.ts +0 -55
- package/dist/src/parser/format-detector.js +0 -98
- package/dist/src/parser/index.d.ts +0 -11
- package/dist/src/parser/index.js +0 -33
- package/dist/src/parser/index.test.d.ts +0 -1
- package/dist/src/parser/index.test.js +0 -23
- package/dist/src/parser/interface.d.ts +0 -8
- package/dist/src/parser/interface.js +0 -1
- package/dist/src/parser/languages/mermaid/constants.d.ts +0 -7
- package/dist/src/parser/languages/mermaid/constants.js +0 -20
- package/dist/src/parser/languages/mermaid/index.d.ts +0 -4
- package/dist/src/parser/languages/mermaid/index.js +0 -11
- package/dist/src/parser/languages/mermaid/lexer.d.ts +0 -14
- package/dist/src/parser/languages/mermaid/lexer.js +0 -152
- package/dist/src/parser/languages/mermaid/lexer.test.d.ts +0 -1
- package/dist/src/parser/languages/mermaid/lexer.test.js +0 -58
- package/dist/src/parser/languages/mermaid/parser.d.ts +0 -22
- package/dist/src/parser/languages/mermaid/parser.js +0 -340
- package/dist/src/parser/languages/mermaid/parser.test.d.ts +0 -1
- package/dist/src/parser/languages/mermaid/parser.test.js +0 -252
- package/dist/src/parser/languages/mermaid/tokens.d.ts +0 -9
- package/dist/src/parser/languages/mermaid/tokens.js +0 -1
- package/dist/src/parser/languages/plantuml/index.d.ts +0 -4
- package/dist/src/parser/languages/plantuml/index.js +0 -11
- package/dist/src/parser/languages/plantuml/lexer.d.ts +0 -15
- package/dist/src/parser/languages/plantuml/lexer.js +0 -143
- package/dist/src/parser/languages/plantuml/parser.d.ts +0 -23
- package/dist/src/parser/languages/plantuml/parser.js +0 -481
- package/dist/src/parser/languages/plantuml/parser.test.d.ts +0 -1
- package/dist/src/parser/languages/plantuml/parser.test.js +0 -236
- package/dist/src/parser/languages/plantuml/tokens.d.ts +0 -9
- package/dist/src/parser/languages/plantuml/tokens.js +0 -1
- package/dist/src/transformer/cleaners/prune-empty.d.ts +0 -9
- package/dist/src/transformer/cleaners/prune-empty.js +0 -27
- package/dist/src/transformer/cleaners/prune-empty.test.d.ts +0 -1
- package/dist/src/transformer/cleaners/prune-empty.test.js +0 -69
- package/dist/src/transformer/cleaners/prune-unused.d.ts +0 -5
- package/dist/src/transformer/cleaners/prune-unused.js +0 -48
- package/dist/src/transformer/cleaners/prune-unused.test.d.ts +0 -1
- package/dist/src/transformer/cleaners/prune-unused.test.js +0 -71
- package/dist/src/transformer/filters/focus.d.ts +0 -13
- package/dist/src/transformer/filters/focus.js +0 -71
- package/dist/src/transformer/filters/focus.test.d.ts +0 -1
- package/dist/src/transformer/filters/focus.test.js +0 -50
- package/dist/src/transformer/filters/remove.d.ts +0 -12
- package/dist/src/transformer/filters/remove.js +0 -82
- package/dist/src/transformer/filters/remove.test.d.ts +0 -1
- package/dist/src/transformer/filters/remove.test.js +0 -38
- package/dist/src/transformer/filters/resolve.d.ts +0 -9
- package/dist/src/transformer/filters/resolve.js +0 -32
- package/dist/src/transformer/filters/resolve.test.d.ts +0 -1
- package/dist/src/transformer/filters/resolve.test.js +0 -48
- package/dist/src/transformer/index.d.ts +0 -10
- package/dist/src/transformer/index.js +0 -10
- package/dist/src/transformer/lens.d.ts +0 -12
- package/dist/src/transformer/lens.js +0 -58
- package/dist/src/transformer/lens.test.d.ts +0 -1
- package/dist/src/transformer/lens.test.js +0 -60
- package/dist/src/transformer/orchestration/engine.d.ts +0 -5
- package/dist/src/transformer/orchestration/engine.js +0 -24
- package/dist/src/transformer/orchestration/engine.test.d.ts +0 -1
- package/dist/src/transformer/orchestration/engine.test.js +0 -52
- package/dist/src/transformer/orchestration/registry.d.ts +0 -10
- package/dist/src/transformer/orchestration/registry.js +0 -27
- package/dist/src/transformer/selector/matcher.d.ts +0 -9
- package/dist/src/transformer/selector/matcher.js +0 -62
- package/dist/src/transformer/selector/matcher.test.d.ts +0 -1
- package/dist/src/transformer/selector/matcher.test.js +0 -70
- package/dist/src/transformer/traverse/walker.d.ts +0 -14
- package/dist/src/transformer/traverse/walker.js +0 -67
- package/dist/src/transformer/traverse/walker.test.d.ts +0 -1
- package/dist/src/transformer/traverse/walker.test.js +0 -111
- package/dist/src/transformer/types.d.ts +0 -47
- package/dist/src/transformer/types.js +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -1,236 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { EventNode, FragmentNode } from '../../ast';
|
|
2
|
-
import { Walker } from '../traverse/walker';
|
|
3
|
-
/**
|
|
4
|
-
* Cleaner that removes empty fragments and branches from the AST.
|
|
5
|
-
* This runs after filters to ensure the AST structure remains valid/clean.
|
|
6
|
-
*/
|
|
7
|
-
export declare class StructureCleaner extends Walker {
|
|
8
|
-
protected visitFragment(node: FragmentNode): EventNode[];
|
|
9
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Walker } from '../traverse/walker';
|
|
2
|
-
/**
|
|
3
|
-
* Cleaner that removes empty fragments and branches from the AST.
|
|
4
|
-
* This runs after filters to ensure the AST structure remains valid/clean.
|
|
5
|
-
*/
|
|
6
|
-
export class StructureCleaner extends Walker {
|
|
7
|
-
visitFragment(node) {
|
|
8
|
-
// 1. Let super map the branches (recurse)
|
|
9
|
-
const result = super.visitFragment(node);
|
|
10
|
-
// Should contain exactly one node (the fragment with mapped branches)
|
|
11
|
-
if (result.length === 0)
|
|
12
|
-
return [];
|
|
13
|
-
const fragment = result[0];
|
|
14
|
-
// 2. Filter out empty branches
|
|
15
|
-
// A branch is "empty" if it has 0 events.
|
|
16
|
-
const validBranches = fragment.branches.filter(b => b.events.length > 0);
|
|
17
|
-
// 3. Evaluation
|
|
18
|
-
if (validBranches.length === 0) {
|
|
19
|
-
return []; // Remove fragment entirely
|
|
20
|
-
}
|
|
21
|
-
// Return updated fragment
|
|
22
|
-
return [{
|
|
23
|
-
...fragment,
|
|
24
|
-
branches: validBranches
|
|
25
|
-
}];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { StructureCleaner } from './prune-empty';
|
|
3
|
-
describe('StructureCleaner', () => {
|
|
4
|
-
const createAst = (events) => ({
|
|
5
|
-
kind: 'root',
|
|
6
|
-
meta: { version: '1', source: 'unknown' },
|
|
7
|
-
participants: [],
|
|
8
|
-
groups: [],
|
|
9
|
-
events
|
|
10
|
-
});
|
|
11
|
-
const msg = {
|
|
12
|
-
kind: 'message',
|
|
13
|
-
id: 'm1',
|
|
14
|
-
text: 'M',
|
|
15
|
-
from: 'A',
|
|
16
|
-
to: 'B',
|
|
17
|
-
type: 'sync',
|
|
18
|
-
style: { line: 'solid', head: 'arrow' }
|
|
19
|
-
};
|
|
20
|
-
it('removes fragment if it has no branches', () => {
|
|
21
|
-
const fragment = {
|
|
22
|
-
kind: 'fragment', id: 'f1', operator: 'alt',
|
|
23
|
-
branches: [] // Empty
|
|
24
|
-
};
|
|
25
|
-
const root = createAst([fragment, msg]);
|
|
26
|
-
const result = new StructureCleaner().transform(root);
|
|
27
|
-
expect(result.events).toHaveLength(1);
|
|
28
|
-
expect(result.events[0].id).toBe('m1');
|
|
29
|
-
});
|
|
30
|
-
it('removes branch if it has no events', () => {
|
|
31
|
-
const fragment = {
|
|
32
|
-
kind: 'fragment', id: 'f1', operator: 'alt',
|
|
33
|
-
branches: [
|
|
34
|
-
{ id: 'b1', condition: 'C1', events: [] }, // Empty
|
|
35
|
-
{ id: 'b2', condition: 'C2', events: [msg] } // Should keep
|
|
36
|
-
]
|
|
37
|
-
};
|
|
38
|
-
const root = createAst([fragment]);
|
|
39
|
-
const result = new StructureCleaner().transform(root);
|
|
40
|
-
const resFrag = result.events[0];
|
|
41
|
-
expect(resFrag.branches).toHaveLength(1);
|
|
42
|
-
expect(resFrag.branches[0].id).toBe('b2');
|
|
43
|
-
});
|
|
44
|
-
it('removes fragment if all branches are empty', () => {
|
|
45
|
-
const fragment = {
|
|
46
|
-
kind: 'fragment', id: 'f1', operator: 'alt',
|
|
47
|
-
branches: [
|
|
48
|
-
{ id: 'b1', condition: 'C1', events: [] }
|
|
49
|
-
]
|
|
50
|
-
};
|
|
51
|
-
const root = createAst([fragment]);
|
|
52
|
-
const result = new StructureCleaner().transform(root);
|
|
53
|
-
expect(result.events).toHaveLength(0);
|
|
54
|
-
});
|
|
55
|
-
it('recursively cleans nested fragments', () => {
|
|
56
|
-
const innerFrag = {
|
|
57
|
-
kind: 'fragment', id: 'inner', operator: 'loop',
|
|
58
|
-
branches: [{ id: 'ib', condition: 'loop', events: [] }]
|
|
59
|
-
};
|
|
60
|
-
const outerFrag = {
|
|
61
|
-
kind: 'fragment', id: 'outer', operator: 'opt',
|
|
62
|
-
branches: [{ id: 'ob', condition: 'opt', events: [innerFrag] }]
|
|
63
|
-
};
|
|
64
|
-
const root = createAst([outerFrag, msg]);
|
|
65
|
-
const result = new StructureCleaner().transform(root);
|
|
66
|
-
expect(result.events).toHaveLength(1);
|
|
67
|
-
expect(result.events[0].id).toBe('m1');
|
|
68
|
-
});
|
|
69
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
export class UnusedCleaner {
|
|
2
|
-
transform(root) {
|
|
3
|
-
const usedIds = this.collectUsedParticipants(root.events);
|
|
4
|
-
// Filter participants
|
|
5
|
-
const activeParticipants = root.participants.filter(p => usedIds.has(p.id));
|
|
6
|
-
// Filter groups (remove if empty, clean up member lists)
|
|
7
|
-
const activeGroups = root.groups.map(g => ({
|
|
8
|
-
...g,
|
|
9
|
-
participantIds: g.participantIds.filter(pid => usedIds.has(pid))
|
|
10
|
-
})).filter(g => g.participantIds.length > 0);
|
|
11
|
-
return {
|
|
12
|
-
...root,
|
|
13
|
-
participants: activeParticipants,
|
|
14
|
-
groups: activeGroups
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
collectUsedParticipants(events) {
|
|
18
|
-
const used = new Set();
|
|
19
|
-
function scan(nodes) {
|
|
20
|
-
for (const node of nodes) {
|
|
21
|
-
switch (node.kind) {
|
|
22
|
-
case 'message':
|
|
23
|
-
if (node.from)
|
|
24
|
-
used.add(node.from);
|
|
25
|
-
if (node.to)
|
|
26
|
-
used.add(node.to);
|
|
27
|
-
break;
|
|
28
|
-
case 'fragment':
|
|
29
|
-
for (const branch of node.branches) {
|
|
30
|
-
scan(branch.events);
|
|
31
|
-
}
|
|
32
|
-
break;
|
|
33
|
-
case 'activation':
|
|
34
|
-
used.add(node.participantId);
|
|
35
|
-
break;
|
|
36
|
-
case 'note':
|
|
37
|
-
node.participantIds.forEach(id => used.add(id));
|
|
38
|
-
break;
|
|
39
|
-
case 'ref':
|
|
40
|
-
node.participantIds.forEach(id => used.add(id));
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
scan(events);
|
|
46
|
-
return used;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { UnusedCleaner } from './prune-unused';
|
|
3
|
-
describe('UnusedCleaner', () => {
|
|
4
|
-
const createAst = (participants, events) => ({
|
|
5
|
-
kind: 'root',
|
|
6
|
-
meta: { version: '1', source: 'unknown' },
|
|
7
|
-
participants,
|
|
8
|
-
groups: [],
|
|
9
|
-
events
|
|
10
|
-
});
|
|
11
|
-
it('removes participants that are not referenced in messages', () => {
|
|
12
|
-
const p1 = { id: 'A', name: 'Alice', type: 'participant' };
|
|
13
|
-
const p2 = { id: 'B', name: 'Bob', type: 'participant' };
|
|
14
|
-
const p3 = { id: 'C', name: 'Charlie', type: 'participant' }; // Unused
|
|
15
|
-
const msg = {
|
|
16
|
-
kind: 'message', id: 'm1', text: 'Hi', from: 'A', to: 'B',
|
|
17
|
-
type: 'sync', style: { line: 'solid', head: 'arrow' }
|
|
18
|
-
};
|
|
19
|
-
const root = createAst([p1, p2, p3], [msg]);
|
|
20
|
-
const result = new UnusedCleaner().transform(root);
|
|
21
|
-
expect(result.participants).toHaveLength(2);
|
|
22
|
-
expect(result.participants.map(p => p.id)).toEqual(['A', 'B']);
|
|
23
|
-
});
|
|
24
|
-
it('keeps participants referenced in activations', () => {
|
|
25
|
-
const p1 = { id: 'A', name: 'Alice', type: 'participant' };
|
|
26
|
-
const act = { kind: 'activation', participantId: 'A', action: 'activate' };
|
|
27
|
-
const root = createAst([p1], [act]);
|
|
28
|
-
const result = new UnusedCleaner().transform(root);
|
|
29
|
-
expect(result.participants).toHaveLength(1);
|
|
30
|
-
});
|
|
31
|
-
it('keeps participants referenced in notes', () => {
|
|
32
|
-
const p1 = { id: 'A', name: 'Alice', type: 'participant' };
|
|
33
|
-
const note = { kind: 'note', id: 'n1', text: 'Note', position: 'over', participantIds: ['A'] };
|
|
34
|
-
const root = createAst([p1], [note]);
|
|
35
|
-
const result = new UnusedCleaner().transform(root);
|
|
36
|
-
expect(result.participants).toHaveLength(1);
|
|
37
|
-
});
|
|
38
|
-
it('cleans up groups removing unused members', () => {
|
|
39
|
-
const p1 = { id: 'A', name: 'Alice', type: 'participant' };
|
|
40
|
-
const p2 = { id: 'B', name: 'Bob', type: 'participant' }; // Unused
|
|
41
|
-
const group = { kind: 'group', id: 'g1', name: 'G', participantIds: ['A', 'B'] };
|
|
42
|
-
const msg = {
|
|
43
|
-
kind: 'message', id: 'm1', text: 'Hi', from: 'A', to: 'A',
|
|
44
|
-
type: 'sync', style: { line: 'solid', head: 'arrow' }
|
|
45
|
-
};
|
|
46
|
-
const root = {
|
|
47
|
-
kind: 'root',
|
|
48
|
-
meta: { version: '1', source: 'unknown' },
|
|
49
|
-
participants: [p1, p2],
|
|
50
|
-
groups: [group],
|
|
51
|
-
events: [msg]
|
|
52
|
-
};
|
|
53
|
-
const result = new UnusedCleaner().transform(root);
|
|
54
|
-
expect(result.participants.map(p => p.id)).toEqual(['A']);
|
|
55
|
-
expect(result.groups).toHaveLength(1);
|
|
56
|
-
expect(result.groups[0].participantIds).toEqual(['A']);
|
|
57
|
-
});
|
|
58
|
-
it('removes group entirely if becomes empty', () => {
|
|
59
|
-
const p2 = { id: 'B', name: 'Bob', type: 'participant' }; // Unused
|
|
60
|
-
const group = { kind: 'group', id: 'g1', name: 'G', participantIds: ['B'] };
|
|
61
|
-
const root = {
|
|
62
|
-
kind: 'root',
|
|
63
|
-
meta: { version: '1', source: 'unknown' },
|
|
64
|
-
participants: [p2],
|
|
65
|
-
groups: [group],
|
|
66
|
-
events: []
|
|
67
|
-
};
|
|
68
|
-
const result = new UnusedCleaner().transform(root);
|
|
69
|
-
expect(result.groups).toHaveLength(0);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { EventNode, PolagramRoot } from '../../ast';
|
|
2
|
-
import { Walker } from '../traverse/walker';
|
|
3
|
-
import { FocusLayer } from '../types';
|
|
4
|
-
export declare class FocusFilter extends Walker {
|
|
5
|
-
private layer;
|
|
6
|
-
private matcher;
|
|
7
|
-
private targetParticipantIds;
|
|
8
|
-
constructor(layer: FocusLayer);
|
|
9
|
-
transform(root: PolagramRoot): PolagramRoot;
|
|
10
|
-
private resolveTargetParticipants;
|
|
11
|
-
protected visitEvent(node: EventNode): EventNode[];
|
|
12
|
-
private isRelatedToParticipant;
|
|
13
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { Matcher } from '../selector/matcher';
|
|
2
|
-
import { Walker } from '../traverse/walker';
|
|
3
|
-
export class FocusFilter extends Walker {
|
|
4
|
-
constructor(layer) {
|
|
5
|
-
super();
|
|
6
|
-
Object.defineProperty(this, "layer", {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
configurable: true,
|
|
9
|
-
writable: true,
|
|
10
|
-
value: layer
|
|
11
|
-
});
|
|
12
|
-
Object.defineProperty(this, "matcher", {
|
|
13
|
-
enumerable: true,
|
|
14
|
-
configurable: true,
|
|
15
|
-
writable: true,
|
|
16
|
-
value: new Matcher()
|
|
17
|
-
});
|
|
18
|
-
Object.defineProperty(this, "targetParticipantIds", {
|
|
19
|
-
enumerable: true,
|
|
20
|
-
configurable: true,
|
|
21
|
-
writable: true,
|
|
22
|
-
value: new Set()
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
transform(root) {
|
|
26
|
-
this.resolveTargetParticipants(root);
|
|
27
|
-
return super.transform(root);
|
|
28
|
-
}
|
|
29
|
-
resolveTargetParticipants(root) {
|
|
30
|
-
this.targetParticipantIds.clear();
|
|
31
|
-
const selector = this.layer.selector;
|
|
32
|
-
root.participants.forEach(p => {
|
|
33
|
-
if (this.matcher.matchParticipant(p, selector)) {
|
|
34
|
-
this.targetParticipantIds.add(p.id);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
visitEvent(node) {
|
|
39
|
-
// Only apply filtering if we have targets.
|
|
40
|
-
// If targets is empty, strictly speaking we should hide everything?
|
|
41
|
-
// Yes, "Focus on X" means "Show ONLY X". If X is not found, show nothing.
|
|
42
|
-
if (node.kind === 'message') {
|
|
43
|
-
const msg = node;
|
|
44
|
-
if (!this.isRelatedToParticipant(msg)) {
|
|
45
|
-
return []; // Drop irrelevant message
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (node.kind === 'note') {
|
|
49
|
-
const note = node;
|
|
50
|
-
const isRelated = note.participantIds.some(pid => this.targetParticipantIds.has(pid));
|
|
51
|
-
if (!isRelated)
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
if (node.kind === 'activation') {
|
|
55
|
-
const activation = node;
|
|
56
|
-
if (!this.targetParticipantIds.has(activation.participantId)) {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return super.visitEvent(node);
|
|
61
|
-
}
|
|
62
|
-
// Helper to check if message involves the participant from selector
|
|
63
|
-
isRelatedToParticipant(msg) {
|
|
64
|
-
// Check if from or to matches any targeted participant ID
|
|
65
|
-
if (msg.from && this.targetParticipantIds.has(msg.from))
|
|
66
|
-
return true;
|
|
67
|
-
if (msg.to && this.targetParticipantIds.has(msg.to))
|
|
68
|
-
return true;
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { FocusFilter } from './focus';
|
|
3
|
-
describe('FocusFilter', () => {
|
|
4
|
-
const createAst = (events) => ({
|
|
5
|
-
kind: 'root',
|
|
6
|
-
meta: { version: '1', source: 'unknown' },
|
|
7
|
-
participants: [
|
|
8
|
-
{ id: 'A', name: 'A', type: 'participant' },
|
|
9
|
-
{ id: 'B', name: 'B', type: 'participant' },
|
|
10
|
-
{ id: 'C', name: 'C', type: 'participant' },
|
|
11
|
-
{ id: 'D', name: 'D', type: 'participant' }
|
|
12
|
-
],
|
|
13
|
-
groups: [],
|
|
14
|
-
events
|
|
15
|
-
});
|
|
16
|
-
const msgAB = { kind: 'message', id: 'm1', text: 'A->B', from: 'A', to: 'B', type: 'sync', style: { line: 'solid', head: 'arrow' } };
|
|
17
|
-
const msgBC = { kind: 'message', id: 'm2', text: 'B->C', from: 'B', to: 'C', type: 'sync', style: { line: 'solid', head: 'arrow' } };
|
|
18
|
-
const msgCD = { kind: 'message', id: 'm3', text: 'C->D', from: 'C', to: 'D', type: 'sync', style: { line: 'solid', head: 'arrow' } };
|
|
19
|
-
it('removes messages not related to focused participant', () => {
|
|
20
|
-
const root = createAst([msgAB, msgBC, msgCD]);
|
|
21
|
-
const layer = {
|
|
22
|
-
action: 'focus',
|
|
23
|
-
selector: { kind: 'participant', name: 'A' }
|
|
24
|
-
};
|
|
25
|
-
// Expect: msgAB (involves A), others removed
|
|
26
|
-
const result = new FocusFilter(layer).transform(root);
|
|
27
|
-
expect(result.events).toHaveLength(1);
|
|
28
|
-
expect(result.events[0].id).toBe('m1');
|
|
29
|
-
});
|
|
30
|
-
it('removes messages even inside fragments, but keeps structure', () => {
|
|
31
|
-
const fragment = {
|
|
32
|
-
kind: 'fragment', id: 'f1', operator: 'alt',
|
|
33
|
-
branches: [
|
|
34
|
-
{ id: 'b1', condition: 'related', events: [msgAB] },
|
|
35
|
-
{ id: 'b2', condition: 'irrelevant', events: [msgCD] }
|
|
36
|
-
]
|
|
37
|
-
};
|
|
38
|
-
const root = createAst([fragment]);
|
|
39
|
-
const layer = {
|
|
40
|
-
action: 'focus',
|
|
41
|
-
selector: { kind: 'participant', name: 'A' }
|
|
42
|
-
};
|
|
43
|
-
const result = new FocusFilter(layer).transform(root);
|
|
44
|
-
const resFrag = result.events[0];
|
|
45
|
-
// b1 should keep event
|
|
46
|
-
expect(resFrag.branches[0].events).toHaveLength(1);
|
|
47
|
-
// b2 should have event removed, BUT branch itself remains (empty)
|
|
48
|
-
expect(resFrag.branches[1].events).toHaveLength(0);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { EventNode, PolagramRoot } from '../../ast';
|
|
2
|
-
import { Walker } from '../traverse/walker';
|
|
3
|
-
import { RemoveLayer } from '../types';
|
|
4
|
-
export declare class RemoveFilter extends Walker {
|
|
5
|
-
private layer;
|
|
6
|
-
private matcher;
|
|
7
|
-
private removedParticipantIds;
|
|
8
|
-
constructor(layer: RemoveLayer);
|
|
9
|
-
transform(root: PolagramRoot): PolagramRoot;
|
|
10
|
-
protected visitEvent(node: EventNode): EventNode[];
|
|
11
|
-
private isRelatedToRemovedParticipant;
|
|
12
|
-
}
|