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.
Files changed (117) hide show
  1. package/README.md +123 -0
  2. package/dist/index.d.ts +267 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +195 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +63 -0
  7. package/src/parser/Core/Bounds.mjs +61 -0
  8. package/src/parser/Core/Bounds.res +65 -0
  9. package/src/parser/Core/Grid.mjs +268 -0
  10. package/src/parser/Core/Grid.res +265 -0
  11. package/src/parser/Core/Position.mjs +83 -0
  12. package/src/parser/Core/Position.res +54 -0
  13. package/src/parser/Core/Types.mjs +435 -0
  14. package/src/parser/Core/Types.res +331 -0
  15. package/src/parser/Core/__tests__/Bounds_test.mjs +326 -0
  16. package/src/parser/Core/__tests__/Bounds_test.res +412 -0
  17. package/src/parser/Core/__tests__/Grid_test.mjs +322 -0
  18. package/src/parser/Core/__tests__/Grid_test.res +319 -0
  19. package/src/parser/Core/__tests__/Types_test.mjs +614 -0
  20. package/src/parser/Core/__tests__/Types_test.res +650 -0
  21. package/src/parser/Detector/BoxTracer.mjs +302 -0
  22. package/src/parser/Detector/BoxTracer.res +374 -0
  23. package/src/parser/Detector/HierarchyBuilder.mjs +158 -0
  24. package/src/parser/Detector/HierarchyBuilder.res +315 -0
  25. package/src/parser/Detector/ShapeDetector.mjs +134 -0
  26. package/src/parser/Detector/ShapeDetector.res +236 -0
  27. package/src/parser/Detector/__tests__/BoxTracer_test.mjs +70 -0
  28. package/src/parser/Detector/__tests__/BoxTracer_test.res +92 -0
  29. package/src/parser/Detector/__tests__/HierarchyBuilder_test.mjs +489 -0
  30. package/src/parser/Detector/__tests__/HierarchyBuilder_test.res +849 -0
  31. package/src/parser/Detector/__tests__/ShapeDetector_test.mjs +377 -0
  32. package/src/parser/Detector/__tests__/ShapeDetector_test.res +563 -0
  33. package/src/parser/Errors/ErrorContext.mjs +106 -0
  34. package/src/parser/Errors/ErrorContext.res +191 -0
  35. package/src/parser/Errors/ErrorMessages.mjs +289 -0
  36. package/src/parser/Errors/ErrorMessages.res +303 -0
  37. package/src/parser/Errors/ErrorTypes.mjs +105 -0
  38. package/src/parser/Errors/ErrorTypes.res +169 -0
  39. package/src/parser/Interactions/InteractionMerger.mjs +266 -0
  40. package/src/parser/Interactions/InteractionMerger.res +450 -0
  41. package/src/parser/Interactions/InteractionParser.mjs +88 -0
  42. package/src/parser/Interactions/InteractionParser.res +127 -0
  43. package/src/parser/Interactions/SimpleInteractionParser.mjs +278 -0
  44. package/src/parser/Interactions/SimpleInteractionParser.res +262 -0
  45. package/src/parser/Interactions/__tests__/InteractionMerger_test.mjs +576 -0
  46. package/src/parser/Interactions/__tests__/InteractionMerger_test.res +646 -0
  47. package/src/parser/Parser.gen.tsx +96 -0
  48. package/src/parser/Parser.mjs +212 -0
  49. package/src/parser/Parser.res +481 -0
  50. package/src/parser/Scanner/__tests__/Grid_manual.mjs +214 -0
  51. package/src/parser/Scanner/__tests__/Grid_manual.res +141 -0
  52. package/src/parser/Semantic/ASTBuilder.mjs +197 -0
  53. package/src/parser/Semantic/ASTBuilder.res +288 -0
  54. package/src/parser/Semantic/AlignmentCalc.mjs +41 -0
  55. package/src/parser/Semantic/AlignmentCalc.res +104 -0
  56. package/src/parser/Semantic/Elements/ButtonParser.mjs +58 -0
  57. package/src/parser/Semantic/Elements/ButtonParser.res +131 -0
  58. package/src/parser/Semantic/Elements/CheckboxParser.mjs +58 -0
  59. package/src/parser/Semantic/Elements/CheckboxParser.res +79 -0
  60. package/src/parser/Semantic/Elements/CodeTextParser.mjs +50 -0
  61. package/src/parser/Semantic/Elements/CodeTextParser.res +111 -0
  62. package/src/parser/Semantic/Elements/ElementParser.mjs +15 -0
  63. package/src/parser/Semantic/Elements/ElementParser.res +83 -0
  64. package/src/parser/Semantic/Elements/EmphasisParser.mjs +46 -0
  65. package/src/parser/Semantic/Elements/EmphasisParser.res +67 -0
  66. package/src/parser/Semantic/Elements/InputParser.mjs +41 -0
  67. package/src/parser/Semantic/Elements/InputParser.res +97 -0
  68. package/src/parser/Semantic/Elements/LinkParser.mjs +60 -0
  69. package/src/parser/Semantic/Elements/LinkParser.res +156 -0
  70. package/src/parser/Semantic/Elements/TextParser.mjs +19 -0
  71. package/src/parser/Semantic/Elements/TextParser.res +42 -0
  72. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.mjs +189 -0
  73. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.res +257 -0
  74. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.mjs +202 -0
  75. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.res +250 -0
  76. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.mjs +293 -0
  77. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.res +134 -0
  78. package/src/parser/Semantic/Elements/__tests__/InputParser_test.mjs +253 -0
  79. package/src/parser/Semantic/Elements/__tests__/InputParser_test.res +304 -0
  80. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.mjs +289 -0
  81. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.res +402 -0
  82. package/src/parser/Semantic/Elements/__tests__/TextParser_test.mjs +149 -0
  83. package/src/parser/Semantic/Elements/__tests__/TextParser_test.res +167 -0
  84. package/src/parser/Semantic/ParserRegistry.mjs +82 -0
  85. package/src/parser/Semantic/ParserRegistry.res +145 -0
  86. package/src/parser/Semantic/SemanticParser.mjs +850 -0
  87. package/src/parser/Semantic/SemanticParser.res +1368 -0
  88. package/src/parser/Semantic/__tests__/ASTBuilder_test.mjs +187 -0
  89. package/src/parser/Semantic/__tests__/ASTBuilder_test.res +192 -0
  90. package/src/parser/Semantic/__tests__/ParserRegistry_test.mjs +154 -0
  91. package/src/parser/Semantic/__tests__/ParserRegistry_test.res +191 -0
  92. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.mjs +768 -0
  93. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.res +1069 -0
  94. package/src/parser/Semantic/__tests__/SemanticParser_manual.mjs +1329 -0
  95. package/src/parser/Semantic/__tests__/SemanticParser_manual.res +544 -0
  96. package/src/parser/TestMain.mjs +21 -0
  97. package/src/parser/TestMain.res +14 -0
  98. package/src/parser/TextExtractor.mjs +179 -0
  99. package/src/parser/TextExtractor.res +264 -0
  100. package/src/parser/__tests__/GridScanner_integration.test.mjs +632 -0
  101. package/src/parser/__tests__/GridScanner_integration.test.res +816 -0
  102. package/src/parser/__tests__/Performance.test.mjs +244 -0
  103. package/src/parser/__tests__/Performance.test.res +371 -0
  104. package/src/parser/__tests__/PerformanceFixtures.mjs +200 -0
  105. package/src/parser/__tests__/PerformanceFixtures.res +284 -0
  106. package/src/parser/__tests__/WyreframeParser_integration.test.mjs +770 -0
  107. package/src/parser/__tests__/WyreframeParser_integration.test.res +1008 -0
  108. package/src/parser/__tests__/fixtures/alignment-test.txt +9 -0
  109. package/src/parser/__tests__/fixtures/all-elements.txt +16 -0
  110. package/src/parser/__tests__/fixtures/login-scene.txt +17 -0
  111. package/src/parser/__tests__/fixtures/multi-scene.txt +25 -0
  112. package/src/parser/__tests__/fixtures/nested-boxes.txt +15 -0
  113. package/src/parser/__tests__/fixtures/simple-box.txt +5 -0
  114. package/src/parser/__tests__/fixtures/with-dividers.txt +14 -0
  115. package/src/renderer/Renderer.gen.tsx +32 -0
  116. package/src/renderer/Renderer.mjs +391 -0
  117. 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 */