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