@polagram/core 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -0
- package/dist/index.d.ts +625 -0
- package/dist/polagram-core.js +3653 -0
- package/dist/polagram-core.umd.cjs +28 -0
- package/dist/src/api.d.ts +75 -0
- package/dist/src/api.js +160 -0
- package/dist/src/ast/ast.test.d.ts +1 -0
- package/dist/src/ast/ast.test.js +146 -0
- package/dist/src/ast/index.d.ts +119 -0
- package/dist/src/ast/index.js +2 -0
- package/dist/src/config/index.d.ts +1 -0
- package/dist/src/config/index.js +1 -0
- package/dist/src/config/schema.d.ts +182 -0
- package/dist/src/config/schema.js +78 -0
- package/dist/src/config/schema.test.d.ts +1 -0
- package/dist/src/config/schema.test.js +94 -0
- package/dist/src/generator/base/walker.d.ts +19 -0
- package/dist/src/generator/base/walker.js +56 -0
- package/dist/src/generator/base/walker.test.d.ts +1 -0
- package/dist/src/generator/base/walker.test.js +49 -0
- package/dist/src/generator/generators/mermaid.d.ts +24 -0
- package/dist/src/generator/generators/mermaid.js +140 -0
- package/dist/src/generator/generators/mermaid.test.d.ts +1 -0
- package/dist/src/generator/generators/mermaid.test.js +70 -0
- package/dist/src/generator/interface.d.ts +17 -0
- package/dist/src/generator/interface.js +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +17 -0
- package/dist/src/parser/base/lexer.d.ts +18 -0
- package/dist/src/parser/base/lexer.js +95 -0
- package/dist/src/parser/base/lexer.test.d.ts +1 -0
- package/dist/src/parser/base/lexer.test.js +53 -0
- package/dist/src/parser/base/parser.d.ts +14 -0
- package/dist/src/parser/base/parser.js +43 -0
- package/dist/src/parser/base/parser.test.d.ts +1 -0
- package/dist/src/parser/base/parser.test.js +90 -0
- package/dist/src/parser/index.d.ts +10 -0
- package/dist/src/parser/index.js +29 -0
- package/dist/src/parser/index.test.d.ts +1 -0
- package/dist/src/parser/index.test.js +23 -0
- package/dist/src/parser/interface.d.ts +8 -0
- package/dist/src/parser/interface.js +1 -0
- package/dist/src/parser/languages/mermaid/constants.d.ts +7 -0
- package/dist/src/parser/languages/mermaid/constants.js +20 -0
- package/dist/src/parser/languages/mermaid/index.d.ts +4 -0
- package/dist/src/parser/languages/mermaid/index.js +11 -0
- package/dist/src/parser/languages/mermaid/lexer.d.ts +14 -0
- package/dist/src/parser/languages/mermaid/lexer.js +152 -0
- package/dist/src/parser/languages/mermaid/lexer.test.d.ts +1 -0
- package/dist/src/parser/languages/mermaid/lexer.test.js +58 -0
- package/dist/src/parser/languages/mermaid/parser.d.ts +21 -0
- package/dist/src/parser/languages/mermaid/parser.js +340 -0
- package/dist/src/parser/languages/mermaid/parser.test.d.ts +1 -0
- package/dist/src/parser/languages/mermaid/parser.test.js +252 -0
- package/dist/src/parser/languages/mermaid/tokens.d.ts +9 -0
- package/dist/src/parser/languages/mermaid/tokens.js +1 -0
- package/dist/src/transformer/cleaners/prune-empty.d.ts +9 -0
- package/dist/src/transformer/cleaners/prune-empty.js +27 -0
- package/dist/src/transformer/cleaners/prune-empty.test.d.ts +1 -0
- package/dist/src/transformer/cleaners/prune-empty.test.js +69 -0
- package/dist/src/transformer/cleaners/prune-unused.d.ts +5 -0
- package/dist/src/transformer/cleaners/prune-unused.js +48 -0
- package/dist/src/transformer/cleaners/prune-unused.test.d.ts +1 -0
- package/dist/src/transformer/cleaners/prune-unused.test.js +71 -0
- package/dist/src/transformer/filters/focus.d.ts +13 -0
- package/dist/src/transformer/filters/focus.js +71 -0
- package/dist/src/transformer/filters/focus.test.d.ts +1 -0
- package/dist/src/transformer/filters/focus.test.js +50 -0
- package/dist/src/transformer/filters/remove.d.ts +12 -0
- package/dist/src/transformer/filters/remove.js +82 -0
- package/dist/src/transformer/filters/remove.test.d.ts +1 -0
- package/dist/src/transformer/filters/remove.test.js +38 -0
- package/dist/src/transformer/filters/resolve.d.ts +9 -0
- package/dist/src/transformer/filters/resolve.js +32 -0
- package/dist/src/transformer/filters/resolve.test.d.ts +1 -0
- package/dist/src/transformer/filters/resolve.test.js +48 -0
- package/dist/src/transformer/index.d.ts +10 -0
- package/dist/src/transformer/index.js +10 -0
- package/dist/src/transformer/lens.d.ts +12 -0
- package/dist/src/transformer/lens.js +58 -0
- package/dist/src/transformer/lens.test.d.ts +1 -0
- package/dist/src/transformer/lens.test.js +60 -0
- package/dist/src/transformer/orchestration/engine.d.ts +5 -0
- package/dist/src/transformer/orchestration/engine.js +24 -0
- package/dist/src/transformer/orchestration/engine.test.d.ts +1 -0
- package/dist/src/transformer/orchestration/engine.test.js +41 -0
- package/dist/src/transformer/orchestration/registry.d.ts +10 -0
- package/dist/src/transformer/orchestration/registry.js +27 -0
- package/dist/src/transformer/selector/matcher.d.ts +9 -0
- package/dist/src/transformer/selector/matcher.js +62 -0
- package/dist/src/transformer/selector/matcher.test.d.ts +1 -0
- package/dist/src/transformer/selector/matcher.test.js +53 -0
- package/dist/src/transformer/traverse/walker.d.ts +14 -0
- package/dist/src/transformer/traverse/walker.js +67 -0
- package/dist/src/transformer/traverse/walker.test.d.ts +1 -0
- package/dist/src/transformer/traverse/walker.test.js +48 -0
- package/dist/src/transformer/types.d.ts +47 -0
- package/dist/src/transformer/types.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { getArrowString } from '../../parser/languages/mermaid/constants';
|
|
2
|
+
import { Traverser } from '../base/walker';
|
|
3
|
+
/**
|
|
4
|
+
* Visitor implementation that generates Mermaid code.
|
|
5
|
+
*/
|
|
6
|
+
export class MermaidGeneratorVisitor {
|
|
7
|
+
constructor() {
|
|
8
|
+
Object.defineProperty(this, "lines", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: []
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "indentLevel", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: 0
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(this, "traverser", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: void 0
|
|
25
|
+
});
|
|
26
|
+
this.traverser = new Traverser(this);
|
|
27
|
+
}
|
|
28
|
+
generate(ast) {
|
|
29
|
+
this.lines = [];
|
|
30
|
+
this.indentLevel = 0;
|
|
31
|
+
this.traverser.traverse(ast);
|
|
32
|
+
return this.lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
visitRoot(node) {
|
|
35
|
+
this.lines.push('sequenceDiagram');
|
|
36
|
+
this.indentLevel++;
|
|
37
|
+
if (node.meta?.title) {
|
|
38
|
+
this.add(`title ${node.meta.title}`);
|
|
39
|
+
}
|
|
40
|
+
// Participants
|
|
41
|
+
// Note: We iterate participants in defined order.
|
|
42
|
+
// Currently, AST does not preserve the interleaving of Box definitions and Participants,
|
|
43
|
+
// so we cannot reliably reconstruct Box blocks wrapping specific ordered participants
|
|
44
|
+
// without potentially reordering them.
|
|
45
|
+
// For now, we revert to simple iteration (ignoring boxes) to preserve order and existing behavior.
|
|
46
|
+
for (const p of node.participants) {
|
|
47
|
+
this.visitParticipant(p);
|
|
48
|
+
}
|
|
49
|
+
// Events
|
|
50
|
+
this.traverser.dispatchEvents(node.events);
|
|
51
|
+
}
|
|
52
|
+
visitParticipant(node) {
|
|
53
|
+
const typeKeyword = node.type === 'actor' ? 'actor' : 'participant';
|
|
54
|
+
const safeName = node.name;
|
|
55
|
+
if (node.id === node.name) {
|
|
56
|
+
this.add(`${typeKeyword} ${node.id}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.add(`${typeKeyword} ${node.id} as ${safeName}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
visitParticipantGroup(node) {
|
|
63
|
+
let line = `box`;
|
|
64
|
+
if (node.style?.backgroundColor) {
|
|
65
|
+
line += ` ${node.style.backgroundColor}`;
|
|
66
|
+
}
|
|
67
|
+
if (node.name) {
|
|
68
|
+
line += ` ${node.name}`;
|
|
69
|
+
}
|
|
70
|
+
this.add(line);
|
|
71
|
+
}
|
|
72
|
+
visitMessage(node) {
|
|
73
|
+
const from = node.from ?? '[*]';
|
|
74
|
+
const to = node.to ?? '[*]';
|
|
75
|
+
const arrow = getArrowString(node.type, node.style);
|
|
76
|
+
let suffix = '';
|
|
77
|
+
if (node.lifecycle?.activateTarget) {
|
|
78
|
+
suffix += '+';
|
|
79
|
+
}
|
|
80
|
+
if (node.lifecycle?.deactivateSource) {
|
|
81
|
+
suffix += '-';
|
|
82
|
+
}
|
|
83
|
+
this.add(`${from}${arrow}${suffix}${to}: ${node.text}`);
|
|
84
|
+
}
|
|
85
|
+
visitFragment(node) {
|
|
86
|
+
if (node.branches.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
const first = node.branches[0];
|
|
89
|
+
const firstCondition = first.condition ? ` ${first.condition}` : '';
|
|
90
|
+
this.add(`${node.operator}${firstCondition}`);
|
|
91
|
+
this.indent(() => {
|
|
92
|
+
this.traverser.dispatchEvents(first.events);
|
|
93
|
+
});
|
|
94
|
+
for (let i = 1; i < node.branches.length; i++) {
|
|
95
|
+
const b = node.branches[i];
|
|
96
|
+
const cond = b.condition ? ` ${b.condition}` : '';
|
|
97
|
+
this.add(`else${cond}`);
|
|
98
|
+
this.indent(() => {
|
|
99
|
+
this.traverser.dispatchEvents(b.events);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
this.add('end');
|
|
103
|
+
}
|
|
104
|
+
visitNote(node) {
|
|
105
|
+
const pos = node.position;
|
|
106
|
+
let target = '';
|
|
107
|
+
if (node.participantIds.length > 0) {
|
|
108
|
+
target = node.participantIds.join(',');
|
|
109
|
+
if (pos !== 'over') {
|
|
110
|
+
target = ` of ${target}`;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
target = ` ${target}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.add(`note ${pos}${target}: ${node.text}`);
|
|
117
|
+
}
|
|
118
|
+
visitActivation(node) {
|
|
119
|
+
this.add(`${node.action} ${node.participantId}`);
|
|
120
|
+
}
|
|
121
|
+
visitDivider(node) {
|
|
122
|
+
this.add(`%% == ${node.text || ''} ==`);
|
|
123
|
+
}
|
|
124
|
+
visitSpacer(node) {
|
|
125
|
+
this.add(`...${node.text || ''}...`);
|
|
126
|
+
}
|
|
127
|
+
visitReference(node) {
|
|
128
|
+
this.add(`%% ref: ${node.text}`);
|
|
129
|
+
}
|
|
130
|
+
// --- Helpers ---
|
|
131
|
+
add(line) {
|
|
132
|
+
const spaces = ' '.repeat(this.indentLevel);
|
|
133
|
+
this.lines.push(`${spaces}${line}`);
|
|
134
|
+
}
|
|
135
|
+
indent(fn) {
|
|
136
|
+
this.indentLevel++;
|
|
137
|
+
fn();
|
|
138
|
+
this.indentLevel--;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { MermaidGeneratorVisitor } from './mermaid';
|
|
3
|
+
describe('MermaidGeneratorVisitor', () => {
|
|
4
|
+
it('should generate sequence diagram header', () => {
|
|
5
|
+
const root = { kind: 'root', meta: { version: '1.0.0', source: 'mermaid' }, participants: [], groups: [], events: [] };
|
|
6
|
+
const visitor = new MermaidGeneratorVisitor();
|
|
7
|
+
const output = visitor.generate(root);
|
|
8
|
+
expect(output).toContain('sequenceDiagram');
|
|
9
|
+
});
|
|
10
|
+
it('should generate title if present', () => {
|
|
11
|
+
const root = { kind: 'root', meta: { version: '1.0.0', source: 'mermaid', title: 'My Diagram' }, participants: [], groups: [], events: [] };
|
|
12
|
+
const visitor = new MermaidGeneratorVisitor();
|
|
13
|
+
const output = visitor.generate(root);
|
|
14
|
+
expect(output).toContain('title My Diagram');
|
|
15
|
+
});
|
|
16
|
+
it('should generate participants', () => {
|
|
17
|
+
const root = {
|
|
18
|
+
kind: 'root',
|
|
19
|
+
meta: { version: '1.0.0', source: 'mermaid' },
|
|
20
|
+
participants: [
|
|
21
|
+
{ id: 'A', name: 'Alice', type: 'participant' },
|
|
22
|
+
{ id: 'B', name: 'Bob', type: 'actor' }
|
|
23
|
+
],
|
|
24
|
+
groups: [],
|
|
25
|
+
events: []
|
|
26
|
+
};
|
|
27
|
+
const visitor = new MermaidGeneratorVisitor();
|
|
28
|
+
const output = visitor.generate(root);
|
|
29
|
+
expect(output).toContain('participant A as Alice');
|
|
30
|
+
expect(output).toContain('actor B as Bob');
|
|
31
|
+
});
|
|
32
|
+
it('should generate participant with multi-word name without extra quotes', () => {
|
|
33
|
+
const root = {
|
|
34
|
+
kind: 'root',
|
|
35
|
+
meta: { version: '1.0.0', source: 'mermaid' },
|
|
36
|
+
participants: [
|
|
37
|
+
{ id: 'API', name: 'API Server', type: 'participant' }
|
|
38
|
+
],
|
|
39
|
+
groups: [],
|
|
40
|
+
events: []
|
|
41
|
+
};
|
|
42
|
+
const visitor = new MermaidGeneratorVisitor();
|
|
43
|
+
const output = visitor.generate(root);
|
|
44
|
+
// We expect it NOT to be quoted if the user wants clean text
|
|
45
|
+
// But currently it IS quoted. checking what it does now vs what we want.
|
|
46
|
+
// If we want to fix it, we should expect "API Server" plain.
|
|
47
|
+
expect(output).toContain('participant API as API Server');
|
|
48
|
+
});
|
|
49
|
+
it('should generate messages', () => {
|
|
50
|
+
const msg = {
|
|
51
|
+
kind: 'message',
|
|
52
|
+
id: '1',
|
|
53
|
+
from: 'A',
|
|
54
|
+
to: 'B',
|
|
55
|
+
text: 'hello',
|
|
56
|
+
type: 'sync',
|
|
57
|
+
style: { line: 'solid', head: 'arrow' }
|
|
58
|
+
};
|
|
59
|
+
const root = {
|
|
60
|
+
kind: 'root',
|
|
61
|
+
meta: { version: '1.0.0', source: 'mermaid' },
|
|
62
|
+
participants: [],
|
|
63
|
+
groups: [],
|
|
64
|
+
events: [msg]
|
|
65
|
+
};
|
|
66
|
+
const visitor = new MermaidGeneratorVisitor();
|
|
67
|
+
const output = visitor.generate(root);
|
|
68
|
+
expect(output).toContain('A->>B: hello');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ActivationNode, DividerNode, FragmentNode, MessageNode, NoteNode, Participant, ParticipantGroup, PolagramRoot, ReferenceNode, SpacerNode } from '../ast';
|
|
2
|
+
/**
|
|
3
|
+
* Visitor interface for traversing the Polagram AST.
|
|
4
|
+
* Implement this interface to create code generators, validators, etc.
|
|
5
|
+
*/
|
|
6
|
+
export interface PolagramVisitor {
|
|
7
|
+
visitRoot(node: PolagramRoot): void;
|
|
8
|
+
visitParticipant(node: Participant): void;
|
|
9
|
+
visitParticipantGroup(node: ParticipantGroup): void;
|
|
10
|
+
visitMessage(node: MessageNode): void;
|
|
11
|
+
visitFragment(node: FragmentNode): void;
|
|
12
|
+
visitNote(node: NoteNode): void;
|
|
13
|
+
visitActivation(node: ActivationNode): void;
|
|
14
|
+
visitDivider(node: DividerNode): void;
|
|
15
|
+
visitSpacer(node: SpacerNode): void;
|
|
16
|
+
visitReference(node: ReferenceNode): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './ast';
|
|
2
|
+
export { ParserFactory } from './parser';
|
|
3
|
+
export type { DiagramParser } from './parser/interface';
|
|
4
|
+
export { Traverser } from './generator/base/walker';
|
|
5
|
+
export type { PolagramVisitor } from './generator/interface';
|
|
6
|
+
export { MermaidGeneratorVisitor } from './generator/generators/mermaid';
|
|
7
|
+
export * from './transformer';
|
|
8
|
+
export { Polagram, PolagramBuilder } from './api';
|
|
9
|
+
export * from './config';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Public API Exports
|
|
2
|
+
// AST
|
|
3
|
+
export * from './ast';
|
|
4
|
+
// Parsers (Factory & Strategy)
|
|
5
|
+
export { ParserFactory } from './parser';
|
|
6
|
+
// Generators
|
|
7
|
+
export { Traverser } from './generator/base/walker';
|
|
8
|
+
// Default Implementations (Optional, or force users to use Factory)
|
|
9
|
+
// We export Mermaid Generator specifically as it might be used directly or via a future Factory
|
|
10
|
+
export { MermaidGeneratorVisitor } from './generator/generators/mermaid';
|
|
11
|
+
// Transformation Engine
|
|
12
|
+
export * from './transformer';
|
|
13
|
+
// Fluent API (Recommended for most users)
|
|
14
|
+
export { Polagram, PolagramBuilder } from './api';
|
|
15
|
+
// Legacy compatibility or convenience helpers could go here if needed.
|
|
16
|
+
// Configuration & Schema
|
|
17
|
+
export * from './config';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Token } from '../languages/mermaid/tokens';
|
|
2
|
+
export declare abstract class BaseLexer {
|
|
3
|
+
protected input: string;
|
|
4
|
+
protected position: number;
|
|
5
|
+
protected readPosition: number;
|
|
6
|
+
protected ch: string;
|
|
7
|
+
protected line: number;
|
|
8
|
+
protected column: number;
|
|
9
|
+
constructor(input: string);
|
|
10
|
+
getInput(): string;
|
|
11
|
+
abstract nextToken(): Token;
|
|
12
|
+
protected readChar(): void;
|
|
13
|
+
protected peekChar(): string;
|
|
14
|
+
protected skipWhitespace(): void;
|
|
15
|
+
protected readWhile(predicate: (ch: string) => boolean): string;
|
|
16
|
+
protected isLetter(ch: string): boolean;
|
|
17
|
+
protected isDigit(ch: string): boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export class BaseLexer {
|
|
2
|
+
constructor(input) {
|
|
3
|
+
Object.defineProperty(this, "input", {
|
|
4
|
+
enumerable: true,
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true,
|
|
7
|
+
value: input
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(this, "position", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: 0
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "readPosition", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: 0
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(this, "ch", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: ''
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(this, "line", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: 1
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(this, "column", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: 0
|
|
38
|
+
});
|
|
39
|
+
this.readChar();
|
|
40
|
+
}
|
|
41
|
+
getInput() {
|
|
42
|
+
return this.input;
|
|
43
|
+
}
|
|
44
|
+
readChar() {
|
|
45
|
+
if (this.readPosition >= this.input.length) {
|
|
46
|
+
this.ch = '';
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.ch = this.input[this.readPosition];
|
|
50
|
+
}
|
|
51
|
+
this.position = this.readPosition;
|
|
52
|
+
this.readPosition += 1;
|
|
53
|
+
this.column += 1;
|
|
54
|
+
if (this.ch === '\n') {
|
|
55
|
+
this.line += 1;
|
|
56
|
+
this.column = 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
peekChar() {
|
|
60
|
+
if (this.readPosition >= this.input.length) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
return this.input[this.readPosition];
|
|
64
|
+
}
|
|
65
|
+
skipWhitespace() {
|
|
66
|
+
while (this.ch === ' ' || this.ch === '\t' || this.ch === '\r') {
|
|
67
|
+
this.readChar();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
readWhile(predicate) {
|
|
71
|
+
const position = this.position;
|
|
72
|
+
while (predicate(this.ch)) {
|
|
73
|
+
this.readChar();
|
|
74
|
+
}
|
|
75
|
+
return this.input.slice(position, this.position);
|
|
76
|
+
}
|
|
77
|
+
isLetter(ch) {
|
|
78
|
+
// Support ASCII letters and underscore
|
|
79
|
+
if (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ch === '_') {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
// Support Unicode letters (including Japanese, Chinese, Korean, etc.)
|
|
83
|
+
// This uses a simple check: if the character code is > 127 (non-ASCII)
|
|
84
|
+
// and it's not a digit or whitespace, treat it as a letter
|
|
85
|
+
const code = ch.charCodeAt(0);
|
|
86
|
+
if (code > 127) {
|
|
87
|
+
// Exclude Unicode digits and whitespace
|
|
88
|
+
return !/[\s\d]/.test(ch);
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
isDigit(ch) {
|
|
93
|
+
return '0' <= ch && ch <= '9';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { BaseLexer } from './lexer';
|
|
3
|
+
// Concrete implementation for testing abstract class
|
|
4
|
+
class TestLexer extends BaseLexer {
|
|
5
|
+
nextToken() {
|
|
6
|
+
this.skipWhitespace();
|
|
7
|
+
if (this.readPosition > this.input.length) { // fixed EOF logic for test, check base-lexer readChar behavior
|
|
8
|
+
// BaseLexer: readPosition moves past End.
|
|
9
|
+
return { type: 'EOF', literal: '', line: this.line, column: this.column, start: this.position, end: this.position };
|
|
10
|
+
}
|
|
11
|
+
const char = this.ch;
|
|
12
|
+
const start = this.position;
|
|
13
|
+
this.readChar();
|
|
14
|
+
return {
|
|
15
|
+
type: 'UNKNOWN', // Dummy type
|
|
16
|
+
literal: char,
|
|
17
|
+
line: this.line,
|
|
18
|
+
column: this.column,
|
|
19
|
+
start,
|
|
20
|
+
end: this.position
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Expose protected methods for testing
|
|
24
|
+
testReadIdentifier() { return this.readWhile(c => this.isLetter(c)); }
|
|
25
|
+
testIsDigit(c) { return this.isDigit(c); }
|
|
26
|
+
testIsLetter(c) { return this.isLetter(c); }
|
|
27
|
+
getChar() { return this.ch; }
|
|
28
|
+
}
|
|
29
|
+
describe('BaseLexer', () => {
|
|
30
|
+
it('should initialize correctly', () => {
|
|
31
|
+
const input = "abc";
|
|
32
|
+
const lexer = new TestLexer(input);
|
|
33
|
+
expect(lexer.getInput()).toBe(input);
|
|
34
|
+
expect(lexer.getChar()).toBe('a');
|
|
35
|
+
});
|
|
36
|
+
it('should identify digits', () => {
|
|
37
|
+
const lexer = new TestLexer("");
|
|
38
|
+
expect(lexer.testIsDigit('1')).toBe(true);
|
|
39
|
+
expect(lexer.testIsDigit('a')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('should identify letters', () => {
|
|
42
|
+
const lexer = new TestLexer("");
|
|
43
|
+
expect(lexer.testIsLetter('a')).toBe(true);
|
|
44
|
+
expect(lexer.testIsLetter('_')).toBe(true);
|
|
45
|
+
expect(lexer.testIsLetter('1')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('should read characters while predicate matches', () => {
|
|
48
|
+
const lexer = new TestLexer("abc 123");
|
|
49
|
+
const ident = lexer.testReadIdentifier();
|
|
50
|
+
expect(ident).toBe("abc");
|
|
51
|
+
expect(lexer.getChar()).toBe(" "); // Stops at space
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PolagramRoot } from '../../ast';
|
|
2
|
+
import { Token, TokenType } from '../languages/mermaid/tokens';
|
|
3
|
+
import { BaseLexer } from './lexer';
|
|
4
|
+
export declare abstract class BaseParser {
|
|
5
|
+
protected lexer: BaseLexer;
|
|
6
|
+
protected currToken: Token;
|
|
7
|
+
protected peekToken: Token;
|
|
8
|
+
constructor(lexer: BaseLexer);
|
|
9
|
+
protected advance(): void;
|
|
10
|
+
abstract parse(): PolagramRoot;
|
|
11
|
+
protected curTokenIs(t: TokenType): boolean;
|
|
12
|
+
protected peekTokenIs(t: TokenType): boolean;
|
|
13
|
+
protected expectPeek(t: TokenType): boolean;
|
|
14
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class BaseParser {
|
|
2
|
+
constructor(lexer) {
|
|
3
|
+
Object.defineProperty(this, "lexer", {
|
|
4
|
+
enumerable: true,
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true,
|
|
7
|
+
value: lexer
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(this, "currToken", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: void 0
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "peekToken", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: void 0
|
|
20
|
+
});
|
|
21
|
+
this.advance();
|
|
22
|
+
this.advance();
|
|
23
|
+
}
|
|
24
|
+
advance() {
|
|
25
|
+
this.currToken = this.peekToken;
|
|
26
|
+
this.peekToken = this.lexer.nextToken();
|
|
27
|
+
}
|
|
28
|
+
curTokenIs(t) {
|
|
29
|
+
return this.currToken.type === t;
|
|
30
|
+
}
|
|
31
|
+
peekTokenIs(t) {
|
|
32
|
+
return this.peekToken.type === t;
|
|
33
|
+
}
|
|
34
|
+
expectPeek(t) {
|
|
35
|
+
if (this.peekTokenIs(t)) {
|
|
36
|
+
this.advance();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { BaseLexer } from './lexer';
|
|
3
|
+
import { BaseParser } from './parser';
|
|
4
|
+
// Mocks
|
|
5
|
+
class MockLexer extends BaseLexer {
|
|
6
|
+
constructor(tokens) {
|
|
7
|
+
super("");
|
|
8
|
+
Object.defineProperty(this, "tokens", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: void 0
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "index", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: 0
|
|
19
|
+
});
|
|
20
|
+
this.tokens = tokens;
|
|
21
|
+
}
|
|
22
|
+
nextToken() {
|
|
23
|
+
if (this.index >= this.tokens.length) {
|
|
24
|
+
return { type: 'EOF', literal: '', line: 0, column: 0, start: 0, end: 0 };
|
|
25
|
+
}
|
|
26
|
+
return this.tokens[this.index++];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class TestParser extends BaseParser {
|
|
30
|
+
parse() {
|
|
31
|
+
return { kind: 'root', meta: { version: '1.0.0', source: 'unknown' }, participants: [], groups: [], events: [] };
|
|
32
|
+
}
|
|
33
|
+
// Expose protected methods
|
|
34
|
+
getCurr() { return this.currToken; }
|
|
35
|
+
getPeek() { return this.peekToken; }
|
|
36
|
+
testAdvance() { this.advance(); }
|
|
37
|
+
testCurTokenIs(t) { return this.curTokenIs(t); }
|
|
38
|
+
testPeakTokenIs(t) { return this.peekTokenIs(t); }
|
|
39
|
+
testExpectPeek(t) { return this.expectPeek(t); }
|
|
40
|
+
}
|
|
41
|
+
describe('BaseParser', () => {
|
|
42
|
+
const tokens = [
|
|
43
|
+
{ type: 'IDENTIFIER', literal: 'A', line: 1, column: 0, start: 0, end: 1 },
|
|
44
|
+
{ type: 'ARROW', literal: '->', line: 1, column: 1, start: 1, end: 3 },
|
|
45
|
+
{ type: 'IDENTIFIER', literal: 'B', line: 1, column: 3, start: 3, end: 4 },
|
|
46
|
+
];
|
|
47
|
+
it('should initialize curr and peek tokens', () => {
|
|
48
|
+
const lexer = new MockLexer(tokens);
|
|
49
|
+
const parser = new TestParser(lexer);
|
|
50
|
+
// Constructor called advance twice.
|
|
51
|
+
// 1st advance: curr=undefined, peek=Tokens[0]
|
|
52
|
+
// 2nd advance: curr=Tokens[0], peek=Tokens[1]
|
|
53
|
+
expect(parser.getCurr().type).toBe('IDENTIFIER');
|
|
54
|
+
expect(parser.getCurr().literal).toBe('A');
|
|
55
|
+
expect(parser.getPeek().type).toBe('ARROW');
|
|
56
|
+
});
|
|
57
|
+
it('should advance tokens', () => {
|
|
58
|
+
const lexer = new MockLexer(tokens);
|
|
59
|
+
const parser = new TestParser(lexer);
|
|
60
|
+
parser.testAdvance();
|
|
61
|
+
// curr=Tokens[1] (ARROW), peek=Tokens[2] (IDENTIFIER B)
|
|
62
|
+
expect(parser.getCurr().type).toBe('ARROW');
|
|
63
|
+
expect(parser.getPeek().type).toBe('IDENTIFIER');
|
|
64
|
+
});
|
|
65
|
+
it('should check current and peek token types', () => {
|
|
66
|
+
const lexer = new MockLexer(tokens);
|
|
67
|
+
const parser = new TestParser(lexer);
|
|
68
|
+
expect(parser.testCurTokenIs('IDENTIFIER')).toBe(true);
|
|
69
|
+
expect(parser.testPeakTokenIs('ARROW')).toBe(true);
|
|
70
|
+
expect(parser.testPeakTokenIs('EOF')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it('should expect peek token and advance if match', () => {
|
|
73
|
+
const lexer = new MockLexer(tokens);
|
|
74
|
+
const parser = new TestParser(lexer);
|
|
75
|
+
// Peek is ARROW. Expect ARROW should likely succeed?
|
|
76
|
+
// Method signature: expectPeek(t). If peek matches, advance and return true.
|
|
77
|
+
const result = parser.testExpectPeek('ARROW');
|
|
78
|
+
expect(result).toBe(true);
|
|
79
|
+
// After advance, curr is ARROW.
|
|
80
|
+
expect(parser.getCurr().type).toBe('ARROW');
|
|
81
|
+
});
|
|
82
|
+
it('should expect peek token and return false if mismatch', () => {
|
|
83
|
+
const lexer = new MockLexer(tokens);
|
|
84
|
+
const parser = new TestParser(lexer);
|
|
85
|
+
const result = parser.testExpectPeek('EOF'); // Peek is ARROW
|
|
86
|
+
expect(result).toBe(false);
|
|
87
|
+
// Should not have advanced
|
|
88
|
+
expect(parser.getCurr().type).toBe('IDENTIFIER');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DiagramParser } from './interface';
|
|
2
|
+
/**
|
|
3
|
+
* Parser Factory
|
|
4
|
+
* Centralizes retrieval of parser strategies.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ParserFactory {
|
|
7
|
+
private static parsers;
|
|
8
|
+
static register(language: string, parser: DiagramParser): void;
|
|
9
|
+
static getParser(language: string): DiagramParser;
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { mermaidParser } from './languages/mermaid';
|
|
3
|
+
/**
|
|
4
|
+
* Parser Factory
|
|
5
|
+
* Centralizes retrieval of parser strategies.
|
|
6
|
+
*/
|
|
7
|
+
export class ParserFactory {
|
|
8
|
+
static register(language, parser) {
|
|
9
|
+
this.parsers.set(language, parser);
|
|
10
|
+
}
|
|
11
|
+
static getParser(language) {
|
|
12
|
+
const parser = this.parsers.get(language);
|
|
13
|
+
if (!parser) {
|
|
14
|
+
throw new Error(`Parser for language '${language}' not found.`);
|
|
15
|
+
}
|
|
16
|
+
return parser;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
_a = ParserFactory;
|
|
20
|
+
Object.defineProperty(ParserFactory, "parsers", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: new Map()
|
|
25
|
+
});
|
|
26
|
+
(() => {
|
|
27
|
+
// Register built-in parsers
|
|
28
|
+
_a.register('mermaid', mermaidParser);
|
|
29
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ParserFactory } from './index';
|
|
3
|
+
describe('ParserFactory', () => {
|
|
4
|
+
it('should retrieve registered parser', () => {
|
|
5
|
+
const parser = ParserFactory.getParser('mermaid');
|
|
6
|
+
expect(parser).toBeDefined();
|
|
7
|
+
// Since we import implemented parser, we expect checking it conforms to interface
|
|
8
|
+
expect(typeof parser.parse).toBe('function');
|
|
9
|
+
});
|
|
10
|
+
it('should throw error for unknown language', () => {
|
|
11
|
+
expect(() => {
|
|
12
|
+
ParserFactory.getParser('unknown-lang');
|
|
13
|
+
}).toThrow("Parser for language 'unknown-lang' not found.");
|
|
14
|
+
});
|
|
15
|
+
it('should allow registering new parser', () => {
|
|
16
|
+
const mockParser = {
|
|
17
|
+
parse: (_code) => ({ kind: 'root', meta: { version: '1.0.0', source: 'unknown' }, participants: [], groups: [], events: [] })
|
|
18
|
+
};
|
|
19
|
+
ParserFactory.register('test-lang', mockParser);
|
|
20
|
+
const retrieved = ParserFactory.getParser('test-lang');
|
|
21
|
+
expect(retrieved).toBe(mockParser);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|