wyreframe 0.1.0
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 +123 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +195 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/parser/Core/Bounds.mjs +61 -0
- package/src/parser/Core/Bounds.res +65 -0
- package/src/parser/Core/Grid.mjs +268 -0
- package/src/parser/Core/Grid.res +265 -0
- package/src/parser/Core/Position.mjs +83 -0
- package/src/parser/Core/Position.res +54 -0
- package/src/parser/Core/Types.mjs +435 -0
- package/src/parser/Core/Types.res +331 -0
- package/src/parser/Core/__tests__/Bounds_test.mjs +326 -0
- package/src/parser/Core/__tests__/Bounds_test.res +412 -0
- package/src/parser/Core/__tests__/Grid_test.mjs +322 -0
- package/src/parser/Core/__tests__/Grid_test.res +319 -0
- package/src/parser/Core/__tests__/Types_test.mjs +614 -0
- package/src/parser/Core/__tests__/Types_test.res +650 -0
- package/src/parser/Detector/BoxTracer.mjs +302 -0
- package/src/parser/Detector/BoxTracer.res +374 -0
- package/src/parser/Detector/HierarchyBuilder.mjs +158 -0
- package/src/parser/Detector/HierarchyBuilder.res +315 -0
- package/src/parser/Detector/ShapeDetector.mjs +134 -0
- package/src/parser/Detector/ShapeDetector.res +236 -0
- package/src/parser/Detector/__tests__/BoxTracer_test.mjs +70 -0
- package/src/parser/Detector/__tests__/BoxTracer_test.res +92 -0
- package/src/parser/Detector/__tests__/HierarchyBuilder_test.mjs +489 -0
- package/src/parser/Detector/__tests__/HierarchyBuilder_test.res +849 -0
- package/src/parser/Detector/__tests__/ShapeDetector_test.mjs +377 -0
- package/src/parser/Detector/__tests__/ShapeDetector_test.res +563 -0
- package/src/parser/Errors/ErrorContext.mjs +106 -0
- package/src/parser/Errors/ErrorContext.res +191 -0
- package/src/parser/Errors/ErrorMessages.mjs +289 -0
- package/src/parser/Errors/ErrorMessages.res +303 -0
- package/src/parser/Errors/ErrorTypes.mjs +105 -0
- package/src/parser/Errors/ErrorTypes.res +169 -0
- package/src/parser/Interactions/InteractionMerger.mjs +266 -0
- package/src/parser/Interactions/InteractionMerger.res +450 -0
- package/src/parser/Interactions/InteractionParser.mjs +88 -0
- package/src/parser/Interactions/InteractionParser.res +127 -0
- package/src/parser/Interactions/SimpleInteractionParser.mjs +278 -0
- package/src/parser/Interactions/SimpleInteractionParser.res +262 -0
- package/src/parser/Interactions/__tests__/InteractionMerger_test.mjs +576 -0
- package/src/parser/Interactions/__tests__/InteractionMerger_test.res +646 -0
- package/src/parser/Parser.gen.tsx +96 -0
- package/src/parser/Parser.mjs +212 -0
- package/src/parser/Parser.res +481 -0
- package/src/parser/Scanner/__tests__/Grid_manual.mjs +214 -0
- package/src/parser/Scanner/__tests__/Grid_manual.res +141 -0
- package/src/parser/Semantic/ASTBuilder.mjs +197 -0
- package/src/parser/Semantic/ASTBuilder.res +288 -0
- package/src/parser/Semantic/AlignmentCalc.mjs +41 -0
- package/src/parser/Semantic/AlignmentCalc.res +104 -0
- package/src/parser/Semantic/Elements/ButtonParser.mjs +58 -0
- package/src/parser/Semantic/Elements/ButtonParser.res +131 -0
- package/src/parser/Semantic/Elements/CheckboxParser.mjs +58 -0
- package/src/parser/Semantic/Elements/CheckboxParser.res +79 -0
- package/src/parser/Semantic/Elements/CodeTextParser.mjs +50 -0
- package/src/parser/Semantic/Elements/CodeTextParser.res +111 -0
- package/src/parser/Semantic/Elements/ElementParser.mjs +15 -0
- package/src/parser/Semantic/Elements/ElementParser.res +83 -0
- package/src/parser/Semantic/Elements/EmphasisParser.mjs +46 -0
- package/src/parser/Semantic/Elements/EmphasisParser.res +67 -0
- package/src/parser/Semantic/Elements/InputParser.mjs +41 -0
- package/src/parser/Semantic/Elements/InputParser.res +97 -0
- package/src/parser/Semantic/Elements/LinkParser.mjs +60 -0
- package/src/parser/Semantic/Elements/LinkParser.res +156 -0
- package/src/parser/Semantic/Elements/TextParser.mjs +19 -0
- package/src/parser/Semantic/Elements/TextParser.res +42 -0
- package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.mjs +189 -0
- package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.res +257 -0
- package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.mjs +202 -0
- package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.res +250 -0
- package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.mjs +293 -0
- package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.res +134 -0
- package/src/parser/Semantic/Elements/__tests__/InputParser_test.mjs +253 -0
- package/src/parser/Semantic/Elements/__tests__/InputParser_test.res +304 -0
- package/src/parser/Semantic/Elements/__tests__/LinkParser_test.mjs +289 -0
- package/src/parser/Semantic/Elements/__tests__/LinkParser_test.res +402 -0
- package/src/parser/Semantic/Elements/__tests__/TextParser_test.mjs +149 -0
- package/src/parser/Semantic/Elements/__tests__/TextParser_test.res +167 -0
- package/src/parser/Semantic/ParserRegistry.mjs +82 -0
- package/src/parser/Semantic/ParserRegistry.res +145 -0
- package/src/parser/Semantic/SemanticParser.mjs +850 -0
- package/src/parser/Semantic/SemanticParser.res +1368 -0
- package/src/parser/Semantic/__tests__/ASTBuilder_test.mjs +187 -0
- package/src/parser/Semantic/__tests__/ASTBuilder_test.res +192 -0
- package/src/parser/Semantic/__tests__/ParserRegistry_test.mjs +154 -0
- package/src/parser/Semantic/__tests__/ParserRegistry_test.res +191 -0
- package/src/parser/Semantic/__tests__/SemanticParser_integration_test.mjs +768 -0
- package/src/parser/Semantic/__tests__/SemanticParser_integration_test.res +1069 -0
- package/src/parser/Semantic/__tests__/SemanticParser_manual.mjs +1329 -0
- package/src/parser/Semantic/__tests__/SemanticParser_manual.res +544 -0
- package/src/parser/TestMain.mjs +21 -0
- package/src/parser/TestMain.res +14 -0
- package/src/parser/TextExtractor.mjs +179 -0
- package/src/parser/TextExtractor.res +264 -0
- package/src/parser/__tests__/GridScanner_integration.test.mjs +632 -0
- package/src/parser/__tests__/GridScanner_integration.test.res +816 -0
- package/src/parser/__tests__/Performance.test.mjs +244 -0
- package/src/parser/__tests__/Performance.test.res +371 -0
- package/src/parser/__tests__/PerformanceFixtures.mjs +200 -0
- package/src/parser/__tests__/PerformanceFixtures.res +284 -0
- package/src/parser/__tests__/WyreframeParser_integration.test.mjs +770 -0
- package/src/parser/__tests__/WyreframeParser_integration.test.res +1008 -0
- package/src/parser/__tests__/fixtures/alignment-test.txt +9 -0
- package/src/parser/__tests__/fixtures/all-elements.txt +16 -0
- package/src/parser/__tests__/fixtures/login-scene.txt +17 -0
- package/src/parser/__tests__/fixtures/multi-scene.txt +25 -0
- package/src/parser/__tests__/fixtures/nested-boxes.txt +15 -0
- package/src/parser/__tests__/fixtures/simple-box.txt +5 -0
- package/src/parser/__tests__/fixtures/with-dividers.txt +14 -0
- package/src/renderer/Renderer.gen.tsx +32 -0
- package/src/renderer/Renderer.mjs +391 -0
- package/src/renderer/Renderer.res +558 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Grid_manual_test.res
|
|
2
|
+
// Manual test file for Grid data structure (without Jest)
|
|
3
|
+
// Run with: node src/parser/Scanner/__tests__/Grid_manual_test.mjs
|
|
4
|
+
|
|
5
|
+
open Types
|
|
6
|
+
|
|
7
|
+
// Test helper
|
|
8
|
+
let check = (condition: bool, message: string): unit => {
|
|
9
|
+
if condition {
|
|
10
|
+
Console.log("✓ PASS: " ++ message)
|
|
11
|
+
} else {
|
|
12
|
+
Console.error("✗ FAIL: " ++ message)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let assertEqual = (actual: 'a, expected: 'a, message: string): unit => {
|
|
17
|
+
if actual == expected {
|
|
18
|
+
Console.log("✓ PASS: " ++ message)
|
|
19
|
+
} else {
|
|
20
|
+
Console.error("✗ FAIL: " ++ message)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Console.log("\n=== Grid Construction Tests ===\n")
|
|
25
|
+
|
|
26
|
+
// Test 1: Basic grid construction
|
|
27
|
+
let lines1 = ["abc", "def", "ghi"]
|
|
28
|
+
let grid1 = Grid.fromLines(lines1)
|
|
29
|
+
assertEqual(grid1.width, 3, "Grid width for uniform lines")
|
|
30
|
+
assertEqual(grid1.height, 3, "Grid height for uniform lines")
|
|
31
|
+
|
|
32
|
+
// Test 2: Normalization of varying length lines
|
|
33
|
+
let lines2 = ["abc", "de", "f"]
|
|
34
|
+
let grid2 = Grid.fromLines(lines2)
|
|
35
|
+
assertEqual(grid2.width, 3, "Grid width after normalization")
|
|
36
|
+
assertEqual(grid2.height, 3, "Grid height after normalization")
|
|
37
|
+
|
|
38
|
+
// Test 3: Character indices
|
|
39
|
+
let lines3 = ["+--+", "| |", "+--+"]
|
|
40
|
+
let grid3 = Grid.fromLines(lines3)
|
|
41
|
+
assertEqual(Array.length(grid3.cornerIndex), 4, "Corner index count")
|
|
42
|
+
assertEqual(Array.length(grid3.hLineIndex), 4, "HLine index count")
|
|
43
|
+
// VLines are only at positions (1,0) and (1,3) in the grid
|
|
44
|
+
assertEqual(Array.length(grid3.vLineIndex), 2, "VLine index count")
|
|
45
|
+
|
|
46
|
+
Console.log("\n=== Character Access Tests ===\n")
|
|
47
|
+
|
|
48
|
+
// Test 4: Get character at position
|
|
49
|
+
switch Grid.get(grid3, Position.make(0, 0)) {
|
|
50
|
+
| Some(Corner) => Console.log("✓ PASS: Get character at (0,0) returns Corner")
|
|
51
|
+
| _ => Console.error("✗ FAIL: Get character at (0,0) should return Corner")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Test 5: Get out of bounds
|
|
55
|
+
switch Grid.get(grid3, Position.make(10, 10)) {
|
|
56
|
+
| None => Console.log("✓ PASS: Get character out of bounds returns None")
|
|
57
|
+
| _ => Console.error("✗ FAIL: Get character out of bounds should return None")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Test 6: Get line
|
|
61
|
+
switch Grid.getLine(grid3, 0) {
|
|
62
|
+
| Some(line) => assertEqual(Array.length(line), 4, "Get line returns correct length")
|
|
63
|
+
| None => Console.error("✗ FAIL: Get line should return Some")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Console.log("\n=== Directional Scanning Tests ===\n")
|
|
67
|
+
|
|
68
|
+
// Test 7: Scan right
|
|
69
|
+
let scanResults = Grid.scanRight(grid3, Position.make(0, 0), cell => {
|
|
70
|
+
switch cell {
|
|
71
|
+
| Corner | HLine => true
|
|
72
|
+
| _ => false
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
assertEqual(Array.length(scanResults), 4, "Scan right collects correct number of characters")
|
|
76
|
+
|
|
77
|
+
// Test 8: Scan down
|
|
78
|
+
let scanDownResults = Grid.scanDown(grid3, Position.make(0, 0), cell => {
|
|
79
|
+
switch cell {
|
|
80
|
+
| Corner | VLine => true
|
|
81
|
+
| _ => false
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
assertEqual(Array.length(scanDownResults), 3, "Scan down collects correct number of characters")
|
|
85
|
+
|
|
86
|
+
Console.log("\n=== Search Operations Tests ===\n")
|
|
87
|
+
|
|
88
|
+
// Test 9: Find all corners
|
|
89
|
+
let allCorners = Grid.findAll(grid3, Corner)
|
|
90
|
+
assertEqual(Array.length(allCorners), 4, "Find all corners returns 4 positions")
|
|
91
|
+
|
|
92
|
+
// Test 10: Find in range
|
|
93
|
+
// Grid corners are at (0,0), (0,3), (2,0), (2,3)
|
|
94
|
+
// With bounds top=0, left=0, bottom=1, right=2, only (0,0) is within range
|
|
95
|
+
let bounds = Bounds.make(~top=0, ~left=0, ~bottom=1, ~right=2)
|
|
96
|
+
let cornersInRange = Grid.findInRange(grid3, Corner, bounds)
|
|
97
|
+
assertEqual(
|
|
98
|
+
Array.length(cornersInRange),
|
|
99
|
+
1,
|
|
100
|
+
"Find in range returns corners within bounds",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
Console.log("\n=== Performance Tests ===\n")
|
|
104
|
+
|
|
105
|
+
// Test 11: Large grid performance
|
|
106
|
+
let largeLines = Array.make(~length=1000, "+"->String.repeat(100))
|
|
107
|
+
let startTime = Date.now()
|
|
108
|
+
let largeGrid = Grid.fromLines(largeLines)
|
|
109
|
+
let endTime = Date.now()
|
|
110
|
+
let duration = endTime -. startTime
|
|
111
|
+
|
|
112
|
+
assertEqual(largeGrid.height, 1000, "Large grid has correct height")
|
|
113
|
+
assertEqual(largeGrid.width, 100, "Large grid has correct width")
|
|
114
|
+
check(duration < 10.0, `Grid construction <10ms (actual: ${Float.toString(duration)}ms)`)
|
|
115
|
+
|
|
116
|
+
// Test 12: Index lookup performance
|
|
117
|
+
let findStartTime = Date.now()
|
|
118
|
+
let _corners = Grid.findAll(largeGrid, Corner)
|
|
119
|
+
let findEndTime = Date.now()
|
|
120
|
+
let findDuration = findEndTime -. findStartTime
|
|
121
|
+
|
|
122
|
+
check(
|
|
123
|
+
findDuration < 5.0,
|
|
124
|
+
`Finding all corners using index <5ms (actual: ${Float.toString(findDuration)}ms)`,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
Console.log("\n=== Edge Cases Tests ===\n")
|
|
128
|
+
|
|
129
|
+
// Test 15: Single character grid
|
|
130
|
+
let singleCharLines = ["+"]
|
|
131
|
+
let singleCharGrid = Grid.fromLines(singleCharLines)
|
|
132
|
+
assertEqual(singleCharGrid.width, 1, "Single character grid width")
|
|
133
|
+
assertEqual(singleCharGrid.height, 1, "Single character grid height")
|
|
134
|
+
|
|
135
|
+
// Test 16: Empty lines
|
|
136
|
+
let emptyLines = ["", "", ""]
|
|
137
|
+
let emptyGrid = Grid.fromLines(emptyLines)
|
|
138
|
+
assertEqual(emptyGrid.width, 0, "Empty lines grid width is 0")
|
|
139
|
+
assertEqual(emptyGrid.height, 3, "Empty lines grid height is 3")
|
|
140
|
+
|
|
141
|
+
Console.log("\n=== All Tests Complete ===\n")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Types from "../Core/Types.mjs";
|
|
4
|
+
import * as ErrorTypes from "../Errors/ErrorTypes.mjs";
|
|
5
|
+
import * as Core__Array from "@rescript/core/src/Core__Array.mjs";
|
|
6
|
+
|
|
7
|
+
function astErrorToParseError(error) {
|
|
8
|
+
switch (error.TAG) {
|
|
9
|
+
case "DuplicateSceneId" :
|
|
10
|
+
return ErrorTypes.make({
|
|
11
|
+
TAG: "InvalidElement",
|
|
12
|
+
content: `Duplicate scene ID: "` + error.sceneId + `"`,
|
|
13
|
+
position: error.firstPosition
|
|
14
|
+
}, undefined);
|
|
15
|
+
case "EmptySceneId" :
|
|
16
|
+
return ErrorTypes.make({
|
|
17
|
+
TAG: "InvalidElement",
|
|
18
|
+
content: "Scene ID cannot be empty",
|
|
19
|
+
position: error.position
|
|
20
|
+
}, undefined);
|
|
21
|
+
case "InvalidSceneStructure" :
|
|
22
|
+
return ErrorTypes.make({
|
|
23
|
+
TAG: "InvalidElement",
|
|
24
|
+
content: `Invalid scene structure for "` + error.sceneId + `": ` + error.reason,
|
|
25
|
+
position: Types.Position.make(0, 0)
|
|
26
|
+
}, undefined);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildScene(config) {
|
|
31
|
+
let trimmedId = config.id.trim();
|
|
32
|
+
if (trimmedId === "") {
|
|
33
|
+
return {
|
|
34
|
+
TAG: "Error",
|
|
35
|
+
_0: {
|
|
36
|
+
TAG: "EmptySceneId",
|
|
37
|
+
position: config.position
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
let t = config.title;
|
|
42
|
+
let title = t !== undefined ? t.trim() : trimmedId.split("-").map(word => word.split("").map((char, i) => {
|
|
43
|
+
if (i === 0) {
|
|
44
|
+
return char.toUpperCase();
|
|
45
|
+
} else {
|
|
46
|
+
return char;
|
|
47
|
+
}
|
|
48
|
+
}).join("")).join(" ");
|
|
49
|
+
let t$1 = config.transition;
|
|
50
|
+
let transition = t$1 !== undefined ? t$1.trim() : "none";
|
|
51
|
+
let elements = config.elements;
|
|
52
|
+
let d = config.device;
|
|
53
|
+
let device = d !== undefined ? d : "Desktop";
|
|
54
|
+
return {
|
|
55
|
+
TAG: "Ok",
|
|
56
|
+
_0: {
|
|
57
|
+
id: trimmedId,
|
|
58
|
+
title: title,
|
|
59
|
+
transition: transition,
|
|
60
|
+
device: device,
|
|
61
|
+
elements: elements
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildAST(sceneConfigs) {
|
|
67
|
+
let seenIds = {};
|
|
68
|
+
let errors = [];
|
|
69
|
+
let validScenes = [];
|
|
70
|
+
sceneConfigs.forEach(config => {
|
|
71
|
+
let firstPosition = seenIds[config.id];
|
|
72
|
+
if (firstPosition !== undefined) {
|
|
73
|
+
let error_0 = config.id;
|
|
74
|
+
let error_2 = config.position;
|
|
75
|
+
let error = {
|
|
76
|
+
TAG: "DuplicateSceneId",
|
|
77
|
+
sceneId: error_0,
|
|
78
|
+
firstPosition: firstPosition,
|
|
79
|
+
secondPosition: error_2
|
|
80
|
+
};
|
|
81
|
+
errors.push(astErrorToParseError(error));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
seenIds[config.id] = config.position;
|
|
85
|
+
let scene = buildScene(config);
|
|
86
|
+
if (scene.TAG === "Ok") {
|
|
87
|
+
validScenes.push(scene._0);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
errors.push(astErrorToParseError(scene._0));
|
|
91
|
+
});
|
|
92
|
+
if (errors.length > 0) {
|
|
93
|
+
return {
|
|
94
|
+
TAG: "Error",
|
|
95
|
+
_0: errors
|
|
96
|
+
};
|
|
97
|
+
} else {
|
|
98
|
+
return {
|
|
99
|
+
TAG: "Ok",
|
|
100
|
+
_0: {
|
|
101
|
+
scenes: validScenes
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildSingleSceneAST(id, title, transition, elements, positionOpt) {
|
|
108
|
+
let position = positionOpt !== undefined ? positionOpt : Types.Position.make(0, 0);
|
|
109
|
+
return buildAST([{
|
|
110
|
+
id: id,
|
|
111
|
+
title: title,
|
|
112
|
+
transition: transition,
|
|
113
|
+
device: undefined,
|
|
114
|
+
elements: elements,
|
|
115
|
+
position: position
|
|
116
|
+
}]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mergeASTs(asts) {
|
|
120
|
+
let allScenes = asts.flatMap(ast => ast.scenes);
|
|
121
|
+
return buildAST(allScenes.map(scene => ({
|
|
122
|
+
id: scene.id,
|
|
123
|
+
title: scene.title,
|
|
124
|
+
transition: scene.transition,
|
|
125
|
+
device: scene.device,
|
|
126
|
+
elements: scene.elements,
|
|
127
|
+
position: Types.Position.make(0, 0)
|
|
128
|
+
})));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateAST(ast) {
|
|
132
|
+
let seenIds = {};
|
|
133
|
+
let errors = [];
|
|
134
|
+
ast.scenes.forEach(scene => {
|
|
135
|
+
let match = seenIds[scene.id];
|
|
136
|
+
if (match !== undefined) {
|
|
137
|
+
let error = ErrorTypes.make({
|
|
138
|
+
TAG: "InvalidElement",
|
|
139
|
+
content: `Duplicate scene ID: "` + scene.id + `"`,
|
|
140
|
+
position: Types.Position.make(0, 0)
|
|
141
|
+
}, undefined);
|
|
142
|
+
errors.push(error);
|
|
143
|
+
} else {
|
|
144
|
+
seenIds[scene.id] = Types.Position.make(0, 0);
|
|
145
|
+
}
|
|
146
|
+
if (scene.id.trim() !== "") {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let error$1 = ErrorTypes.make({
|
|
150
|
+
TAG: "InvalidElement",
|
|
151
|
+
content: "Scene ID cannot be empty",
|
|
152
|
+
position: Types.Position.make(0, 0)
|
|
153
|
+
}, undefined);
|
|
154
|
+
errors.push(error$1);
|
|
155
|
+
});
|
|
156
|
+
if (errors.length > 0) {
|
|
157
|
+
return {
|
|
158
|
+
TAG: "Error",
|
|
159
|
+
_0: errors
|
|
160
|
+
};
|
|
161
|
+
} else {
|
|
162
|
+
return {
|
|
163
|
+
TAG: "Ok",
|
|
164
|
+
_0: undefined
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getSceneById(ast, sceneId) {
|
|
170
|
+
return ast.scenes.find(scene => scene.id === sceneId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasScene(ast, sceneId) {
|
|
174
|
+
return ast.scenes.some(scene => scene.id === sceneId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function countElements(ast) {
|
|
178
|
+
return Core__Array.reduce(ast.scenes, 0, (count, scene) => count + scene.elements.length | 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getSceneIds(ast) {
|
|
182
|
+
return ast.scenes.map(scene => scene.id);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
astErrorToParseError,
|
|
187
|
+
buildScene,
|
|
188
|
+
buildAST,
|
|
189
|
+
buildSingleSceneAST,
|
|
190
|
+
mergeASTs,
|
|
191
|
+
validateAST,
|
|
192
|
+
getSceneById,
|
|
193
|
+
hasScene,
|
|
194
|
+
countElements,
|
|
195
|
+
getSceneIds,
|
|
196
|
+
}
|
|
197
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// ASTBuilder.res
|
|
2
|
+
// Module for building Abstract Syntax Trees from parsed elements
|
|
3
|
+
// Handles scene construction, validation, and AST assembly
|
|
4
|
+
|
|
5
|
+
// Import core types
|
|
6
|
+
type element = Types.element
|
|
7
|
+
type scene = Types.scene
|
|
8
|
+
type ast = Types.ast
|
|
9
|
+
type parseError = ErrorTypes.t
|
|
10
|
+
type position = Types.Position.t
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Error specific to AST building operations
|
|
14
|
+
*/
|
|
15
|
+
type astError =
|
|
16
|
+
| DuplicateSceneId({
|
|
17
|
+
sceneId: string,
|
|
18
|
+
firstPosition: position,
|
|
19
|
+
secondPosition: position,
|
|
20
|
+
})
|
|
21
|
+
| EmptySceneId({position: position})
|
|
22
|
+
| InvalidSceneStructure({
|
|
23
|
+
sceneId: string,
|
|
24
|
+
reason: string,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert AST-specific errors to ParseError
|
|
29
|
+
*/
|
|
30
|
+
let astErrorToParseError = (error: astError): parseError => {
|
|
31
|
+
switch error {
|
|
32
|
+
| DuplicateSceneId({sceneId, firstPosition, secondPosition: _}) =>
|
|
33
|
+
ErrorTypes.make(
|
|
34
|
+
InvalidElement({
|
|
35
|
+
content: `Duplicate scene ID: "${sceneId}"`,
|
|
36
|
+
position: firstPosition,
|
|
37
|
+
}),
|
|
38
|
+
None,
|
|
39
|
+
)
|
|
40
|
+
| EmptySceneId({position}) =>
|
|
41
|
+
ErrorTypes.make(
|
|
42
|
+
InvalidElement({
|
|
43
|
+
content: "Scene ID cannot be empty",
|
|
44
|
+
position: position,
|
|
45
|
+
}),
|
|
46
|
+
None,
|
|
47
|
+
)
|
|
48
|
+
| InvalidSceneStructure({sceneId, reason}) =>
|
|
49
|
+
ErrorTypes.make(
|
|
50
|
+
InvalidElement({
|
|
51
|
+
content: `Invalid scene structure for "${sceneId}": ${reason}`,
|
|
52
|
+
position: Types.Position.make(0, 0),
|
|
53
|
+
}),
|
|
54
|
+
None,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scene builder configuration with optional fields
|
|
61
|
+
*/
|
|
62
|
+
type sceneConfig = {
|
|
63
|
+
id: string,
|
|
64
|
+
title: option<string>,
|
|
65
|
+
transition: option<string>,
|
|
66
|
+
device: option<Types.deviceType>,
|
|
67
|
+
elements: array<element>,
|
|
68
|
+
position: position, // Position where scene directive was found
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a single scene from configuration
|
|
73
|
+
* Handles optional fields by providing sensible defaults
|
|
74
|
+
*
|
|
75
|
+
* @param config - Scene configuration with id, title, transition, and elements
|
|
76
|
+
* @returns Result with scene or error
|
|
77
|
+
*/
|
|
78
|
+
let buildScene = (config: sceneConfig): result<scene, astError> => {
|
|
79
|
+
// Validate scene ID is not empty
|
|
80
|
+
let trimmedId = String.trim(config.id)
|
|
81
|
+
if trimmedId == "" {
|
|
82
|
+
Error(EmptySceneId({position: config.position}))
|
|
83
|
+
} else {
|
|
84
|
+
// Use title from config or derive from ID (capitalize and replace hyphens/underscores)
|
|
85
|
+
let title = switch config.title {
|
|
86
|
+
| Some(t) => String.trim(t)
|
|
87
|
+
| None => {
|
|
88
|
+
// Derive title from ID: "login-page" -> "Login Page"
|
|
89
|
+
trimmedId
|
|
90
|
+
->String.split("-")
|
|
91
|
+
->Array.map(word =>
|
|
92
|
+
word->String.split("")->Array.mapWithIndex((char, i) => i == 0 ? String.toUpperCase(char) : char)->Array.join("")
|
|
93
|
+
)
|
|
94
|
+
->Array.join(" ")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Use transition from config or default to "none"
|
|
99
|
+
let transition = switch config.transition {
|
|
100
|
+
| Some(t) => String.trim(t)
|
|
101
|
+
| None => "none"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate elements array (could add more validation here)
|
|
105
|
+
let elements = config.elements
|
|
106
|
+
|
|
107
|
+
// Use device from config or default to Desktop
|
|
108
|
+
let device = switch config.device {
|
|
109
|
+
| Some(d) => d
|
|
110
|
+
| None => Types.Desktop
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Ok({
|
|
114
|
+
id: trimmedId,
|
|
115
|
+
title: title,
|
|
116
|
+
transition: transition,
|
|
117
|
+
device: device,
|
|
118
|
+
elements: elements,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build complete AST from scene configurations
|
|
125
|
+
* Validates unique scene IDs and constructs the final AST
|
|
126
|
+
*
|
|
127
|
+
* @param sceneConfigs - Array of scene configurations to build
|
|
128
|
+
* @returns Result with complete AST or array of errors
|
|
129
|
+
*/
|
|
130
|
+
let buildAST = (sceneConfigs: array<sceneConfig>): result<ast, array<parseError>> => {
|
|
131
|
+
// Track seen scene IDs to detect duplicates
|
|
132
|
+
let seenIds = Dict.make()
|
|
133
|
+
let errors = []
|
|
134
|
+
let validScenes = []
|
|
135
|
+
|
|
136
|
+
// Process each scene configuration
|
|
137
|
+
sceneConfigs->Array.forEach(config => {
|
|
138
|
+
// Check for duplicate scene ID
|
|
139
|
+
switch Dict.get(seenIds, config.id) {
|
|
140
|
+
| Some(firstPosition) => {
|
|
141
|
+
// Duplicate found
|
|
142
|
+
let error = DuplicateSceneId({
|
|
143
|
+
sceneId: config.id,
|
|
144
|
+
firstPosition: firstPosition,
|
|
145
|
+
secondPosition: config.position,
|
|
146
|
+
})
|
|
147
|
+
errors->Array.push(astErrorToParseError(error))->ignore
|
|
148
|
+
}
|
|
149
|
+
| None => {
|
|
150
|
+
// Record this ID
|
|
151
|
+
Dict.set(seenIds, config.id, config.position)
|
|
152
|
+
|
|
153
|
+
// Build the scene
|
|
154
|
+
switch buildScene(config) {
|
|
155
|
+
| Ok(scene) => validScenes->Array.push(scene)->ignore
|
|
156
|
+
| Error(error) => errors->Array.push(astErrorToParseError(error))->ignore
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Return errors if any, otherwise return AST
|
|
163
|
+
if Array.length(errors) > 0 {
|
|
164
|
+
Error(errors)
|
|
165
|
+
} else {
|
|
166
|
+
Ok({scenes: validScenes})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build AST from a single scene (convenience function)
|
|
172
|
+
* Useful when parsing simple wireframes with only one scene
|
|
173
|
+
*/
|
|
174
|
+
let buildSingleSceneAST = (
|
|
175
|
+
~id: string,
|
|
176
|
+
~title: option<string>=?,
|
|
177
|
+
~transition: option<string>=?,
|
|
178
|
+
~elements: array<element>,
|
|
179
|
+
~position: position=Types.Position.make(0, 0),
|
|
180
|
+
): result<ast, array<parseError>> => {
|
|
181
|
+
buildAST([
|
|
182
|
+
{
|
|
183
|
+
id: id,
|
|
184
|
+
title: title,
|
|
185
|
+
transition: transition,
|
|
186
|
+
device: None,
|
|
187
|
+
elements: elements,
|
|
188
|
+
position: position,
|
|
189
|
+
},
|
|
190
|
+
])
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Merge multiple ASTs into a single AST
|
|
195
|
+
* Validates that scene IDs remain unique across merged ASTs
|
|
196
|
+
*/
|
|
197
|
+
let mergeASTs = (asts: array<ast>): result<ast, array<parseError>> => {
|
|
198
|
+
// Extract all scenes from all ASTs
|
|
199
|
+
let allScenes = asts->Array.flatMap(ast => ast.scenes)
|
|
200
|
+
|
|
201
|
+
// Convert scenes back to configs for validation
|
|
202
|
+
let configs = allScenes->Array.map(scene => {
|
|
203
|
+
{
|
|
204
|
+
id: scene.id,
|
|
205
|
+
title: Some(scene.title),
|
|
206
|
+
transition: Some(scene.transition),
|
|
207
|
+
device: Some(scene.device),
|
|
208
|
+
elements: scene.elements,
|
|
209
|
+
position: Types.Position.make(0, 0), // Position not available from scene
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Use buildAST to validate and merge
|
|
214
|
+
buildAST(configs)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validate an existing AST
|
|
219
|
+
* Checks for duplicate scene IDs and other structural issues
|
|
220
|
+
*/
|
|
221
|
+
let validateAST = (ast: ast): result<unit, array<parseError>> => {
|
|
222
|
+
let seenIds = Dict.make()
|
|
223
|
+
let errors = []
|
|
224
|
+
|
|
225
|
+
ast.scenes->Array.forEach(scene => {
|
|
226
|
+
switch Dict.get(seenIds, scene.id) {
|
|
227
|
+
| Some(_) => {
|
|
228
|
+
let error = ErrorTypes.make(
|
|
229
|
+
InvalidElement({
|
|
230
|
+
content: `Duplicate scene ID: "${scene.id}"`,
|
|
231
|
+
position: Types.Position.make(0, 0),
|
|
232
|
+
}),
|
|
233
|
+
None,
|
|
234
|
+
)
|
|
235
|
+
errors->Array.push(error)->ignore
|
|
236
|
+
}
|
|
237
|
+
| None => Dict.set(seenIds, scene.id, Types.Position.make(0, 0))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Validate scene has a non-empty ID
|
|
241
|
+
if String.trim(scene.id) == "" {
|
|
242
|
+
let error = ErrorTypes.make(
|
|
243
|
+
InvalidElement({
|
|
244
|
+
content: "Scene ID cannot be empty",
|
|
245
|
+
position: Types.Position.make(0, 0),
|
|
246
|
+
}),
|
|
247
|
+
None,
|
|
248
|
+
)
|
|
249
|
+
errors->Array.push(error)->ignore
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if Array.length(errors) > 0 {
|
|
254
|
+
Error(errors)
|
|
255
|
+
} else {
|
|
256
|
+
Ok()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get scene by ID from AST
|
|
262
|
+
*/
|
|
263
|
+
let getSceneById = (ast: ast, sceneId: string): option<scene> => {
|
|
264
|
+
ast.scenes->Array.find(scene => scene.id == sceneId)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check if AST contains a scene with given ID
|
|
269
|
+
*/
|
|
270
|
+
let hasScene = (ast: ast, sceneId: string): bool => {
|
|
271
|
+
ast.scenes->Array.some(scene => scene.id == sceneId)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Count total elements across all scenes
|
|
276
|
+
*/
|
|
277
|
+
let countElements = (ast: ast): int => {
|
|
278
|
+
ast.scenes->Array.reduce(0, (count, scene) => {
|
|
279
|
+
count + Array.length(scene.elements)
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get all scene IDs from AST
|
|
285
|
+
*/
|
|
286
|
+
let getSceneIds = (ast: ast): array<string> => {
|
|
287
|
+
ast.scenes->Array.map(scene => scene.id)
|
|
288
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function calculate(content, position, boxBounds) {
|
|
5
|
+
let trimmed = content.trim();
|
|
6
|
+
let contentStart = position.col;
|
|
7
|
+
let contentEnd = contentStart + trimmed.length | 0;
|
|
8
|
+
let boxLeft = boxBounds.left + 1 | 0;
|
|
9
|
+
let boxRight = boxBounds.right - 1 | 0;
|
|
10
|
+
let boxWidth = boxRight - boxLeft | 0;
|
|
11
|
+
if (boxWidth <= 0) {
|
|
12
|
+
return "Left";
|
|
13
|
+
}
|
|
14
|
+
let leftSpace = contentStart - boxLeft | 0;
|
|
15
|
+
let rightSpace = boxRight - contentEnd | 0;
|
|
16
|
+
let leftRatio = leftSpace / boxWidth;
|
|
17
|
+
let rightRatio = rightSpace / boxWidth;
|
|
18
|
+
if (leftRatio < 0.2 && rightRatio > 0.3) {
|
|
19
|
+
return "Left";
|
|
20
|
+
} else if (rightRatio < 0.2 && leftRatio > 0.3) {
|
|
21
|
+
return "Right";
|
|
22
|
+
} else if (Math.abs(leftRatio - rightRatio) < 0.15) {
|
|
23
|
+
return "Center";
|
|
24
|
+
} else {
|
|
25
|
+
return "Left";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function calculateWithStrategy(content, position, boxBounds, strategy) {
|
|
30
|
+
if (strategy === "RespectPosition") {
|
|
31
|
+
return calculate(content, position, boxBounds);
|
|
32
|
+
} else {
|
|
33
|
+
return "Left";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
calculate,
|
|
39
|
+
calculateWithStrategy,
|
|
40
|
+
}
|
|
41
|
+
/* No side effect */
|