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,61 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ function make(top, left, bottom, right) {
5
+ if (top < bottom && left < right) {
6
+ return {
7
+ top: top,
8
+ left: left,
9
+ bottom: bottom,
10
+ right: right
11
+ };
12
+ }
13
+ }
14
+
15
+ function width(bounds) {
16
+ return bounds.right - bounds.left | 0;
17
+ }
18
+
19
+ function height(bounds) {
20
+ return bounds.bottom - bounds.top | 0;
21
+ }
22
+
23
+ function area(bounds) {
24
+ return width(bounds) * height(bounds) | 0;
25
+ }
26
+
27
+ function contains(outer, inner) {
28
+ if (outer.top < inner.top && outer.left < inner.left && outer.bottom > inner.bottom) {
29
+ return outer.right > inner.right;
30
+ } else {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ function overlaps(a, b) {
36
+ return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom);
37
+ }
38
+
39
+ function toString(bounds) {
40
+ return `Bounds{top: ` + bounds.top.toString() + `, left: ` + bounds.left.toString() + `, bottom: ` + bounds.bottom.toString() + `, right: ` + bounds.right.toString() + `}`;
41
+ }
42
+
43
+ function equals(a, b) {
44
+ if (a.top === b.top && a.left === b.left && a.bottom === b.bottom) {
45
+ return a.right === b.right;
46
+ } else {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ export {
52
+ make,
53
+ width,
54
+ height,
55
+ area,
56
+ contains,
57
+ overlaps,
58
+ toString,
59
+ equals,
60
+ }
61
+ /* No side effect */
@@ -0,0 +1,65 @@
1
+ // Bounds.res
2
+ // Bounding box representation for rectangular regions
3
+
4
+ type t = {
5
+ top: int,
6
+ left: int,
7
+ bottom: int,
8
+ right: int,
9
+ }
10
+
11
+ // Create a bounding box with validation
12
+ // Validates that top < bottom and left < right
13
+ let make = (~top: int, ~left: int, ~bottom: int, ~right: int): option<t> => {
14
+ if top < bottom && left < right {
15
+ Some({top, left, bottom, right})
16
+ } else {
17
+ None
18
+ }
19
+ }
20
+
21
+ // Calculate width of the bounding box
22
+ let width = (bounds: t): int => {
23
+ bounds.right - bounds.left
24
+ }
25
+
26
+ // Calculate height of the bounding box
27
+ let height = (bounds: t): int => {
28
+ bounds.bottom - bounds.top
29
+ }
30
+
31
+ // Calculate area of the bounding box
32
+ let area = (bounds: t): int => {
33
+ width(bounds) * height(bounds)
34
+ }
35
+
36
+ // Check if outer completely contains inner
37
+ // Returns true if outer's bounds completely enclose inner's bounds
38
+ let contains = (outer: t, inner: t): bool => {
39
+ outer.top < inner.top &&
40
+ outer.left < inner.left &&
41
+ outer.bottom > inner.bottom &&
42
+ outer.right > inner.right
43
+ }
44
+
45
+ // Check if two bounding boxes overlap (partially or completely)
46
+ // Returns true if there is any intersection between the two boxes
47
+ let overlaps = (a: t, b: t): bool => {
48
+ // No overlap if:
49
+ // - a is completely to the left of b (a.right <= b.left)
50
+ // - a is completely to the right of b (a.left >= b.right)
51
+ // - a is completely above b (a.bottom <= b.top)
52
+ // - a is completely below b (a.top >= b.bottom)
53
+ // Overlap exists if none of the above conditions are true
54
+ !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom)
55
+ }
56
+
57
+ // Convert bounds to string for debugging
58
+ let toString = (bounds: t): string => {
59
+ `Bounds{top: ${Int.toString(bounds.top)}, left: ${Int.toString(bounds.left)}, bottom: ${Int.toString(bounds.bottom)}, right: ${Int.toString(bounds.right)}}`
60
+ }
61
+
62
+ // Check if two bounds are equal
63
+ let equals = (a: t, b: t): bool => {
64
+ a.top == b.top && a.left == b.left && a.bottom == b.bottom && a.right == b.right
65
+ }
@@ -0,0 +1,268 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Types from "./Types.mjs";
4
+ import * as Core__Array from "@rescript/core/src/Core__Array.mjs";
5
+ import * as Core__Option from "@rescript/core/src/Core__Option.mjs";
6
+
7
+ function charToCellChar(char) {
8
+ switch (char) {
9
+ case " " :
10
+ return "Space";
11
+ case "+" :
12
+ return "Corner";
13
+ case "-" :
14
+ return "HLine";
15
+ case "=" :
16
+ return "Divider";
17
+ case "|" :
18
+ return "VLine";
19
+ default:
20
+ return {
21
+ TAG: "Char",
22
+ _0: char
23
+ };
24
+ }
25
+ }
26
+
27
+ function cellCharToString(cell) {
28
+ if (typeof cell === "object") {
29
+ return cell._0;
30
+ }
31
+ switch (cell) {
32
+ case "Corner" :
33
+ return "+";
34
+ case "HLine" :
35
+ return "-";
36
+ case "VLine" :
37
+ return "|";
38
+ case "Divider" :
39
+ return "=";
40
+ case "Space" :
41
+ return " ";
42
+ }
43
+ }
44
+
45
+ function fromLines(lines) {
46
+ let maxWidth = Core__Array.reduce(lines, 0, (acc, line) => {
47
+ let len = line.length;
48
+ if (acc > len) {
49
+ return acc;
50
+ } else {
51
+ return len;
52
+ }
53
+ });
54
+ let cells = lines.map(line => {
55
+ let chars = line.split("");
56
+ let cellRow = chars.map(charToCellChar);
57
+ let paddingNeeded = maxWidth - cellRow.length | 0;
58
+ if (paddingNeeded <= 0) {
59
+ return cellRow;
60
+ }
61
+ let padding = Core__Array.make(paddingNeeded, "Space");
62
+ return cellRow.concat(padding);
63
+ });
64
+ let cornerIndex = [];
65
+ let hLineIndex = [];
66
+ let vLineIndex = [];
67
+ let dividerIndex = [];
68
+ cells.forEach((row, rowIdx) => {
69
+ row.forEach((cell, colIdx) => {
70
+ let pos = Types.Position.make(rowIdx, colIdx);
71
+ if (typeof cell === "object") {
72
+ return;
73
+ }
74
+ switch (cell) {
75
+ case "Corner" :
76
+ cornerIndex.push(pos);
77
+ return;
78
+ case "HLine" :
79
+ hLineIndex.push(pos);
80
+ return;
81
+ case "VLine" :
82
+ vLineIndex.push(pos);
83
+ return;
84
+ case "Divider" :
85
+ dividerIndex.push(pos);
86
+ return;
87
+ case "Space" :
88
+ return;
89
+ }
90
+ });
91
+ });
92
+ return {
93
+ cells: cells,
94
+ width: maxWidth,
95
+ height: cells.length,
96
+ cornerIndex: cornerIndex,
97
+ hLineIndex: hLineIndex,
98
+ vLineIndex: vLineIndex,
99
+ dividerIndex: dividerIndex
100
+ };
101
+ }
102
+
103
+ function get(grid, pos) {
104
+ if (pos.row >= 0 && pos.row < grid.height && pos.col >= 0 && pos.col < grid.width) {
105
+ return Core__Option.flatMap(grid.cells[pos.row], row => row[pos.col]);
106
+ }
107
+ }
108
+
109
+ function getLine(grid, row) {
110
+ if (row >= 0 && row < grid.height) {
111
+ return grid.cells[row];
112
+ }
113
+ }
114
+
115
+ function getRange(grid, row, startCol, endCol) {
116
+ return Core__Option.map(getLine(grid, row), line => {
117
+ let start = startCol < 0 ? 0 : startCol;
118
+ let end = endCol >= line.length ? line.length - 1 | 0 : endCol;
119
+ return line.slice(start, end + 1 | 0);
120
+ });
121
+ }
122
+
123
+ function scanRight(grid, pos, predicate) {
124
+ let results = [];
125
+ let currentPos = pos;
126
+ let $$continue = true;
127
+ while (currentPos.col < grid.width && $$continue) {
128
+ let cell = get(grid, currentPos);
129
+ if (cell !== undefined && predicate(cell)) {
130
+ results.push([
131
+ currentPos,
132
+ cell
133
+ ]);
134
+ currentPos = Types.Position.right(currentPos, undefined);
135
+ } else {
136
+ $$continue = false;
137
+ }
138
+ };
139
+ return results;
140
+ }
141
+
142
+ function scanDown(grid, pos, predicate) {
143
+ let results = [];
144
+ let currentPos = pos;
145
+ let $$continue = true;
146
+ while (currentPos.row < grid.height && $$continue) {
147
+ let cell = get(grid, currentPos);
148
+ if (cell !== undefined && predicate(cell)) {
149
+ results.push([
150
+ currentPos,
151
+ cell
152
+ ]);
153
+ currentPos = Types.Position.down(currentPos, undefined);
154
+ } else {
155
+ $$continue = false;
156
+ }
157
+ };
158
+ return results;
159
+ }
160
+
161
+ function scanLeft(grid, pos, predicate) {
162
+ let results = [];
163
+ let currentPos = pos;
164
+ let $$continue = true;
165
+ while (currentPos.col >= 0 && $$continue) {
166
+ let cell = get(grid, currentPos);
167
+ if (cell !== undefined && predicate(cell)) {
168
+ results.push([
169
+ currentPos,
170
+ cell
171
+ ]);
172
+ currentPos = Types.Position.left(currentPos, undefined);
173
+ } else {
174
+ $$continue = false;
175
+ }
176
+ };
177
+ return results;
178
+ }
179
+
180
+ function scanUp(grid, pos, predicate) {
181
+ let results = [];
182
+ let currentPos = pos;
183
+ let $$continue = true;
184
+ while (currentPos.row >= 0 && $$continue) {
185
+ let cell = get(grid, currentPos);
186
+ if (cell !== undefined && predicate(cell)) {
187
+ results.push([
188
+ currentPos,
189
+ cell
190
+ ]);
191
+ currentPos = Types.Position.up(currentPos, undefined);
192
+ } else {
193
+ $$continue = false;
194
+ }
195
+ };
196
+ return results;
197
+ }
198
+
199
+ function findAll(grid, cellType) {
200
+ if (typeof cellType !== "object") {
201
+ switch (cellType) {
202
+ case "Corner" :
203
+ return grid.cornerIndex;
204
+ case "HLine" :
205
+ return grid.hLineIndex;
206
+ case "VLine" :
207
+ return grid.vLineIndex;
208
+ case "Divider" :
209
+ return grid.dividerIndex;
210
+ case "Space" :
211
+ break;
212
+ }
213
+ }
214
+ let positions = [];
215
+ grid.cells.forEach((row, rowIdx) => {
216
+ row.forEach((cell, colIdx) => {
217
+ if (typeof cellType === "object") {
218
+ if (typeof cell !== "object" || cellType._0 !== cell._0) {
219
+ return;
220
+ } else {
221
+ positions.push(Types.Position.make(rowIdx, colIdx));
222
+ return;
223
+ }
224
+ }
225
+ if (cellType !== "Space") {
226
+ return;
227
+ }
228
+ if (typeof cell === "object") {
229
+ return;
230
+ }
231
+ if (cell !== "Space") {
232
+ return;
233
+ }
234
+ positions.push(Types.Position.make(rowIdx, colIdx));
235
+ });
236
+ });
237
+ return positions;
238
+ }
239
+
240
+ function findInRange(grid, cellType, bounds) {
241
+ let allPositions = findAll(grid, cellType);
242
+ return allPositions.filter(pos => Types.Position.isWithin(pos, bounds));
243
+ }
244
+
245
+ function isValidPosition(grid, pos) {
246
+ if (pos.row >= 0 && pos.row < grid.height && pos.col >= 0) {
247
+ return pos.col < grid.width;
248
+ } else {
249
+ return false;
250
+ }
251
+ }
252
+
253
+ export {
254
+ charToCellChar,
255
+ cellCharToString,
256
+ fromLines,
257
+ get,
258
+ getLine,
259
+ getRange,
260
+ scanRight,
261
+ scanDown,
262
+ scanLeft,
263
+ scanUp,
264
+ findAll,
265
+ findInRange,
266
+ isValidPosition,
267
+ }
268
+ /* Types Not a pure module */
@@ -0,0 +1,265 @@
1
+ // Grid.res
2
+ // 2D character grid data structure with efficient position-based operations
3
+ // Provides the foundation for spatial parsing of ASCII wireframes
4
+
5
+ open Types
6
+
7
+ type t = {
8
+ cells: array<array<cellChar>>,
9
+ width: int,
10
+ height: int,
11
+ cornerIndex: array<Position.t>,
12
+ hLineIndex: array<Position.t>,
13
+ vLineIndex: array<Position.t>,
14
+ dividerIndex: array<Position.t>,
15
+ }
16
+
17
+ // Helper function to convert a string character to cellChar
18
+ let charToCellChar = (char: string): cellChar => {
19
+ switch char {
20
+ | "+" => Corner
21
+ | "-" => HLine
22
+ | "|" => VLine
23
+ | "=" => Divider
24
+ | " " => Space
25
+ | c => Char(c)
26
+ }
27
+ }
28
+
29
+ // Helper function to convert cellChar back to string
30
+ let cellCharToString = (cell: cellChar): string => {
31
+ switch cell {
32
+ | Corner => "+"
33
+ | HLine => "-"
34
+ | VLine => "|"
35
+ | Divider => "="
36
+ | Space => " "
37
+ | Char(c) => c
38
+ }
39
+ }
40
+
41
+ // Create grid from lines with normalization
42
+ // Normalizes line lengths by padding shorter lines with spaces
43
+ let fromLines = (lines: array<string>): t => {
44
+ // Find maximum line width
45
+ let maxWidth = Array.reduce(lines, 0, (acc, line) => {
46
+ let len = String.length(line)
47
+ acc > len ? acc : len
48
+ })
49
+
50
+ // Build 2D cell array with normalization
51
+ let cells = Array.map(lines, line => {
52
+ let chars = String.split(line, "")
53
+ let cellRow = Array.map(chars, charToCellChar)
54
+
55
+ // Pad to max width with spaces
56
+ let paddingNeeded = maxWidth - Array.length(cellRow)
57
+ if paddingNeeded > 0 {
58
+ let padding = Array.make(~length=paddingNeeded, Space)
59
+ Array.concat(cellRow, padding)
60
+ } else {
61
+ cellRow
62
+ }
63
+ })
64
+
65
+ // Build special character indices for O(1) lookup
66
+ let cornerIndex = []
67
+ let hLineIndex = []
68
+ let vLineIndex = []
69
+ let dividerIndex = []
70
+
71
+ Array.forEachWithIndex(cells, (row, rowIdx) => {
72
+ Array.forEachWithIndex(row, (cell, colIdx) => {
73
+ let pos = Position.make(rowIdx, colIdx)
74
+ switch cell {
75
+ | Corner => {
76
+ let _ = Array.push(cornerIndex, pos)
77
+ }
78
+ | HLine => {
79
+ let _ = Array.push(hLineIndex, pos)
80
+ }
81
+ | VLine => {
82
+ let _ = Array.push(vLineIndex, pos)
83
+ }
84
+ | Divider => {
85
+ let _ = Array.push(dividerIndex, pos)
86
+ }
87
+ | _ => ()
88
+ }
89
+ })
90
+ })
91
+
92
+ {
93
+ cells,
94
+ width: maxWidth,
95
+ height: Array.length(cells),
96
+ cornerIndex,
97
+ hLineIndex,
98
+ vLineIndex,
99
+ dividerIndex,
100
+ }
101
+ }
102
+
103
+ // Get character at position, returns None if out of bounds
104
+ let get = (grid: t, pos: Position.t): option<cellChar> => {
105
+ if pos.row >= 0 && pos.row < grid.height && pos.col >= 0 && pos.col < grid.width {
106
+ Array.get(grid.cells, pos.row)->Option.flatMap(row => Array.get(row, pos.col))
107
+ } else {
108
+ None
109
+ }
110
+ }
111
+
112
+ // Get entire line at row index
113
+ let getLine = (grid: t, row: int): option<array<cellChar>> => {
114
+ if row >= 0 && row < grid.height {
115
+ Array.get(grid.cells, row)
116
+ } else {
117
+ None
118
+ }
119
+ }
120
+
121
+ // Get range of characters from a line (inclusive)
122
+ let getRange = (grid: t, row: int, ~startCol: int, ~endCol: int): option<array<cellChar>> => {
123
+ getLine(grid, row)->Option.map(line => {
124
+ let start = startCol < 0 ? 0 : startCol
125
+ let end = endCol >= Array.length(line) ? Array.length(line) - 1 : endCol
126
+ Array.slice(line, ~start, ~end=end + 1)
127
+ })
128
+ }
129
+
130
+ // Scan right from position while predicate is true
131
+ // Returns array of (position, cellChar) tuples
132
+ let scanRight = (grid: t, pos: Position.t, predicate: cellChar => bool): array<(
133
+ Position.t,
134
+ cellChar,
135
+ )> => {
136
+ let results = []
137
+ let currentPos = ref(pos)
138
+ let continue = ref(true)
139
+
140
+ while currentPos.contents.col < grid.width && continue.contents {
141
+ switch get(grid, currentPos.contents) {
142
+ | Some(cell) =>
143
+ if predicate(cell) {
144
+ let _ = Array.push(results, (currentPos.contents, cell))
145
+ currentPos := Position.right(currentPos.contents)
146
+ } else {
147
+ continue := false
148
+ }
149
+ | None => continue := false
150
+ }
151
+ }
152
+
153
+ results
154
+ }
155
+
156
+ // Scan down from position while predicate is true
157
+ let scanDown = (grid: t, pos: Position.t, predicate: cellChar => bool): array<(
158
+ Position.t,
159
+ cellChar,
160
+ )> => {
161
+ let results = []
162
+ let currentPos = ref(pos)
163
+ let continue = ref(true)
164
+
165
+ while currentPos.contents.row < grid.height && continue.contents {
166
+ switch get(grid, currentPos.contents) {
167
+ | Some(cell) =>
168
+ if predicate(cell) {
169
+ let _ = Array.push(results, (currentPos.contents, cell))
170
+ currentPos := Position.down(currentPos.contents)
171
+ } else {
172
+ continue := false
173
+ }
174
+ | None => continue := false
175
+ }
176
+ }
177
+
178
+ results
179
+ }
180
+
181
+ // Scan left from position while predicate is true
182
+ let scanLeft = (grid: t, pos: Position.t, predicate: cellChar => bool): array<(
183
+ Position.t,
184
+ cellChar,
185
+ )> => {
186
+ let results = []
187
+ let currentPos = ref(pos)
188
+ let continue = ref(true)
189
+
190
+ while currentPos.contents.col >= 0 && continue.contents {
191
+ switch get(grid, currentPos.contents) {
192
+ | Some(cell) =>
193
+ if predicate(cell) {
194
+ let _ = Array.push(results, (currentPos.contents, cell))
195
+ currentPos := Position.left(currentPos.contents)
196
+ } else {
197
+ continue := false
198
+ }
199
+ | None => continue := false
200
+ }
201
+ }
202
+
203
+ results
204
+ }
205
+
206
+ // Scan up from position while predicate is true
207
+ let scanUp = (grid: t, pos: Position.t, predicate: cellChar => bool): array<(Position.t, cellChar)> => {
208
+ let results = []
209
+ let currentPos = ref(pos)
210
+ let continue = ref(true)
211
+
212
+ while currentPos.contents.row >= 0 && continue.contents {
213
+ switch get(grid, currentPos.contents) {
214
+ | Some(cell) =>
215
+ if predicate(cell) {
216
+ let _ = Array.push(results, (currentPos.contents, cell))
217
+ currentPos := Position.up(currentPos.contents)
218
+ } else {
219
+ continue := false
220
+ }
221
+ | None => continue := false
222
+ }
223
+ }
224
+
225
+ results
226
+ }
227
+
228
+ // Find all positions of a specific cellChar type using prebuilt index
229
+ let findAll = (grid: t, cellType: cellChar): array<Position.t> => {
230
+ switch cellType {
231
+ | Corner => grid.cornerIndex
232
+ | HLine => grid.hLineIndex
233
+ | VLine => grid.vLineIndex
234
+ | Divider => grid.dividerIndex
235
+ | Space | Char(_) => {
236
+ // For Space and Char types, we need to scan the entire grid
237
+ let positions = []
238
+ Array.forEachWithIndex(grid.cells, (row, rowIdx) => {
239
+ Array.forEachWithIndex(row, (cell, colIdx) => {
240
+ switch (cellType, cell) {
241
+ | (Space, Space) => {
242
+ let _ = Array.push(positions, Position.make(rowIdx, colIdx))
243
+ }
244
+ | (Char(expected), Char(actual)) if expected === actual => {
245
+ let _ = Array.push(positions, Position.make(rowIdx, colIdx))
246
+ }
247
+ | _ => ()
248
+ }
249
+ })
250
+ })
251
+ positions
252
+ }
253
+ }
254
+ }
255
+
256
+ // Find all positions of a cellChar within bounds
257
+ let findInRange = (grid: t, cellType: cellChar, bounds: Bounds.t): array<Position.t> => {
258
+ let allPositions = findAll(grid, cellType)
259
+ Array.filter(allPositions, pos => Position.isWithin(pos, bounds))
260
+ }
261
+
262
+ // Check if position is valid (within grid bounds)
263
+ let isValidPosition = (grid: t, pos: Position.t): bool => {
264
+ pos.row >= 0 && pos.row < grid.height && pos.col >= 0 && pos.col < grid.width
265
+ }