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,563 @@
1
+ // ShapeDetector_test.res
2
+ // Integration tests for ShapeDetector module
3
+ //
4
+ // Tests shape detection including:
5
+ // - Single boxes
6
+ // - Nested boxes (2-3 levels)
7
+ // - Sibling boxes
8
+ // - Dividers
9
+ // - Box names
10
+ // - Malformed boxes (error cases)
11
+ //
12
+ // Requirements: REQ-25 (Testability - Unit Test Coverage ≥90%)
13
+
14
+ open Vitest
15
+ open Types
16
+
17
+ // ============================================================================
18
+ // Test Helpers
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Create a grid from a multi-line string wireframe.
23
+ * Handles newline splitting and normalization.
24
+ */
25
+ let makeGrid = (wireframe: string): Grid.t => {
26
+ wireframe
27
+ ->String.trim
28
+ ->String.split("\n")
29
+ ->Grid.fromLines
30
+ }
31
+
32
+ /**
33
+ * Helper to verify result is Ok and contains expected number of boxes.
34
+ */
35
+ let expectOkWithBoxCount = (
36
+ t: Vitest_Types.testCtx,
37
+ result: ShapeDetector.detectResult,
38
+ expectedCount: int
39
+ ): array<BoxTracer.box> => {
40
+ switch result {
41
+ | Ok(boxes) => {
42
+ t->expect(Array.length(boxes))->Expect.toBe(expectedCount)
43
+ boxes
44
+ }
45
+ | Error(errors) => {
46
+ Console.error("Expected Ok but got Error:")
47
+ errors->Array.forEach(err => {
48
+ Console.error(ErrorTypes.getCodeName(err.code))
49
+ })
50
+ t->expect(true)->Expect.toBe(false) // fail: Expected Ok with boxes, got Error
51
+ []
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Helper to verify result is Error and contains expected number of errors.
58
+ */
59
+ let expectErrorWithCount = (
60
+ t: Vitest_Types.testCtx,
61
+ result: ShapeDetector.detectResult,
62
+ minErrorCount: int
63
+ ): array<ErrorTypes.t> => {
64
+ switch result {
65
+ | Error(errors) => {
66
+ t->expect(Array.length(errors))->Expect.Int.toBeGreaterThanOrEqual(minErrorCount)
67
+ errors
68
+ }
69
+ | Ok(boxes) => {
70
+ t->expect(true)->Expect.toBe(false) // fail: Expected Error but got Ok
71
+ []
72
+ }
73
+ }
74
+ }
75
+
76
+ // ============================================================================
77
+ // SD-01: Single Box Detection
78
+ // ============================================================================
79
+
80
+ describe("ShapeDetector - Single Box", () => {
81
+ test("SD-01: detects a simple box", t => {
82
+ // Create a simple box wireframe
83
+ let wireframe = `
84
+ +------+
85
+ | |
86
+ +------+
87
+ `
88
+ let grid = makeGrid(wireframe)
89
+ let result = ShapeDetector.detect(grid)
90
+
91
+ let boxes = expectOkWithBoxCount(t, result, 1)
92
+ let box = boxes[0]->Option.getExn
93
+
94
+ // Verify no children and no name
95
+ t->expect(Array.length(box.children))->Expect.toBe(0)
96
+ t->expect(box.name)->Expect.toBe(None)
97
+ })
98
+
99
+ test("SD-01b: handles boxes with different dimensions", t => {
100
+ let wireframe = `
101
+ +----------+
102
+ | |
103
+ | |
104
+ +----------+
105
+ `
106
+ let grid = makeGrid(wireframe)
107
+ let result = ShapeDetector.detect(grid)
108
+
109
+ let _ = expectOkWithBoxCount(t, result, 1)
110
+ })
111
+ })
112
+
113
+ // ============================================================================
114
+ // SD-02-03: Nested Boxes
115
+ // ============================================================================
116
+
117
+ describe("ShapeDetector - Nested Boxes", () => {
118
+ test("SD-02: detects 2-level nested boxes", t => {
119
+ let wireframe = `
120
+ +----------+
121
+ | +----+ |
122
+ | | | |
123
+ | +----+ |
124
+ +----------+
125
+ `
126
+ let grid = makeGrid(wireframe)
127
+ let result = ShapeDetector.detect(grid)
128
+
129
+ let boxes = expectOkWithBoxCount(t, result, 1)
130
+ let outer = boxes[0]->Option.getExn
131
+
132
+ // Outer should have 1 child
133
+ t->expect(Array.length(outer.children))->Expect.toBe(1)
134
+
135
+ let inner = outer.children[0]->Option.getExn
136
+
137
+ // Verify containment
138
+ t->expect(Bounds.contains(outer.bounds, inner.bounds))->Expect.toBe(true)
139
+ })
140
+
141
+ test("SD-03: detects 3-level nested boxes", t => {
142
+ let wireframe = `
143
+ +--------------+
144
+ | +----------+ |
145
+ | | +------+ | |
146
+ | | | | | |
147
+ | | +------+ | |
148
+ | +----------+ |
149
+ +--------------+
150
+ `
151
+ let grid = makeGrid(wireframe)
152
+ let result = ShapeDetector.detect(grid)
153
+
154
+ let boxes = expectOkWithBoxCount(t, result, 1)
155
+ let outer = boxes[0]->Option.getExn
156
+
157
+ t->expect(Array.length(outer.children))->Expect.toBe(1)
158
+ let middle = outer.children[0]->Option.getExn
159
+
160
+ t->expect(Array.length(middle.children))->Expect.toBe(1)
161
+ let inner = middle.children[0]->Option.getExn
162
+
163
+ t->expect(Array.length(inner.children))->Expect.toBe(0)
164
+
165
+ // Verify total count
166
+ let total = ShapeDetector.countBoxes(boxes)
167
+ t->expect(total)->Expect.toBe(3)
168
+ })
169
+ })
170
+
171
+ // ============================================================================
172
+ // SD-04: Sibling Boxes
173
+ // ============================================================================
174
+
175
+ describe("ShapeDetector - Sibling Boxes", () => {
176
+ test("SD-04: detects sibling boxes at same level", t => {
177
+ let wireframe = `
178
+ +-----+ +-----+
179
+ | | | |
180
+ +-----+ +-----+
181
+ `
182
+ let grid = makeGrid(wireframe)
183
+ let result = ShapeDetector.detect(grid)
184
+
185
+ let boxes = expectOkWithBoxCount(t, result, 2)
186
+
187
+ let box1 = boxes[0]->Option.getExn
188
+ let box2 = boxes[1]->Option.getExn
189
+
190
+ // Neither contains the other
191
+ t->expect(Bounds.contains(box1.bounds, box2.bounds))->Expect.toBe(false)
192
+ t->expect(Bounds.contains(box2.bounds, box1.bounds))->Expect.toBe(false)
193
+
194
+ // No overlap
195
+ t->expect(Bounds.overlaps(box1.bounds, box2.bounds))->Expect.toBe(false)
196
+
197
+ // Both have no children
198
+ t->expect(Array.length(box1.children))->Expect.toBe(0)
199
+ t->expect(Array.length(box2.children))->Expect.toBe(0)
200
+ })
201
+ })
202
+
203
+ // ============================================================================
204
+ // SD-05-06: Dividers
205
+ // ============================================================================
206
+
207
+ describe("ShapeDetector - Dividers", () => {
208
+ test("SD-05: handles box with single divider", t => {
209
+ let wireframe = `
210
+ +-----+
211
+ | |
212
+ +=====+
213
+ | |
214
+ +-----+
215
+ `
216
+ let grid = makeGrid(wireframe)
217
+ let result = ShapeDetector.detect(grid)
218
+
219
+ let _ = expectOkWithBoxCount(t, result, 1)
220
+ })
221
+
222
+ test("SD-06: handles box with multiple dividers", t => {
223
+ let wireframe = `
224
+ +-----+
225
+ | |
226
+ +=====+
227
+ | |
228
+ +=====+
229
+ | |
230
+ +-----+
231
+ `
232
+ let grid = makeGrid(wireframe)
233
+ let result = ShapeDetector.detect(grid)
234
+
235
+ let _ = expectOkWithBoxCount(t, result, 1)
236
+ })
237
+ })
238
+
239
+ // ============================================================================
240
+ // SD-07-08: Box Names
241
+ // ============================================================================
242
+
243
+ describe("ShapeDetector - Box Names", () => {
244
+ test("SD-07: extracts box name from top border", t => {
245
+ let wireframe = `
246
+ +--Login--+
247
+ | |
248
+ +---------+
249
+ `
250
+ let grid = makeGrid(wireframe)
251
+ let result = ShapeDetector.detect(grid)
252
+
253
+ let boxes = expectOkWithBoxCount(t, result, 1)
254
+ let box = boxes[0]->Option.getExn
255
+
256
+ t->expect(box.name)->Expect.toEqual(Some("Login"))
257
+ })
258
+
259
+ test("SD-08: handles multiple named boxes", t => {
260
+ let wireframe = `
261
+ +--Header--+
262
+ | |
263
+ +----------+
264
+
265
+ +--Content-+
266
+ | |
267
+ +----------+
268
+ `
269
+ let grid = makeGrid(wireframe)
270
+ let result = ShapeDetector.detect(grid)
271
+
272
+ let boxes = expectOkWithBoxCount(t, result, 2)
273
+
274
+ // Find boxes by name
275
+ let hasHeader = boxes->Array.some(box => {
276
+ switch box.name {
277
+ | Some("Header") => true
278
+ | _ => false
279
+ }
280
+ })
281
+
282
+ let hasContent = boxes->Array.some(box => {
283
+ switch box.name {
284
+ | Some("Content") => true
285
+ | _ => false
286
+ }
287
+ })
288
+
289
+ t->expect(hasHeader)->Expect.toBe(true)
290
+ t->expect(hasContent)->Expect.toBe(true)
291
+ })
292
+ })
293
+
294
+ // ============================================================================
295
+ // SD-09-13: Error Cases
296
+ // ============================================================================
297
+
298
+ describe("ShapeDetector - Error Cases", () => {
299
+ test("SD-09: detects unclosed box - missing top corner", t => {
300
+ let wireframe = `
301
+ +-----
302
+ | |
303
+ +-----+
304
+ `
305
+ let grid = makeGrid(wireframe)
306
+ let result = ShapeDetector.detect(grid)
307
+
308
+ let errors = expectErrorWithCount(t, result, 1)
309
+
310
+ let hasUncloseError = errors->Array.some(err => {
311
+ switch err.code {
312
+ | UncloseBox({direction: "top"}) => true
313
+ | _ => false
314
+ }
315
+ })
316
+
317
+ t->expect(hasUncloseError)->Expect.toBe(true)
318
+ })
319
+
320
+ test("SD-10: detects unclosed box - missing bottom corner", t => {
321
+ let wireframe = `
322
+ +-----+
323
+ | |
324
+ +-----
325
+ `
326
+ let grid = makeGrid(wireframe)
327
+ let result = ShapeDetector.detect(grid)
328
+
329
+ let errors = expectErrorWithCount(t, result, 1)
330
+
331
+ let hasUncloseError = errors->Array.some(err => {
332
+ switch err.code {
333
+ | UncloseBox({direction: "bottom"}) => true
334
+ | _ => false
335
+ }
336
+ })
337
+
338
+ t->expect(hasUncloseError)->Expect.toBe(true)
339
+ })
340
+
341
+ test("SD-11: detects mismatched width", t => {
342
+ let wireframe = `
343
+ +-----+
344
+ | |
345
+ +-------+
346
+ `
347
+ let grid = makeGrid(wireframe)
348
+ let result = ShapeDetector.detect(grid)
349
+
350
+ let errors = expectErrorWithCount(t, result, 1)
351
+
352
+ let hasMismatchError = errors->Array.some(err => {
353
+ switch err.code {
354
+ | MismatchedWidth(_) => true
355
+ | _ => false
356
+ }
357
+ })
358
+
359
+ t->expect(hasMismatchError)->Expect.toBe(true)
360
+ })
361
+
362
+ test("SD-12: detects unclosed box - misaligned left edge", t => {
363
+ // This wireframe has a space at row 2 col 0 where a pipe should be
364
+ // This causes an unclosed box error on the left edge
365
+ let wireframe = `
366
+ +-----+
367
+ | |
368
+ | |
369
+ +-----+
370
+ `
371
+ let grid = makeGrid(wireframe)
372
+ let result = ShapeDetector.detect(grid)
373
+
374
+ let errors = expectErrorWithCount(t, result, 1)
375
+
376
+ let hasUncloseError = errors->Array.some(err => {
377
+ switch err.code {
378
+ | UncloseBox({direction: "left"}) => true
379
+ | _ => false
380
+ }
381
+ })
382
+
383
+ t->expect(hasUncloseError)->Expect.toBe(true)
384
+ })
385
+ })
386
+
387
+ // ============================================================================
388
+ // SD-14: Edge Cases
389
+ // ============================================================================
390
+
391
+ describe("ShapeDetector - Edge Cases", () => {
392
+ test("SD-14: handles empty grid with no boxes", t => {
393
+ let wireframe = `
394
+ abc
395
+ def
396
+ ghi
397
+ `
398
+ let grid = makeGrid(wireframe)
399
+ let result = ShapeDetector.detect(grid)
400
+
401
+ // Should return Ok with empty array
402
+ let _ = expectOkWithBoxCount(t, result, 0)
403
+ })
404
+ })
405
+
406
+ // ============================================================================
407
+ // SD-15: Complex Integration Test
408
+ // ============================================================================
409
+
410
+ describe("ShapeDetector - Complex Integration", () => {
411
+ test("SD-15: handles nested structure with sibling inner boxes", t => {
412
+ // Simplified test: outer box with two sibling inner boxes
413
+ let wireframe = `
414
+ +----------+
415
+ | +--+ +--+|
416
+ | | | | ||
417
+ | +--+ +--+|
418
+ +----------+
419
+ `
420
+ let grid = makeGrid(wireframe)
421
+ let result = ShapeDetector.detect(grid)
422
+
423
+ let boxes = expectOkWithBoxCount(t, result, 1)
424
+ let outer = boxes[0]->Option.getExn
425
+
426
+ // Outer box should have 2 sibling children
427
+ t->expect(Array.length(outer.children))->Expect.toBe(2)
428
+
429
+ // Verify total count: outer + 2 inner = 3
430
+ let total = ShapeDetector.countBoxes(boxes)
431
+ t->expect(total)->Expect.toBe(3)
432
+ })
433
+ })
434
+
435
+ // ============================================================================
436
+ // SD-16-17: Deduplication and Error Recovery
437
+ // ============================================================================
438
+
439
+ describe("ShapeDetector - Deduplication & Error Recovery", () => {
440
+ test("SD-16: deduplicates boxes traced from multiple corners", t => {
441
+ let wireframe = `
442
+ +-----+
443
+ | |
444
+ +-----+
445
+ `
446
+ let grid = makeGrid(wireframe)
447
+
448
+ // Verify 4 corners exist
449
+ t->expect(Array.length(grid.cornerIndex))->Expect.toBe(4)
450
+
451
+ let result = ShapeDetector.detect(grid)
452
+
453
+ // Only 1 unique box
454
+ let _ = expectOkWithBoxCount(t, result, 1)
455
+ })
456
+
457
+ test("SD-17: returns valid boxes when some traces succeed (REQ-28)", t => {
458
+ // This wireframe has one valid box and two malformed ones
459
+ // Current implementation returns Ok with valid boxes when some succeed
460
+ let wireframe = `
461
+ +---+ +-----
462
+ | | | |
463
+ +---+ +-----+
464
+
465
+ +-----+
466
+ | |
467
+ +-------+
468
+ `
469
+ let grid = makeGrid(wireframe)
470
+ let result = ShapeDetector.detect(grid)
471
+
472
+ // Should have at least 1 valid box (the left one: +---+)
473
+ let boxes = expectOkWithBoxCount(t, result, 1)
474
+ t->expect(Array.length(boxes))->Expect.Int.toBeGreaterThanOrEqual(1)
475
+ })
476
+ })
477
+
478
+ // ============================================================================
479
+ // SD-18: Helper Functions
480
+ // ============================================================================
481
+
482
+ describe("ShapeDetector - Helper Functions", () => {
483
+ test("SD-18a: countBoxes counts all boxes including nested", t => {
484
+ let wireframe = `
485
+ +----------+
486
+ | +------+ |
487
+ | | +--+ | |
488
+ | | | | | |
489
+ | | +--+ | |
490
+ | +------+ |
491
+ +----------+
492
+ `
493
+ let grid = makeGrid(wireframe)
494
+ let result = ShapeDetector.detect(grid)
495
+
496
+ let boxes = expectOkWithBoxCount(t, result, 1)
497
+ let total = ShapeDetector.countBoxes(boxes)
498
+
499
+ t->expect(total)->Expect.toBe(3)
500
+ })
501
+
502
+ test("SD-18b: flattenBoxes returns all boxes in flat array", t => {
503
+ let wireframe = `
504
+ +----------+
505
+ | +------+ |
506
+ | | +--+ | |
507
+ | | | | | |
508
+ | | +--+ | |
509
+ | +------+ |
510
+ +----------+
511
+ `
512
+ let grid = makeGrid(wireframe)
513
+ let result = ShapeDetector.detect(grid)
514
+
515
+ let boxes = expectOkWithBoxCount(t, result, 1)
516
+ let flat = ShapeDetector.flattenBoxes(boxes)
517
+
518
+ t->expect(Array.length(flat))->Expect.toBe(3)
519
+ })
520
+
521
+ test("SD-18c: getStats returns formatted statistics for Ok result", t => {
522
+ let wireframe = `
523
+ +-----+
524
+ | |
525
+ +-----+
526
+ `
527
+ let grid = makeGrid(wireframe)
528
+ let result = ShapeDetector.detect(grid)
529
+
530
+ let stats = ShapeDetector.getStats(result)
531
+
532
+ t->expect(stats->String.includes("Success"))->Expect.toBe(true)
533
+ t->expect(stats->String.includes("Root boxes: 1"))->Expect.toBe(true)
534
+ })
535
+
536
+ test("SD-18d: getStats returns formatted statistics for Error result", t => {
537
+ let wireframe = `
538
+ +-----
539
+ | |
540
+ +-----+
541
+ `
542
+ let grid = makeGrid(wireframe)
543
+ let result = ShapeDetector.detect(grid)
544
+
545
+ let stats = ShapeDetector.getStats(result)
546
+
547
+ t->expect(stats->String.includes("Failed"))->Expect.toBe(true)
548
+ })
549
+
550
+ test("SD-18e: countBoxes handles empty array", t => {
551
+ let emptyBoxes: array<BoxTracer.box> = []
552
+ let count = ShapeDetector.countBoxes(emptyBoxes)
553
+
554
+ t->expect(count)->Expect.toBe(0)
555
+ })
556
+
557
+ test("SD-18f: flattenBoxes handles empty array", t => {
558
+ let emptyBoxes: array<BoxTracer.box> = []
559
+ let flat = ShapeDetector.flattenBoxes(emptyBoxes)
560
+
561
+ t->expect(Array.length(flat))->Expect.toBe(0)
562
+ })
563
+ })
@@ -0,0 +1,106 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Grid from "../Core/Grid.mjs";
4
+ import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
5
+
6
+ function cellCharToString(cell) {
7
+ if (typeof cell === "object") {
8
+ return cell._0;
9
+ }
10
+ switch (cell) {
11
+ case "Corner" :
12
+ return "+";
13
+ case "HLine" :
14
+ return "-";
15
+ case "VLine" :
16
+ return "|";
17
+ case "Divider" :
18
+ return "=";
19
+ case "Space" :
20
+ return " ";
21
+ }
22
+ }
23
+
24
+ function buildCodeSnippet(grid, position, radius) {
25
+ let startRow = Math.max(0, position.row - radius | 0);
26
+ let endRow = Math.min(grid.height - 1 | 0, position.row + radius | 0);
27
+ let padStart = (str, targetLength, padChar) => {
28
+ let currentLength = str.length;
29
+ if (currentLength >= targetLength) {
30
+ return str;
31
+ }
32
+ let padLength = targetLength - currentLength | 0;
33
+ let padding = padChar.repeat(padLength);
34
+ return padding + str;
35
+ };
36
+ let lines = [];
37
+ for (let row = startRow; row <= endRow; ++row) {
38
+ let lineNum = padStart((row + 1 | 0).toString(), 4, " ");
39
+ let prefix = row === position.row ? " → " : " ";
40
+ let chars = Grid.getLine(grid, row);
41
+ if (chars !== undefined) {
42
+ let lineText = Belt_Array.map(chars, cellCharToString).join("");
43
+ let formattedLine = prefix + lineNum + ` │ ` + lineText;
44
+ lines.push(formattedLine);
45
+ if (row === position.row) {
46
+ let spacingBeforePointer = 10 + position.col | 0;
47
+ let pointer = " ".repeat(spacingBeforePointer) + "^";
48
+ lines.push(` │ ` + pointer);
49
+ }
50
+ } else {
51
+ let formattedLine$1 = prefix + lineNum + ` │ `;
52
+ lines.push(formattedLine$1);
53
+ }
54
+ }
55
+ return lines.join("\n");
56
+ }
57
+
58
+ function make(grid, position, radiusOpt) {
59
+ let radius = radiusOpt !== undefined ? radiusOpt : 2;
60
+ let startRow = Math.max(0, position.row - radius | 0);
61
+ let endRow = Math.min(grid.height - 1 | 0, position.row + radius | 0);
62
+ let linesBefore = position.row - startRow | 0;
63
+ let linesAfter = endRow - position.row | 0;
64
+ let snippet = buildCodeSnippet(grid, position, radius);
65
+ return {
66
+ codeSnippet: snippet,
67
+ linesBefore: linesBefore,
68
+ linesAfter: linesAfter
69
+ };
70
+ }
71
+
72
+ function empty() {
73
+ return {
74
+ codeSnippet: undefined,
75
+ linesBefore: 0,
76
+ linesAfter: 0
77
+ };
78
+ }
79
+
80
+ function getSnippet(ctx) {
81
+ let snippet = ctx.codeSnippet;
82
+ if (snippet !== undefined) {
83
+ return snippet;
84
+ } else {
85
+ return "";
86
+ }
87
+ }
88
+
89
+ function hasSnippet(ctx) {
90
+ return ctx.codeSnippet !== undefined;
91
+ }
92
+
93
+ function totalLines(ctx) {
94
+ return (ctx.linesBefore + 1 | 0) + ctx.linesAfter | 0;
95
+ }
96
+
97
+ export {
98
+ cellCharToString,
99
+ buildCodeSnippet,
100
+ make,
101
+ empty,
102
+ getSnippet,
103
+ hasSnippet,
104
+ totalLines,
105
+ }
106
+ /* Grid Not a pure module */