testeranto 0.233.2 → 0.233.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.
- package/package.json +1 -2
- package/src/grafeovidajo/README.md +34 -21
- package/src/grafeovidajo/package.json +2 -2
- package/src/server/serverClasses/Server_Http/Server_HTTP_utils.ts +22 -22
- package/src/viz.md +2 -2
- package/src/viz-app/MarkdownChart.tsx +0 -115
- package/src/viz-app/hooks/useChartSync.ts +0 -66
- package/src/viz-app/hooks/useMarkdownFiles.ts +0 -84
- package/src/viz-app/index.ts +0 -10
- package/src/viz-app/markdown/parser.ts +0 -115
- package/src/viz-app/types.ts +0 -40
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "testeranto",
|
|
3
3
|
"displayName": "Testeranto",
|
|
4
4
|
"description": "the Dockerized, AI powered, BDD test framework for polyglot projects",
|
|
5
|
-
"version": "0.233.
|
|
5
|
+
"version": "0.233.4",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.19.0",
|
|
8
8
|
"vscode": "^1.60.0",
|
|
@@ -345,7 +345,6 @@
|
|
|
345
345
|
"url": "^0.11.4",
|
|
346
346
|
"uuid": "^10.0.0",
|
|
347
347
|
"validate-with-xmllint": "^1.2.1",
|
|
348
|
-
"viz": "^0.1.0",
|
|
349
348
|
"ws": "^8.18.3",
|
|
350
349
|
"xmldom": "^0.6.0",
|
|
351
350
|
"xterm": "^5.3.0",
|
|
@@ -2,14 +2,14 @@ this file documents how test results can be visualized.
|
|
|
2
2
|
|
|
3
3
|
testeranto includes user-defined visualizations, in the form of react components passed via configuration.
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The grafeovidajo library is a set of react components to assist that. At it's core, it takes graph data, and projects it onto a 2d space, similar to d3. The goal is to provide the right hooks to allow the creation of multiple popular charts.
|
|
6
6
|
|
|
7
7
|
- eishhower matrix
|
|
8
8
|
- gantt chart
|
|
9
9
|
- kanan board
|
|
10
10
|
- trees and graphs
|
|
11
11
|
|
|
12
|
-
given 2 attributes on the nodes and some customizations, the
|
|
12
|
+
given 2 attributes on the nodes and some customizations, the grafeovidajo library project your graph data onto the 2d screen.
|
|
13
13
|
|
|
14
14
|
First, an x and y attribute are chosen
|
|
15
15
|
Second, if those attributes are a set or continuous
|
|
@@ -46,18 +46,18 @@ interface Edge {
|
|
|
46
46
|
interface ProjectionConfig {
|
|
47
47
|
xAttribute: string;
|
|
48
48
|
yAttribute: string;
|
|
49
|
-
xType:
|
|
50
|
-
yType:
|
|
51
|
-
|
|
52
|
-
layout?:
|
|
49
|
+
xType: "categorical" | "continuous" | "ordinal" | "temporal";
|
|
50
|
+
yType: "categorical" | "continuous" | "ordinal" | "temporal";
|
|
51
|
+
|
|
52
|
+
layout?: "grid" | "force" | "tree" | "timeline";
|
|
53
53
|
spacing?: {
|
|
54
54
|
x: number;
|
|
55
55
|
y: number;
|
|
56
56
|
};
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
xDomain?: [min: number, max: number] | string[];
|
|
59
59
|
yDomain?: [min: number, max: number] | string[];
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
xTransform?: (value: any) => number;
|
|
62
62
|
yTransform?: (value: any) => number;
|
|
63
63
|
}
|
|
@@ -66,7 +66,7 @@ interface ProjectionConfig {
|
|
|
66
66
|
interface StyleConfig {
|
|
67
67
|
nodeSize: number | ((node: Node) => number);
|
|
68
68
|
nodeColor: string | ((node: Node) => string);
|
|
69
|
-
nodeShape:
|
|
69
|
+
nodeShape: "circle" | "square" | "diamond" | ((node: Node) => string);
|
|
70
70
|
edgeColor?: string;
|
|
71
71
|
edgeWidth?: number;
|
|
72
72
|
labels?: {
|
|
@@ -94,8 +94,8 @@ interface VizConfig {
|
|
|
94
94
|
```typescript
|
|
95
95
|
// Project graph data to screen coordinates
|
|
96
96
|
function projectGraph(
|
|
97
|
-
graph: GraphData,
|
|
98
|
-
config: ProjectionConfig
|
|
97
|
+
graph: GraphData,
|
|
98
|
+
config: ProjectionConfig,
|
|
99
99
|
): ProjectedGraph;
|
|
100
100
|
|
|
101
101
|
interface ProjectedGraph {
|
|
@@ -115,15 +115,25 @@ interface ProjectedNode extends Node {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// Layout algorithms
|
|
118
|
-
function layoutGrid(
|
|
118
|
+
function layoutGrid(
|
|
119
|
+
nodes: ProjectedNode[],
|
|
120
|
+
spacing: { x: number; y: number },
|
|
121
|
+
): ProjectedNode[];
|
|
119
122
|
function layoutForce(nodes: ProjectedNode[], edges?: Edge[]): ProjectedNode[];
|
|
120
|
-
function layoutTree(
|
|
121
|
-
|
|
123
|
+
function layoutTree(
|
|
124
|
+
nodes: ProjectedNode[],
|
|
125
|
+
edges: Edge[],
|
|
126
|
+
rootId?: string,
|
|
127
|
+
): ProjectedNode[];
|
|
128
|
+
function layoutTimeline(
|
|
129
|
+
nodes: ProjectedNode[],
|
|
130
|
+
timeAttribute: string,
|
|
131
|
+
): ProjectedNode[];
|
|
122
132
|
|
|
123
133
|
// Apply styles to projected graph
|
|
124
134
|
function applyStyles(
|
|
125
135
|
projectedGraph: ProjectedGraph,
|
|
126
|
-
styleConfig: StyleConfig
|
|
136
|
+
styleConfig: StyleConfig,
|
|
127
137
|
): StyledGraph;
|
|
128
138
|
|
|
129
139
|
interface StyledGraph extends ProjectedGraph {
|
|
@@ -154,10 +164,10 @@ interface VizComponentProps {
|
|
|
154
164
|
// Eisenhower Matrix
|
|
155
165
|
interface EisenhowerConfig extends VizConfig {
|
|
156
166
|
quadrants: {
|
|
157
|
-
urgentImportant: { x: [0, 0.5]
|
|
158
|
-
notUrgentImportant: { x: [0.5, 1]
|
|
159
|
-
urgentNotImportant: { x: [0, 0.5]
|
|
160
|
-
notUrgentNotImportant: { x: [0.5, 1]
|
|
167
|
+
urgentImportant: { x: [0, 0.5]; y: [0, 0.5] };
|
|
168
|
+
notUrgentImportant: { x: [0.5, 1]; y: [0, 0.5] };
|
|
169
|
+
urgentNotImportant: { x: [0, 0.5]; y: [0.5, 1] };
|
|
170
|
+
notUrgentNotImportant: { x: [0.5, 1]; y: [0.5, 1] };
|
|
161
171
|
};
|
|
162
172
|
}
|
|
163
173
|
|
|
@@ -181,7 +191,7 @@ interface KanbanConfig extends VizConfig {
|
|
|
181
191
|
// Tree/Graph
|
|
182
192
|
interface TreeConfig extends VizConfig {
|
|
183
193
|
rootId?: string;
|
|
184
|
-
orientation:
|
|
194
|
+
orientation: "horizontal" | "vertical";
|
|
185
195
|
nodeSeparation: number;
|
|
186
196
|
levelSeparation: number;
|
|
187
197
|
}
|
|
@@ -221,7 +231,7 @@ const eisenhowerConfig: EisenhowerConfig = {
|
|
|
221
231
|
};
|
|
222
232
|
|
|
223
233
|
// Usage in React component
|
|
224
|
-
<EisenhowerMatrix
|
|
234
|
+
<EisenhowerMatrix
|
|
225
235
|
data={testResultsGraph}
|
|
226
236
|
config={eisenhowerConfig}
|
|
227
237
|
width={800}
|
|
@@ -237,4 +247,7 @@ const eisenhowerConfig: EisenhowerConfig = {
|
|
|
237
247
|
3. **Extensible Layouts**: Easy to add new layout algorithms
|
|
238
248
|
4. **Responsive Design**: Works with any screen size
|
|
239
249
|
5. **Interactive**: Built-in support for hover, click, drag interactions
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
|
|
240
253
|
```
|
|
@@ -318,7 +318,7 @@ export class Server_HTTP_utils {
|
|
|
318
318
|
fileContents: await this.extractFileContentsFromTree(collatedFilesTree),
|
|
319
319
|
// Add feature graph for visualization
|
|
320
320
|
featureGraph: featureGraph,
|
|
321
|
-
// Add
|
|
321
|
+
// Add grafeovidajo configuration
|
|
322
322
|
vizConfig: {
|
|
323
323
|
projection: {
|
|
324
324
|
xAttribute: 'status',
|
|
@@ -353,7 +353,7 @@ export class Server_HTTP_utils {
|
|
|
353
353
|
const jsonString = JSON.stringify(embeddedData);
|
|
354
354
|
// Encode to base64 to avoid any HTML/JS escaping issues
|
|
355
355
|
const base64String = Buffer.from(jsonString, 'utf8').toString('base64');
|
|
356
|
-
|
|
356
|
+
|
|
357
357
|
// Create a script tag that decodes the base64 string
|
|
358
358
|
const scriptTag = `<script>
|
|
359
359
|
window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
@@ -466,20 +466,20 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
466
466
|
// Try to read and parse markdown file
|
|
467
467
|
let frontmatter: Record<string, any> = {};
|
|
468
468
|
let content: string | undefined;
|
|
469
|
-
|
|
469
|
+
|
|
470
470
|
try {
|
|
471
471
|
const fullPath = path.join(process.cwd(), normalizedPath);
|
|
472
472
|
if (fs.existsSync(fullPath)) {
|
|
473
473
|
const fileContent = await fs.promises.readFile(fullPath, 'utf-8');
|
|
474
474
|
content = fileContent;
|
|
475
|
-
|
|
475
|
+
|
|
476
476
|
// Parse markdown frontmatter
|
|
477
477
|
const match = fileContent.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
478
478
|
if (match) {
|
|
479
479
|
const [, frontmatterStr] = match;
|
|
480
480
|
frontmatter = this.parseYamlFrontmatter(frontmatterStr);
|
|
481
481
|
}
|
|
482
|
-
|
|
482
|
+
|
|
483
483
|
// Create feature node for graph
|
|
484
484
|
const nodeId = `feature:${normalizedPath}`;
|
|
485
485
|
featureNodes.push({
|
|
@@ -492,13 +492,13 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
492
492
|
_feature: feature
|
|
493
493
|
}
|
|
494
494
|
});
|
|
495
|
-
|
|
495
|
+
|
|
496
496
|
// Add edges for dependencies
|
|
497
497
|
if (frontmatter.dependsUpon) {
|
|
498
|
-
const dependencies = Array.isArray(frontmatter.dependsUpon)
|
|
499
|
-
? frontmatter.dependsUpon
|
|
498
|
+
const dependencies = Array.isArray(frontmatter.dependsUpon)
|
|
499
|
+
? frontmatter.dependsUpon
|
|
500
500
|
: [frontmatter.dependsUpon];
|
|
501
|
-
|
|
501
|
+
|
|
502
502
|
for (const dep of dependencies) {
|
|
503
503
|
let depPath = dep;
|
|
504
504
|
if (depPath.startsWith('./')) {
|
|
@@ -508,7 +508,7 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
508
508
|
depPath = depPath.substring(1);
|
|
509
509
|
}
|
|
510
510
|
depPath = depPath.replace(/\\/g, '/');
|
|
511
|
-
|
|
511
|
+
|
|
512
512
|
const depNodeId = `feature:${depPath}`;
|
|
513
513
|
featureEdges.push({
|
|
514
514
|
source: depNodeId,
|
|
@@ -544,7 +544,7 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
544
544
|
feature,
|
|
545
545
|
isDocumentation: false,
|
|
546
546
|
});
|
|
547
|
-
|
|
547
|
+
|
|
548
548
|
// Create node for plain feature
|
|
549
549
|
const nodeId = `feature:plain:${feature.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
550
550
|
featureNodes.push({
|
|
@@ -583,16 +583,16 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
583
583
|
try {
|
|
584
584
|
const result: Record<string, any> = {};
|
|
585
585
|
const lines = yamlStr.split('\n');
|
|
586
|
-
|
|
586
|
+
|
|
587
587
|
for (const line of lines) {
|
|
588
588
|
const trimmed = line.trim();
|
|
589
589
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
590
|
-
|
|
590
|
+
|
|
591
591
|
const colonIndex = trimmed.indexOf(':');
|
|
592
592
|
if (colonIndex > 0) {
|
|
593
593
|
const key = trimmed.substring(0, colonIndex).trim();
|
|
594
594
|
let value = trimmed.substring(colonIndex + 1).trim();
|
|
595
|
-
|
|
595
|
+
|
|
596
596
|
// Try to parse values
|
|
597
597
|
if (value === 'true') value = true;
|
|
598
598
|
else if (value === 'false') value = false;
|
|
@@ -610,11 +610,11 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
610
610
|
// Keep as string
|
|
611
611
|
}
|
|
612
612
|
}
|
|
613
|
-
|
|
613
|
+
|
|
614
614
|
result[key] = value;
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
|
-
|
|
617
|
+
|
|
618
618
|
return result;
|
|
619
619
|
} catch (error) {
|
|
620
620
|
console.warn('Failed to parse YAML frontmatter:', error);
|
|
@@ -854,7 +854,7 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
854
854
|
|
|
855
855
|
private static async readDocumentationFiles(files: string[]): Promise<Record<string, string>> {
|
|
856
856
|
const contents: Record<string, string> = {};
|
|
857
|
-
|
|
857
|
+
|
|
858
858
|
for (const file of files) {
|
|
859
859
|
try {
|
|
860
860
|
const fullPath = path.join(process.cwd(), file);
|
|
@@ -866,16 +866,16 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
866
866
|
console.warn(`Could not read documentation file ${file}:`, error);
|
|
867
867
|
}
|
|
868
868
|
}
|
|
869
|
-
|
|
869
|
+
|
|
870
870
|
return contents;
|
|
871
871
|
}
|
|
872
872
|
|
|
873
873
|
private static async extractFileContentsFromTree(tree: any): Promise<Record<string, string>> {
|
|
874
874
|
const contents: Record<string, string> = {};
|
|
875
|
-
|
|
875
|
+
|
|
876
876
|
const traverse = async (node: any, currentPath: string = '') => {
|
|
877
877
|
if (!node) return;
|
|
878
|
-
|
|
878
|
+
|
|
879
879
|
if (node.type === 'file' && node.path) {
|
|
880
880
|
try {
|
|
881
881
|
const fullPath = path.join(process.cwd(), node.path);
|
|
@@ -887,14 +887,14 @@ window.TESTERANTO_EMBEDDED_DATA = JSON.parse(atob('${base64String}'));
|
|
|
887
887
|
console.warn(`Could not read file ${node.path}:`, error);
|
|
888
888
|
}
|
|
889
889
|
}
|
|
890
|
-
|
|
890
|
+
|
|
891
891
|
if (node.children) {
|
|
892
892
|
for (const [key, child] of Object.entries(node.children)) {
|
|
893
893
|
await traverse(child, currentPath ? `${currentPath}/${key}` : key);
|
|
894
894
|
}
|
|
895
895
|
}
|
|
896
896
|
};
|
|
897
|
-
|
|
897
|
+
|
|
898
898
|
await traverse(tree);
|
|
899
899
|
return contents;
|
|
900
900
|
}
|
package/src/viz.md
CHANGED
|
@@ -22,9 +22,9 @@ dependsUpon: ./someOtherFeature.md
|
|
|
22
22
|
|
|
23
23
|
This graph is precomputed and embedded in the html. The front end presents it. WE can still use fetch command, so we do not necessarily need to embed the file contents int the graph, only theirilfenames.
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## grafeovidajo
|
|
26
26
|
|
|
27
|
-
the
|
|
27
|
+
the grafeovidajo library works by mapping graph data to 2d plain. We need to achieve the falling charts
|
|
28
28
|
|
|
29
29
|
- esenhow matric
|
|
30
30
|
- gantt
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import React, { useMemo, useCallback } from 'react';
|
|
2
|
-
import { GraphData, VizComponentProps, Node } from 'viz';
|
|
3
|
-
import { MarkdownChartProps } from './types';
|
|
4
|
-
import { useMarkdownFiles } from './hooks/useMarkdownFiles';
|
|
5
|
-
import { useChartSync } from './hooks/useChartSync';
|
|
6
|
-
import { markdownFilesToGraphData } from './markdown/parser';
|
|
7
|
-
|
|
8
|
-
export const MarkdownChart: React.FC<MarkdownChartProps> = ({
|
|
9
|
-
filePattern,
|
|
10
|
-
watchFiles = false,
|
|
11
|
-
chartComponent: ChartComponent,
|
|
12
|
-
chartConfig,
|
|
13
|
-
attributeMapping,
|
|
14
|
-
onFileChange,
|
|
15
|
-
onNodeUpdate,
|
|
16
|
-
width,
|
|
17
|
-
height
|
|
18
|
-
}) => {
|
|
19
|
-
const {
|
|
20
|
-
files,
|
|
21
|
-
loading,
|
|
22
|
-
error,
|
|
23
|
-
updateFile
|
|
24
|
-
} = useMarkdownFiles(filePattern, watchFiles);
|
|
25
|
-
|
|
26
|
-
const { handleNodeUpdate, handleNodeDrag } = useChartSync(
|
|
27
|
-
files,
|
|
28
|
-
updateFile,
|
|
29
|
-
attributeMapping
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
// Convert markdown files to graph data
|
|
33
|
-
const graphData = useMemo<GraphData>(() => {
|
|
34
|
-
return markdownFilesToGraphData(files, attributeMapping);
|
|
35
|
-
}, [files, attributeMapping]);
|
|
36
|
-
|
|
37
|
-
// Handle node interactions
|
|
38
|
-
const handleNodeClick = useCallback((node: Node) => {
|
|
39
|
-
// Could open the markdown file for editing
|
|
40
|
-
console.log('Node clicked:', node);
|
|
41
|
-
}, []);
|
|
42
|
-
|
|
43
|
-
const handleNodeHover = useCallback((node: Node | null) => {
|
|
44
|
-
// Could show tooltip with file info
|
|
45
|
-
console.log('Node hover:', node);
|
|
46
|
-
}, []);
|
|
47
|
-
|
|
48
|
-
const handleNodeInteraction = useCallback(async (
|
|
49
|
-
node: Node,
|
|
50
|
-
interactionType: 'drag' | 'attributeChange',
|
|
51
|
-
data: any
|
|
52
|
-
) => {
|
|
53
|
-
try {
|
|
54
|
-
if (interactionType === 'drag') {
|
|
55
|
-
await handleNodeDrag(node, data);
|
|
56
|
-
} else if (interactionType === 'attributeChange') {
|
|
57
|
-
await handleNodeUpdate(node, data);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Notify parent component
|
|
61
|
-
onNodeUpdate?.(node, data);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error('Failed to handle node interaction:', error);
|
|
64
|
-
}
|
|
65
|
-
}, [handleNodeDrag, handleNodeUpdate, onNodeUpdate]);
|
|
66
|
-
|
|
67
|
-
if (loading) {
|
|
68
|
-
return <div>Loading markdown files...</div>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (error) {
|
|
72
|
-
return <div>Error loading files: {error.message}</div>;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (files.length === 0) {
|
|
76
|
-
return <div>No markdown files found matching pattern: {filePattern}</div>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Enhance chart config with attribute mapping
|
|
80
|
-
const enhancedConfig = {
|
|
81
|
-
...chartConfig,
|
|
82
|
-
projection: {
|
|
83
|
-
...chartConfig.projection,
|
|
84
|
-
xAttribute: attributeMapping.xAttribute || chartConfig.projection.xAttribute,
|
|
85
|
-
yAttribute: attributeMapping.yAttribute || chartConfig.projection.yAttribute
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<div className="markdown-chart-container">
|
|
91
|
-
<div className="chart-header">
|
|
92
|
-
<h3>Visualizing {files.length} markdown files</h3>
|
|
93
|
-
<div className="file-info">
|
|
94
|
-
Files will be updated as you interact with the chart
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<ChartComponent
|
|
99
|
-
data={graphData}
|
|
100
|
-
config={enhancedConfig}
|
|
101
|
-
width={width}
|
|
102
|
-
height={height}
|
|
103
|
-
onNodeClick={handleNodeClick}
|
|
104
|
-
onNodeHover={handleNodeHover}
|
|
105
|
-
// Additional props for drag interactions could be passed here
|
|
106
|
-
/>
|
|
107
|
-
|
|
108
|
-
<div className="chart-footer">
|
|
109
|
-
<small>
|
|
110
|
-
Each node represents a markdown file. Drag nodes to update their attributes.
|
|
111
|
-
</small>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
};
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
2
|
-
import { Node } from 'viz';
|
|
3
|
-
import { MarkdownFile } from '../types';
|
|
4
|
-
|
|
5
|
-
export function useChartSync(
|
|
6
|
-
files: MarkdownFile[],
|
|
7
|
-
updateFile: (filePath: string, updates: Record<string, any>) => Promise<MarkdownFile>,
|
|
8
|
-
attributeMapping: { idAttribute: string; xAttribute?: string; yAttribute?: string }
|
|
9
|
-
) {
|
|
10
|
-
const handleNodeUpdate = useCallback(async (
|
|
11
|
-
node: Node,
|
|
12
|
-
updates: Record<string, any>
|
|
13
|
-
) => {
|
|
14
|
-
const filePath = node.attributes._path;
|
|
15
|
-
if (!filePath) {
|
|
16
|
-
console.warn('Node has no associated file path:', node.id);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Remove internal attributes from updates
|
|
21
|
-
const cleanUpdates = { ...updates };
|
|
22
|
-
delete cleanUpdates._path;
|
|
23
|
-
delete cleanUpdates._body;
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
await updateFile(filePath, cleanUpdates);
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error('Failed to update file:', error);
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}, [updateFile]);
|
|
32
|
-
|
|
33
|
-
const handleNodeDrag = useCallback(async (
|
|
34
|
-
node: Node,
|
|
35
|
-
newPosition: { x: number; y: number }
|
|
36
|
-
) => {
|
|
37
|
-
// Map screen coordinates back to attribute values
|
|
38
|
-
// This depends on the projection configuration
|
|
39
|
-
const updates: Record<string, any> = {};
|
|
40
|
-
|
|
41
|
-
if (attributeMapping.xAttribute) {
|
|
42
|
-
// This is a simplification - actual mapping would depend on projection config
|
|
43
|
-
updates[attributeMapping.xAttribute] = newPosition.x;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (attributeMapping.yAttribute) {
|
|
47
|
-
updates[attributeMapping.yAttribute] = newPosition.y;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return handleNodeUpdate(node, updates);
|
|
51
|
-
}, [handleNodeUpdate, attributeMapping]);
|
|
52
|
-
|
|
53
|
-
const handleNodeAttributeChange = useCallback(async (
|
|
54
|
-
node: Node,
|
|
55
|
-
attribute: string,
|
|
56
|
-
value: any
|
|
57
|
-
) => {
|
|
58
|
-
return handleNodeUpdate(node, { [attribute]: value });
|
|
59
|
-
}, [handleNodeUpdate]);
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
handleNodeUpdate,
|
|
63
|
-
handleNodeDrag,
|
|
64
|
-
handleNodeAttributeChange
|
|
65
|
-
};
|
|
66
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { MarkdownFile, FileSystemAdapter } from '../types';
|
|
3
|
-
import { parseMarkdownFile, updateMarkdownFile } from '../markdown/parser';
|
|
4
|
-
|
|
5
|
-
// Default file system adapter (can be overridden)
|
|
6
|
-
const defaultFileSystemAdapter: FileSystemAdapter = {
|
|
7
|
-
async readFiles(pattern: string | string[]): Promise<MarkdownFile[]> {
|
|
8
|
-
// This would be implemented with actual file system access
|
|
9
|
-
// For now, return empty array - implementation depends on environment
|
|
10
|
-
console.warn('Default file system adapter not implemented');
|
|
11
|
-
return [];
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
async writeFile(file: MarkdownFile): Promise<void> {
|
|
15
|
-
console.warn('Default file system adapter not implemented');
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
watchFiles(pattern: string | string[], callback: (files: MarkdownFile[]) => void): () => void {
|
|
19
|
-
console.warn('Default file system adapter not implemented');
|
|
20
|
-
return () => {};
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export function useMarkdownFiles(
|
|
25
|
-
filePattern: string | string[],
|
|
26
|
-
watchFiles: boolean = false,
|
|
27
|
-
fileSystemAdapter: FileSystemAdapter = defaultFileSystemAdapter
|
|
28
|
-
) {
|
|
29
|
-
const [files, setFiles] = useState<MarkdownFile[]>([]);
|
|
30
|
-
const [loading, setLoading] = useState(true);
|
|
31
|
-
const [error, setError] = useState<Error | null>(null);
|
|
32
|
-
|
|
33
|
-
const loadFiles = useCallback(async () => {
|
|
34
|
-
try {
|
|
35
|
-
setLoading(true);
|
|
36
|
-
const fileContents = await fileSystemAdapter.readFiles(filePattern);
|
|
37
|
-
setFiles(fileContents);
|
|
38
|
-
setError(null);
|
|
39
|
-
} catch (err) {
|
|
40
|
-
setError(err instanceof Error ? err : new Error('Failed to load files'));
|
|
41
|
-
} finally {
|
|
42
|
-
setLoading(false);
|
|
43
|
-
}
|
|
44
|
-
}, [filePattern, fileSystemAdapter]);
|
|
45
|
-
|
|
46
|
-
const updateFile = useCallback(async (
|
|
47
|
-
filePath: string,
|
|
48
|
-
updates: Record<string, any>
|
|
49
|
-
) => {
|
|
50
|
-
const file = files.find(f => f.path === filePath);
|
|
51
|
-
if (!file) throw new Error(`File not found: ${filePath}`);
|
|
52
|
-
|
|
53
|
-
const updatedFile = updateMarkdownFile(file, updates);
|
|
54
|
-
|
|
55
|
-
await fileSystemAdapter.writeFile(updatedFile);
|
|
56
|
-
|
|
57
|
-
// Update local state
|
|
58
|
-
setFiles(prev => prev.map(f =>
|
|
59
|
-
f.path === filePath ? updatedFile : f
|
|
60
|
-
));
|
|
61
|
-
|
|
62
|
-
return updatedFile;
|
|
63
|
-
}, [files, fileSystemAdapter]);
|
|
64
|
-
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
loadFiles();
|
|
67
|
-
|
|
68
|
-
if (watchFiles) {
|
|
69
|
-
const unwatch = fileSystemAdapter.watchFiles(filePattern, (newFiles) => {
|
|
70
|
-
setFiles(newFiles);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
return unwatch;
|
|
74
|
-
}
|
|
75
|
-
}, [filePattern, watchFiles, fileSystemAdapter, loadFiles]);
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
files,
|
|
79
|
-
loading,
|
|
80
|
-
error,
|
|
81
|
-
reload: loadFiles,
|
|
82
|
-
updateFile
|
|
83
|
-
};
|
|
84
|
-
}
|
package/src/viz-app/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export { MarkdownChart } from './MarkdownChart';
|
|
2
|
-
export { useMarkdownFiles } from './hooks/useMarkdownFiles';
|
|
3
|
-
export { useChartSync } from './hooks/useChartSync';
|
|
4
|
-
export { parseMarkdownFile, markdownFilesToGraphData, updateMarkdownFile } from './markdown/parser';
|
|
5
|
-
|
|
6
|
-
export type {
|
|
7
|
-
MarkdownChartProps,
|
|
8
|
-
MarkdownFile,
|
|
9
|
-
FileSystemAdapter
|
|
10
|
-
} from './types';
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { MarkdownFile } from '../types';
|
|
2
|
-
import { GraphData } from 'viz';
|
|
3
|
-
|
|
4
|
-
const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
5
|
-
|
|
6
|
-
export function parseMarkdownFile(path: string, content: string): MarkdownFile {
|
|
7
|
-
const match = content.match(FRONTMATTER_REGEX);
|
|
8
|
-
|
|
9
|
-
if (match) {
|
|
10
|
-
const [, frontmatterStr, body] = match;
|
|
11
|
-
const frontmatter = parseYAML(frontmatterStr);
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
path,
|
|
15
|
-
content,
|
|
16
|
-
frontmatter,
|
|
17
|
-
body: body.trim()
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// No frontmatter - treat entire content as body
|
|
22
|
-
return {
|
|
23
|
-
path,
|
|
24
|
-
content,
|
|
25
|
-
frontmatter: {},
|
|
26
|
-
body: content.trim()
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function parseYAML(yamlStr: string): Record<string, any> {
|
|
31
|
-
try {
|
|
32
|
-
// Simple YAML parser for basic key-value pairs
|
|
33
|
-
const result: Record<string, any> = {};
|
|
34
|
-
const lines = yamlStr.split('\n');
|
|
35
|
-
|
|
36
|
-
for (const line of lines) {
|
|
37
|
-
const trimmed = line.trim();
|
|
38
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
39
|
-
|
|
40
|
-
const colonIndex = trimmed.indexOf(':');
|
|
41
|
-
if (colonIndex > 0) {
|
|
42
|
-
const key = trimmed.substring(0, colonIndex).trim();
|
|
43
|
-
let value = trimmed.substring(colonIndex + 1).trim();
|
|
44
|
-
|
|
45
|
-
// Try to parse values
|
|
46
|
-
if (value === 'true') value = true;
|
|
47
|
-
else if (value === 'false') value = false;
|
|
48
|
-
else if (value === 'null') value = null;
|
|
49
|
-
else if (!isNaN(Number(value)) && value !== '') value = Number(value);
|
|
50
|
-
else if (value.startsWith('"') && value.endsWith('"')) {
|
|
51
|
-
value = value.slice(1, -1);
|
|
52
|
-
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
53
|
-
value = value.slice(1, -1);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
result[key] = value;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return result;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.warn('Failed to parse YAML frontmatter:', error);
|
|
63
|
-
return {};
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function markdownFilesToGraphData(
|
|
68
|
-
files: MarkdownFile[],
|
|
69
|
-
attributeMapping: { idAttribute: string; xAttribute?: string; yAttribute?: string }
|
|
70
|
-
): GraphData {
|
|
71
|
-
const nodes = files.map(file => {
|
|
72
|
-
const id = file.frontmatter[attributeMapping.idAttribute] ||
|
|
73
|
-
file.path.split('/').pop()?.replace('.md', '') ||
|
|
74
|
-
`node-${Math.random().toString(36).substr(2, 9)}`;
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
id,
|
|
78
|
-
attributes: {
|
|
79
|
-
...file.frontmatter,
|
|
80
|
-
_path: file.path,
|
|
81
|
-
_body: file.body
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
return { nodes };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function updateMarkdownFile(
|
|
90
|
-
file: MarkdownFile,
|
|
91
|
-
updatedAttributes: Record<string, any>
|
|
92
|
-
): MarkdownFile {
|
|
93
|
-
const newFrontmatter = { ...file.frontmatter, ...updatedAttributes };
|
|
94
|
-
|
|
95
|
-
// Remove internal attributes that shouldn't be in frontmatter
|
|
96
|
-
delete newFrontmatter._path;
|
|
97
|
-
delete newFrontmatter._body;
|
|
98
|
-
|
|
99
|
-
// Convert frontmatter back to YAML
|
|
100
|
-
const frontmatterLines = Object.entries(newFrontmatter)
|
|
101
|
-
.map(([key, value]) => {
|
|
102
|
-
if (value === null || value === undefined) return `${key}: null`;
|
|
103
|
-
if (typeof value === 'string') return `${key}: "${value}"`;
|
|
104
|
-
if (typeof value === 'boolean') return `${key}: ${value}`;
|
|
105
|
-
return `${key}: ${value}`;
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const newContent = `---\n${frontmatterLines.join('\n')}\n---\n\n${file.body}`;
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
...file,
|
|
112
|
-
content: newContent,
|
|
113
|
-
frontmatter: newFrontmatter
|
|
114
|
-
};
|
|
115
|
-
}
|
package/src/viz-app/types.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { GraphData, Node, VizConfig, VizComponentProps } from 'viz';
|
|
2
|
-
|
|
3
|
-
export interface MarkdownFile {
|
|
4
|
-
path: string;
|
|
5
|
-
content: string;
|
|
6
|
-
frontmatter: Record<string, any>;
|
|
7
|
-
body: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface MarkdownChartProps {
|
|
11
|
-
// File system configuration
|
|
12
|
-
filePattern: string | string[]; // Glob pattern(s) for markdown files
|
|
13
|
-
watchFiles?: boolean; // Watch for file changes
|
|
14
|
-
|
|
15
|
-
// Chart configuration
|
|
16
|
-
chartComponent: React.ComponentType<VizComponentProps>;
|
|
17
|
-
chartConfig: VizConfig;
|
|
18
|
-
|
|
19
|
-
// Attribute mapping
|
|
20
|
-
attributeMapping: {
|
|
21
|
-
idAttribute: string; // Which frontmatter field becomes node.id
|
|
22
|
-
xAttribute?: string; // Maps to projection.xAttribute
|
|
23
|
-
yAttribute?: string; // Maps to projection.yAttribute
|
|
24
|
-
// Additional attribute mappings can be inferred from frontmatter
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Callbacks
|
|
28
|
-
onFileChange?: (file: MarkdownFile) => void;
|
|
29
|
-
onNodeUpdate?: (node: Node, oldAttributes: Record<string, any>) => void;
|
|
30
|
-
|
|
31
|
-
// Display
|
|
32
|
-
width: number;
|
|
33
|
-
height: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface FileSystemAdapter {
|
|
37
|
-
readFiles(pattern: string | string[]): Promise<MarkdownFile[]>;
|
|
38
|
-
writeFile(file: MarkdownFile): Promise<void>;
|
|
39
|
-
watchFiles(pattern: string | string[], callback: (files: MarkdownFile[]) => void): () => void;
|
|
40
|
-
}
|