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,302 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Grid from "../Core/Grid.mjs";
4
+ import * as Types from "../Core/Types.mjs";
5
+ import * as ErrorTypes from "../Errors/ErrorTypes.mjs";
6
+
7
+ function extractBoxName(topEdgeChars) {
8
+ let chars = topEdgeChars.map(Grid.cellCharToString);
9
+ let content = chars.join("");
10
+ let trimmed = content.replaceAll("+", "").replaceAll("-", "").replaceAll("=", "").trim();
11
+ if (trimmed.length > 0) {
12
+ return trimmed;
13
+ }
14
+ }
15
+
16
+ function isValidHorizontalChar(cell) {
17
+ if (typeof cell === "object") {
18
+ return true;
19
+ }
20
+ switch (cell) {
21
+ case "VLine" :
22
+ case "Space" :
23
+ return false;
24
+ default:
25
+ return true;
26
+ }
27
+ }
28
+
29
+ function isValidVerticalChar(cell) {
30
+ if (typeof cell === "object") {
31
+ return false;
32
+ }
33
+ switch (cell) {
34
+ case "Corner" :
35
+ case "VLine" :
36
+ return true;
37
+ default:
38
+ return false;
39
+ }
40
+ }
41
+
42
+ function isDividerOnlyEdge(edgeChars) {
43
+ return !edgeChars.some(cell => {
44
+ if (typeof cell !== "object") {
45
+ return cell === "HLine";
46
+ } else {
47
+ return true;
48
+ }
49
+ });
50
+ }
51
+
52
+ function traceBox(grid, topLeft) {
53
+ let match = Grid.get(grid, topLeft);
54
+ if (match === "Corner" && typeof match !== "object") {
55
+ let topEdgeScan = Grid.scanRight(grid, topLeft, isValidHorizontalChar);
56
+ let lastCorner = {
57
+ contents: undefined
58
+ };
59
+ topEdgeScan.forEach(param => {
60
+ let pos = param[0];
61
+ let tmp = param[1];
62
+ if (typeof tmp !== "object" && tmp === "Corner" && !Types.Position.equals(pos, topLeft)) {
63
+ lastCorner.contents = pos;
64
+ return;
65
+ }
66
+ });
67
+ let topRightOpt = lastCorner.contents;
68
+ if (topRightOpt === undefined) {
69
+ return {
70
+ TAG: "Error",
71
+ _0: ErrorTypes.makeSimple({
72
+ TAG: "UncloseBox",
73
+ corner: Types.Position.make(topLeft.row, topLeft.col),
74
+ direction: "top"
75
+ })
76
+ };
77
+ }
78
+ let topEdgeChars = topEdgeScan.map(param => param[1]);
79
+ if (isDividerOnlyEdge(topEdgeChars)) {
80
+ return {
81
+ TAG: "Error",
82
+ _0: ErrorTypes.makeSimple({
83
+ TAG: "InvalidElement",
84
+ content: "Divider-only pattern, not a box border",
85
+ position: topLeft
86
+ })
87
+ };
88
+ }
89
+ let boxName = extractBoxName(topEdgeChars);
90
+ let topWidth = topRightOpt.col - topLeft.col | 0;
91
+ let rightEdgeScan = Grid.scanDown(grid, topRightOpt, isValidVerticalChar);
92
+ let lastCorner$1 = {
93
+ contents: undefined
94
+ };
95
+ rightEdgeScan.forEach(param => {
96
+ let pos = param[0];
97
+ let tmp = param[1];
98
+ if (typeof tmp !== "object" && tmp === "Corner" && !Types.Position.equals(pos, topRightOpt)) {
99
+ lastCorner$1.contents = pos;
100
+ return;
101
+ }
102
+ });
103
+ let bottomRightOpt = lastCorner$1.contents;
104
+ if (bottomRightOpt !== undefined) {
105
+ let bottomEdgeScan = Grid.scanLeft(grid, bottomRightOpt, isValidHorizontalChar);
106
+ let lastCorner$2 = {
107
+ contents: undefined
108
+ };
109
+ bottomEdgeScan.forEach(param => {
110
+ let pos = param[0];
111
+ let tmp = param[1];
112
+ if (typeof tmp !== "object" && tmp === "Corner" && !Types.Position.equals(pos, bottomRightOpt)) {
113
+ lastCorner$2.contents = pos;
114
+ return;
115
+ }
116
+ });
117
+ let bottomLeftOpt = lastCorner$2.contents;
118
+ if (bottomLeftOpt === undefined) {
119
+ return {
120
+ TAG: "Error",
121
+ _0: ErrorTypes.makeSimple({
122
+ TAG: "UncloseBox",
123
+ corner: bottomRightOpt,
124
+ direction: "bottom"
125
+ })
126
+ };
127
+ }
128
+ let bottomWidth = bottomRightOpt.col - bottomLeftOpt.col | 0;
129
+ if (topWidth !== bottomWidth) {
130
+ return {
131
+ TAG: "Error",
132
+ _0: ErrorTypes.makeSimple({
133
+ TAG: "MismatchedWidth",
134
+ topLeft: topLeft,
135
+ topWidth: topWidth,
136
+ bottomWidth: bottomWidth
137
+ })
138
+ };
139
+ }
140
+ let leftEdgeScan = Grid.scanUp(grid, bottomLeftOpt, isValidVerticalChar);
141
+ let reachesStart = leftEdgeScan.some(param => Types.Position.equals(param[0], topLeft));
142
+ if (!reachesStart) {
143
+ return {
144
+ TAG: "Error",
145
+ _0: ErrorTypes.makeSimple({
146
+ TAG: "UncloseBox",
147
+ corner: bottomLeftOpt,
148
+ direction: "left"
149
+ })
150
+ };
151
+ }
152
+ let leftAlignmentError = {
153
+ contents: undefined
154
+ };
155
+ leftEdgeScan.forEach(param => {
156
+ let pos = param[0];
157
+ let tmp = param[1];
158
+ if (typeof tmp !== "object" && tmp === "VLine" && pos.col !== topLeft.col) {
159
+ leftAlignmentError.contents = ErrorTypes.makeSimple({
160
+ TAG: "MisalignedPipe",
161
+ position: pos,
162
+ expectedCol: topLeft.col,
163
+ actualCol: pos.col
164
+ });
165
+ return;
166
+ }
167
+ });
168
+ let err = leftAlignmentError.contents;
169
+ if (err !== undefined) {
170
+ return {
171
+ TAG: "Error",
172
+ _0: err
173
+ };
174
+ }
175
+ let rightAlignmentError = {
176
+ contents: undefined
177
+ };
178
+ rightEdgeScan.forEach(param => {
179
+ let pos = param[0];
180
+ let tmp = param[1];
181
+ if (typeof tmp !== "object" && tmp === "VLine" && pos.col !== topRightOpt.col) {
182
+ rightAlignmentError.contents = ErrorTypes.makeSimple({
183
+ TAG: "MisalignedPipe",
184
+ position: pos,
185
+ expectedCol: topRightOpt.col,
186
+ actualCol: pos.col
187
+ });
188
+ return;
189
+ }
190
+ });
191
+ let err$1 = rightAlignmentError.contents;
192
+ if (err$1 !== undefined) {
193
+ return {
194
+ TAG: "Error",
195
+ _0: err$1
196
+ };
197
+ }
198
+ let bounds_top = topLeft.row;
199
+ let bounds_left = topLeft.col;
200
+ let bounds_bottom = bottomLeftOpt.row;
201
+ let bounds_right = topRightOpt.col;
202
+ let bounds = {
203
+ top: bounds_top,
204
+ left: bounds_left,
205
+ bottom: bounds_bottom,
206
+ right: bounds_right
207
+ };
208
+ return {
209
+ TAG: "Ok",
210
+ _0: {
211
+ name: boxName,
212
+ bounds: bounds,
213
+ children: []
214
+ }
215
+ };
216
+ }
217
+ let leftEdgeScan$1 = Grid.scanDown(grid, topLeft, isValidVerticalChar);
218
+ let lastCorner$3 = {
219
+ contents: undefined
220
+ };
221
+ leftEdgeScan$1.forEach(param => {
222
+ let pos = param[0];
223
+ let tmp = param[1];
224
+ if (typeof tmp !== "object" && tmp === "Corner" && !Types.Position.equals(pos, topLeft)) {
225
+ lastCorner$3.contents = pos;
226
+ return;
227
+ }
228
+ });
229
+ let bottomLeftOpt$1 = lastCorner$3.contents;
230
+ if (bottomLeftOpt$1 === undefined) {
231
+ return {
232
+ TAG: "Error",
233
+ _0: ErrorTypes.makeSimple({
234
+ TAG: "UncloseBox",
235
+ corner: topRightOpt,
236
+ direction: "right"
237
+ })
238
+ };
239
+ }
240
+ let bottomEdgeScan$1 = Grid.scanRight(grid, bottomLeftOpt$1, isValidHorizontalChar);
241
+ let lastCorner$4 = {
242
+ contents: undefined
243
+ };
244
+ bottomEdgeScan$1.forEach(param => {
245
+ let pos = param[0];
246
+ let tmp = param[1];
247
+ if (typeof tmp !== "object" && tmp === "Corner" && !Types.Position.equals(pos, bottomLeftOpt$1)) {
248
+ lastCorner$4.contents = pos;
249
+ return;
250
+ }
251
+ });
252
+ let bottomRightFromLeft = lastCorner$4.contents;
253
+ if (bottomRightFromLeft === undefined) {
254
+ return {
255
+ TAG: "Error",
256
+ _0: ErrorTypes.makeSimple({
257
+ TAG: "UncloseBox",
258
+ corner: bottomLeftOpt$1,
259
+ direction: "bottom"
260
+ })
261
+ };
262
+ }
263
+ let bottomWidth$1 = bottomRightFromLeft.col - bottomLeftOpt$1.col | 0;
264
+ if (topWidth !== bottomWidth$1) {
265
+ return {
266
+ TAG: "Error",
267
+ _0: ErrorTypes.makeSimple({
268
+ TAG: "MismatchedWidth",
269
+ topLeft: topLeft,
270
+ topWidth: topWidth,
271
+ bottomWidth: bottomWidth$1
272
+ })
273
+ };
274
+ } else {
275
+ return {
276
+ TAG: "Error",
277
+ _0: ErrorTypes.makeSimple({
278
+ TAG: "UncloseBox",
279
+ corner: topRightOpt,
280
+ direction: "right"
281
+ })
282
+ };
283
+ }
284
+ }
285
+ return {
286
+ TAG: "Error",
287
+ _0: ErrorTypes.makeSimple({
288
+ TAG: "InvalidElement",
289
+ content: "Expected '+' corner at start position",
290
+ position: topLeft
291
+ })
292
+ };
293
+ }
294
+
295
+ export {
296
+ extractBoxName,
297
+ isValidHorizontalChar,
298
+ isValidVerticalChar,
299
+ isDividerOnlyEdge,
300
+ traceBox,
301
+ }
302
+ /* Grid Not a pure module */
@@ -0,0 +1,374 @@
1
+ // BoxTracer.res
2
+ // Box boundary tracing implementation
3
+ // Traces rectangular boxes starting from corner characters and validates structure
4
+ // Integrates with ErrorTypes for structured error reporting with context
5
+
6
+ open Types
7
+
8
+ // Box structure (without children initially, those are added by HierarchyBuilder)
9
+ type rec box = {
10
+ name: option<string>,
11
+ bounds: Bounds.t,
12
+ mutable children: array<box>,
13
+ }
14
+
15
+ // Result type using structured ParseError from ErrorTypes module
16
+ // Requirements: REQ-16 (Structured Error Objects)
17
+ type traceResult = result<box, ErrorTypes.t>
18
+
19
+ // Extract box name from top border characters
20
+ // Recognizes patterns like "+--Name--+" and extracts "Name"
21
+ // Also handles divider borders like "+===+" by treating "=" as a border char
22
+ let extractBoxName = (topEdgeChars: array<cellChar>): option<string> => {
23
+ // Convert cellChars to string
24
+ let chars = Array.map(topEdgeChars, Grid.cellCharToString)
25
+ let content = Array.join(chars, "")
26
+
27
+ // Remove leading and trailing dashes, equals signs, and corners
28
+ // These are all border characters that should not be part of the name
29
+ let trimmed = content
30
+ ->String.replaceAll("+", "")
31
+ ->String.replaceAll("-", "")
32
+ ->String.replaceAll("=", "") // Divider character is also a border
33
+ ->String.trim
34
+
35
+ // If there's any content left, it's the box name
36
+ if String.length(trimmed) > 0 {
37
+ Some(trimmed)
38
+ } else {
39
+ None
40
+ }
41
+ }
42
+
43
+ // Check if a cellChar is valid for horizontal edges (top/bottom)
44
+ let isValidHorizontalChar = (cell: cellChar): bool => {
45
+ switch cell {
46
+ | HLine | Divider | Corner | Char(_) => true
47
+ | _ => false
48
+ }
49
+ }
50
+
51
+ // Check if a cellChar is valid for vertical edges (left/right)
52
+ let isValidVerticalChar = (cell: cellChar): bool => {
53
+ switch cell {
54
+ | VLine | Corner => true
55
+ | _ => false
56
+ }
57
+ }
58
+
59
+ // Check if a top edge is a divider-only pattern (e.g., +===+)
60
+ // These should not be treated as box borders
61
+ let isDividerOnlyEdge = (edgeChars: array<cellChar>): bool => {
62
+ // Check if edge contains only Corners and Dividers (no HLine or Char)
63
+ let hasHLineOrChar = edgeChars->Array.some(cell => {
64
+ switch cell {
65
+ | HLine | Char(_) => true
66
+ | _ => false
67
+ }
68
+ })
69
+ // If there are no HLine or Char, it's a divider-only pattern
70
+ !hasHLineOrChar
71
+ }
72
+
73
+ /**
74
+ * Trace a box starting from the top-left corner position.
75
+ *
76
+ * Algorithm:
77
+ * 1. Verify starting position is a Corner ('+')
78
+ * 2. Scan right along top edge to find top-right corner
79
+ * 3. Extract box name from top edge if present
80
+ * 4. Scan down from top-right to find bottom-right corner
81
+ * 5. Scan left from bottom-right to find bottom-left corner
82
+ * 6. Validate bottom width matches top width
83
+ * 7. Scan up from bottom-left to verify it reaches starting position
84
+ * 8. Validate vertical pipe alignment
85
+ * 9. Create Bounds and return box
86
+ */
87
+ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
88
+ // Step 1: Verify starting position is a corner
89
+ switch Grid.get(grid, topLeft) {
90
+ | Some(Corner) => {
91
+ // Step 2: Scan right along top edge to find top-right corner
92
+ let topEdgeScan = Grid.scanRight(grid, topLeft, isValidHorizontalChar)
93
+
94
+ // Find the top-right corner (last Corner in the scan)
95
+ let topRightOpt = {
96
+ let lastCorner = ref(None)
97
+ Array.forEach(topEdgeScan, ((pos, cell)) => {
98
+ switch cell {
99
+ | Corner if !Position.equals(pos, topLeft) => lastCorner := Some(pos)
100
+ | _ => ()
101
+ }
102
+ })
103
+ lastCorner.contents
104
+ }
105
+
106
+ switch topRightOpt {
107
+ | None =>
108
+ Error(
109
+ ErrorTypes.makeSimple(
110
+ ErrorTypes.UncloseBox({
111
+ corner: Position.make(topLeft.row, topLeft.col),
112
+ direction: "top",
113
+ }),
114
+ ),
115
+ )
116
+ | Some(topRight) => {
117
+ // Step 3: Extract box name from top edge
118
+ let topEdgeChars = Array.map(topEdgeScan, ((_, cell)) => cell)
119
+
120
+ // Check if this is a divider-only pattern (+===+)
121
+ // These should not be traced as boxes
122
+ if isDividerOnlyEdge(topEdgeChars) {
123
+ Error(
124
+ ErrorTypes.makeSimple(
125
+ ErrorTypes.InvalidElement({
126
+ content: "Divider-only pattern, not a box border",
127
+ position: topLeft,
128
+ }),
129
+ ),
130
+ )
131
+ } else {
132
+ let boxName = extractBoxName(topEdgeChars)
133
+
134
+ // Calculate top width
135
+ let topWidth = topRight.col - topLeft.col
136
+
137
+ // Step 4: Scan down from top-right to find bottom-right corner
138
+ let rightEdgeScan = Grid.scanDown(grid, topRight, isValidVerticalChar)
139
+
140
+ let bottomRightOpt = {
141
+ let lastCorner = ref(None)
142
+ Array.forEach(rightEdgeScan, ((pos, cell)) => {
143
+ switch cell {
144
+ | Corner if !Position.equals(pos, topRight) => lastCorner := Some(pos)
145
+ | _ => ()
146
+ }
147
+ })
148
+ lastCorner.contents
149
+ }
150
+
151
+ switch bottomRightOpt {
152
+ | None => {
153
+ // Right edge scan failed - could be width mismatch
154
+ // Try tracing from left side to detect width mismatch
155
+ let leftEdgeScan = Grid.scanDown(grid, topLeft, isValidVerticalChar)
156
+ let bottomLeftOpt = {
157
+ let lastCorner = ref(None)
158
+ Array.forEach(leftEdgeScan, ((pos, cell)) => {
159
+ switch cell {
160
+ | Corner if !Position.equals(pos, topLeft) => lastCorner := Some(pos)
161
+ | _ => ()
162
+ }
163
+ })
164
+ lastCorner.contents
165
+ }
166
+
167
+ switch bottomLeftOpt {
168
+ | None =>
169
+ Error(
170
+ ErrorTypes.makeSimple(
171
+ ErrorTypes.UncloseBox({
172
+ corner: topRight,
173
+ direction: "right",
174
+ }),
175
+ ),
176
+ )
177
+ | Some(bottomLeft) => {
178
+ // Found bottom-left, now scan right to find bottom edge width
179
+ let bottomEdgeScan = Grid.scanRight(grid, bottomLeft, isValidHorizontalChar)
180
+ let bottomRightFromLeft = {
181
+ let lastCorner = ref(None)
182
+ Array.forEach(bottomEdgeScan, ((pos, cell)) => {
183
+ switch cell {
184
+ | Corner if !Position.equals(pos, bottomLeft) => lastCorner := Some(pos)
185
+ | _ => ()
186
+ }
187
+ })
188
+ lastCorner.contents
189
+ }
190
+
191
+ switch bottomRightFromLeft {
192
+ | None =>
193
+ Error(
194
+ ErrorTypes.makeSimple(
195
+ ErrorTypes.UncloseBox({
196
+ corner: bottomLeft,
197
+ direction: "bottom",
198
+ }),
199
+ ),
200
+ )
201
+ | Some(actualBottomRight) => {
202
+ // Check if this is a width mismatch
203
+ let bottomWidth = actualBottomRight.col - bottomLeft.col
204
+ if topWidth !== bottomWidth {
205
+ Error(
206
+ ErrorTypes.makeSimple(
207
+ ErrorTypes.MismatchedWidth({
208
+ topLeft: topLeft,
209
+ topWidth: topWidth,
210
+ bottomWidth: bottomWidth,
211
+ }),
212
+ ),
213
+ )
214
+ } else {
215
+ // Widths match but right edge still failed - unclosed box
216
+ Error(
217
+ ErrorTypes.makeSimple(
218
+ ErrorTypes.UncloseBox({
219
+ corner: topRight,
220
+ direction: "right",
221
+ }),
222
+ ),
223
+ )
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ | Some(bottomRight) => {
231
+ // Step 5: Scan left from bottom-right to find bottom-left corner
232
+ let bottomEdgeScan = Grid.scanLeft(grid, bottomRight, isValidHorizontalChar)
233
+
234
+ let bottomLeftOpt = {
235
+ let lastCorner = ref(None)
236
+ Array.forEach(bottomEdgeScan, ((pos, cell)) => {
237
+ switch cell {
238
+ | Corner if !Position.equals(pos, bottomRight) => lastCorner := Some(pos)
239
+ | _ => ()
240
+ }
241
+ })
242
+ lastCorner.contents
243
+ }
244
+
245
+ switch bottomLeftOpt {
246
+ | None =>
247
+ Error(
248
+ ErrorTypes.makeSimple(
249
+ ErrorTypes.UncloseBox({
250
+ corner: bottomRight,
251
+ direction: "bottom",
252
+ }),
253
+ ),
254
+ )
255
+ | Some(bottomLeft) => {
256
+ // Step 6: Validate bottom width matches top width
257
+ let bottomWidth = bottomRight.col - bottomLeft.col
258
+
259
+ if topWidth !== bottomWidth {
260
+ Error(
261
+ ErrorTypes.makeSimple(
262
+ ErrorTypes.MismatchedWidth({
263
+ topLeft: topLeft,
264
+ topWidth: topWidth,
265
+ bottomWidth: bottomWidth,
266
+ }),
267
+ ),
268
+ )
269
+ } else {
270
+ // Step 7: Scan up from bottom-left to verify closure
271
+ let leftEdgeScan = Grid.scanUp(grid, bottomLeft, isValidVerticalChar)
272
+
273
+ // Check if we reach the starting position
274
+ let reachesStart = Array.some(leftEdgeScan, ((pos, _)) =>
275
+ Position.equals(pos, topLeft)
276
+ )
277
+
278
+ if !reachesStart {
279
+ Error(
280
+ ErrorTypes.makeSimple(
281
+ ErrorTypes.UncloseBox({
282
+ corner: bottomLeft,
283
+ direction: "left",
284
+ }),
285
+ ),
286
+ )
287
+ } else {
288
+ // Step 8: Validate vertical pipe alignment
289
+ // Check that all vertical pipes on the left edge are aligned with topLeft.col
290
+ let leftAlignmentError = ref(None)
291
+ Array.forEach(leftEdgeScan, ((pos, cell)) => {
292
+ switch cell {
293
+ | VLine =>
294
+ if pos.col !== topLeft.col {
295
+ leftAlignmentError :=
296
+ Some(
297
+ ErrorTypes.makeSimple(
298
+ ErrorTypes.MisalignedPipe({
299
+ position: pos,
300
+ expectedCol: topLeft.col,
301
+ actualCol: pos.col,
302
+ }),
303
+ ),
304
+ )
305
+ }
306
+ | _ => ()
307
+ }
308
+ })
309
+
310
+ switch leftAlignmentError.contents {
311
+ | Some(err) => Error(err)
312
+ | None => {
313
+ // Check right edge alignment
314
+ let rightAlignmentError = ref(None)
315
+ Array.forEach(rightEdgeScan, ((pos, cell)) => {
316
+ switch cell {
317
+ | VLine =>
318
+ if pos.col !== topRight.col {
319
+ rightAlignmentError :=
320
+ Some(
321
+ ErrorTypes.makeSimple(
322
+ ErrorTypes.MisalignedPipe({
323
+ position: pos,
324
+ expectedCol: topRight.col,
325
+ actualCol: pos.col,
326
+ }),
327
+ ),
328
+ )
329
+ }
330
+ | _ => ()
331
+ }
332
+ })
333
+
334
+ switch rightAlignmentError.contents {
335
+ | Some(err) => Error(err)
336
+ | None => {
337
+ // Step 9: Create Bounds and return box
338
+ let bounds = {
339
+ Bounds.top: topLeft.row,
340
+ left: topLeft.col,
341
+ bottom: bottomLeft.row,
342
+ right: topRight.col,
343
+ }
344
+
345
+ Ok({
346
+ name: boxName,
347
+ bounds: bounds,
348
+ children: [],
349
+ })
350
+ }
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+ } // Close else block for isDividerOnlyEdge
362
+ }
363
+ }
364
+ | _ =>
365
+ Error(
366
+ ErrorTypes.makeSimple(
367
+ ErrorTypes.InvalidElement({
368
+ content: "Expected '+' corner at start position",
369
+ position: topLeft,
370
+ }),
371
+ ),
372
+ )
373
+ }
374
+ }