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,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
|
+
}
|