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,158 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Types from "../Core/Types.mjs";
|
|
4
|
+
import * as ErrorTypes from "../Errors/ErrorTypes.mjs";
|
|
5
|
+
import * as Belt_Option from "@rescript/runtime/lib/es6/Belt_Option.js";
|
|
6
|
+
|
|
7
|
+
function contains(outer, inner) {
|
|
8
|
+
return Types.Bounds.contains(outer, inner);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function findParent(target, candidates) {
|
|
12
|
+
let containers = candidates.filter(candidate => {
|
|
13
|
+
if (candidate !== target) {
|
|
14
|
+
return Types.Bounds.contains(candidate.bounds, target.bounds);
|
|
15
|
+
} else {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
if (containers.length === 0) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let sorted = containers.toSorted((a, b) => {
|
|
23
|
+
let areaA = Types.Bounds.area(a.bounds);
|
|
24
|
+
let areaB = Types.Bounds.area(b.bounds);
|
|
25
|
+
return areaA - areaB | 0;
|
|
26
|
+
});
|
|
27
|
+
return sorted[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildHierarchy(boxes) {
|
|
31
|
+
let overlappingPair = boxes.find(box1 => boxes.some(box2 => {
|
|
32
|
+
if (box1 !== box2 && !Types.Bounds.contains(box1.bounds, box2.bounds) && !Types.Bounds.contains(box2.bounds, box1.bounds)) {
|
|
33
|
+
return Types.Bounds.overlaps(box1.bounds, box2.bounds);
|
|
34
|
+
} else {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
if (overlappingPair !== undefined) {
|
|
39
|
+
let box2 = Belt_Option.getExn(boxes.find(box2 => {
|
|
40
|
+
if (overlappingPair !== box2 && !Types.Bounds.contains(overlappingPair.bounds, box2.bounds) && !Types.Bounds.contains(box2.bounds, overlappingPair.bounds)) {
|
|
41
|
+
return Types.Bounds.overlaps(overlappingPair.bounds, box2.bounds);
|
|
42
|
+
} else {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
return {
|
|
47
|
+
TAG: "Error",
|
|
48
|
+
_0: {
|
|
49
|
+
TAG: "OverlappingBoxes",
|
|
50
|
+
box1: overlappingPair,
|
|
51
|
+
box2: box2
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
let sorted = boxes.toSorted((a, b) => {
|
|
56
|
+
let areaA = Types.Bounds.area(a.bounds);
|
|
57
|
+
let areaB = Types.Bounds.area(b.bounds);
|
|
58
|
+
return areaB - areaA | 0;
|
|
59
|
+
});
|
|
60
|
+
sorted.forEach(box => {
|
|
61
|
+
let parent = findParent(box, sorted);
|
|
62
|
+
if (parent !== undefined) {
|
|
63
|
+
parent.children.push(box);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
let roots = sorted.filter(box => !sorted.some(candidate => {
|
|
68
|
+
if (candidate !== box) {
|
|
69
|
+
return candidate.children.includes(box);
|
|
70
|
+
} else {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
return {
|
|
75
|
+
TAG: "Ok",
|
|
76
|
+
_0: roots
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getDepth(box, allBoxes) {
|
|
81
|
+
let parent = findParent(box, allBoxes);
|
|
82
|
+
if (parent !== undefined) {
|
|
83
|
+
return 1 + getDepth(parent, allBoxes) | 0;
|
|
84
|
+
} else {
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function collectDeepNestingWarnings(box, currentDepth, thresholdOpt) {
|
|
90
|
+
let threshold = thresholdOpt !== undefined ? thresholdOpt : 4;
|
|
91
|
+
let warnings = [];
|
|
92
|
+
if (currentDepth > threshold) {
|
|
93
|
+
let warningCode_1 = {
|
|
94
|
+
row: box.bounds.top,
|
|
95
|
+
col: box.bounds.left
|
|
96
|
+
};
|
|
97
|
+
let warningCode = {
|
|
98
|
+
TAG: "DeepNesting",
|
|
99
|
+
depth: currentDepth,
|
|
100
|
+
position: warningCode_1
|
|
101
|
+
};
|
|
102
|
+
let warning = ErrorTypes.make(warningCode, undefined);
|
|
103
|
+
warnings.push(warning);
|
|
104
|
+
}
|
|
105
|
+
box.children.forEach(child => {
|
|
106
|
+
let childWarnings = collectDeepNestingWarnings(child, currentDepth + 1 | 0, threshold);
|
|
107
|
+
childWarnings.forEach(w => {
|
|
108
|
+
warnings.push(w);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
return warnings;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function detectDeepNesting(roots, thresholdOpt) {
|
|
115
|
+
let threshold = thresholdOpt !== undefined ? thresholdOpt : 4;
|
|
116
|
+
let allWarnings = [];
|
|
117
|
+
roots.forEach(root => {
|
|
118
|
+
let warnings = collectDeepNestingWarnings(root, 0, threshold);
|
|
119
|
+
warnings.forEach(w => {
|
|
120
|
+
allWarnings.push(w);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return allWarnings;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getMaxDepth(roots) {
|
|
127
|
+
let maxDepth = {
|
|
128
|
+
contents: 0
|
|
129
|
+
};
|
|
130
|
+
let traverse = (box, depth) => {
|
|
131
|
+
if (depth > maxDepth.contents) {
|
|
132
|
+
maxDepth.contents = depth;
|
|
133
|
+
}
|
|
134
|
+
box.children.forEach(child => traverse(child, depth + 1 | 0));
|
|
135
|
+
};
|
|
136
|
+
roots.forEach(root => traverse(root, 0));
|
|
137
|
+
return maxDepth.contents;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function makeBox(name, bounds) {
|
|
141
|
+
return {
|
|
142
|
+
name: name,
|
|
143
|
+
bounds: bounds,
|
|
144
|
+
children: []
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
contains,
|
|
150
|
+
findParent,
|
|
151
|
+
buildHierarchy,
|
|
152
|
+
getDepth,
|
|
153
|
+
collectDeepNestingWarnings,
|
|
154
|
+
detectDeepNesting,
|
|
155
|
+
getMaxDepth,
|
|
156
|
+
makeBox,
|
|
157
|
+
}
|
|
158
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// HierarchyBuilder.res
|
|
2
|
+
// Build hierarchical parent-child relationships between boxes based on spatial containment
|
|
3
|
+
// Requirements: REQ-6 (Shape Detector - Nesting Hierarchy Construction)
|
|
4
|
+
|
|
5
|
+
open Types
|
|
6
|
+
|
|
7
|
+
// Re-use the box type from BoxTracer
|
|
8
|
+
type box = BoxTracer.box
|
|
9
|
+
|
|
10
|
+
// Hierarchy building errors
|
|
11
|
+
type hierarchyError =
|
|
12
|
+
| OverlappingBoxes({
|
|
13
|
+
box1: box,
|
|
14
|
+
box2: box,
|
|
15
|
+
})
|
|
16
|
+
| CircularNesting
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Containment Detection
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if outer box completely contains inner box.
|
|
24
|
+
* Uses the Bounds.contains function for the actual containment check.
|
|
25
|
+
*
|
|
26
|
+
* A box is considered to contain another if its bounds completely enclose
|
|
27
|
+
* the other box's bounds (strict containment, not touching edges).
|
|
28
|
+
*
|
|
29
|
+
* @param outer - The potentially containing box bounds
|
|
30
|
+
* @param inner - The potentially contained box bounds
|
|
31
|
+
* @return bool - true if outer completely contains inner
|
|
32
|
+
*
|
|
33
|
+
* Requirements: REQ-6 (Nesting Hierarchy Construction)
|
|
34
|
+
*/
|
|
35
|
+
let contains = (outer: Bounds.t, inner: Bounds.t): bool => {
|
|
36
|
+
Bounds.contains(outer, inner)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Parent-Child Relationships
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Find the smallest box that completely contains the given box.
|
|
45
|
+
* This identifies the immediate parent in the hierarchy.
|
|
46
|
+
*
|
|
47
|
+
* Algorithm:
|
|
48
|
+
* 1. Filter candidates to only boxes that contain the target
|
|
49
|
+
* 2. Among containing boxes, find the one with the smallest area
|
|
50
|
+
* 3. Return None if no containing box exists (box is a root)
|
|
51
|
+
*
|
|
52
|
+
* @param target - The box to find a parent for
|
|
53
|
+
* @param candidates - Array of potential parent boxes
|
|
54
|
+
* @return option<box> - Some(parent) if found, None if box is a root
|
|
55
|
+
*
|
|
56
|
+
* Requirements: REQ-6 (Nesting Hierarchy Construction)
|
|
57
|
+
*/
|
|
58
|
+
let findParent = (target: box, candidates: array<box>): option<box> => {
|
|
59
|
+
// Find all boxes that contain this box
|
|
60
|
+
let containers =
|
|
61
|
+
candidates->Array.filter(candidate => {
|
|
62
|
+
// Don't compare with self
|
|
63
|
+
candidate !== target && contains(candidate.bounds, target.bounds)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// If no containers, this is a root box
|
|
67
|
+
if Array.length(containers) === 0 {
|
|
68
|
+
None
|
|
69
|
+
} else {
|
|
70
|
+
// Find the smallest container (immediate parent)
|
|
71
|
+
// Sort by area ascending and take the first
|
|
72
|
+
let sorted =
|
|
73
|
+
containers->Array.toSorted((a, b) => {
|
|
74
|
+
let areaA = Bounds.area(a.bounds)
|
|
75
|
+
let areaB = Bounds.area(b.bounds)
|
|
76
|
+
Int.toFloat(areaA - areaB) // Ascending - smallest first
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
Array.get(sorted, 0)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build parent-child hierarchy from a flat array of boxes.
|
|
85
|
+
*
|
|
86
|
+
* Algorithm:
|
|
87
|
+
* 1. Sort boxes by area (descending) - larger boxes processed first
|
|
88
|
+
* 2. For each box, find its immediate parent (smallest containing box)
|
|
89
|
+
* 3. Populate children arrays by adding child to parent's children
|
|
90
|
+
* 4. Validate no invalid overlaps exist (boxes must be nested or disjoint)
|
|
91
|
+
* 5. Return only root-level boxes (boxes with no parent)
|
|
92
|
+
*
|
|
93
|
+
* This function mutates the children arrays of the boxes to build the hierarchy.
|
|
94
|
+
*
|
|
95
|
+
* @param boxes - Flat array of all boxes detected
|
|
96
|
+
* @return result<array<box>, hierarchyError> - Ok(roots) or Error(overlap)
|
|
97
|
+
*
|
|
98
|
+
* Example hierarchy:
|
|
99
|
+
* Root1
|
|
100
|
+
* ├── Child1-1
|
|
101
|
+
* │ └── Child1-1-1
|
|
102
|
+
* └── Child1-2
|
|
103
|
+
* Root2
|
|
104
|
+
* └── Child2-1
|
|
105
|
+
*
|
|
106
|
+
* Requirements: REQ-6 (Nesting Hierarchy Construction)
|
|
107
|
+
*/
|
|
108
|
+
let buildHierarchy = (boxes: array<box>): result<array<box>, hierarchyError> => {
|
|
109
|
+
// Step 1: Check for invalid overlapping boxes first (before building hierarchy)
|
|
110
|
+
// Boxes are invalid if they overlap but neither contains the other
|
|
111
|
+
let overlappingPair = boxes->Array.find(box1 => {
|
|
112
|
+
boxes->Array.some(box2 => {
|
|
113
|
+
// Check if boxes are different
|
|
114
|
+
box1 !== box2 &&
|
|
115
|
+
// Not nested (neither contains the other)
|
|
116
|
+
!contains(box1.bounds, box2.bounds) &&
|
|
117
|
+
!contains(box2.bounds, box1.bounds) &&
|
|
118
|
+
// But they overlap
|
|
119
|
+
Bounds.overlaps(box1.bounds, box2.bounds)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
switch overlappingPair {
|
|
124
|
+
| Some(box1) => {
|
|
125
|
+
// Find the specific box2 that overlaps with box1
|
|
126
|
+
let box2 =
|
|
127
|
+
boxes
|
|
128
|
+
->Array.find(box2 => {
|
|
129
|
+
box1 !== box2 &&
|
|
130
|
+
!contains(box1.bounds, box2.bounds) &&
|
|
131
|
+
!contains(box2.bounds, box1.bounds) &&
|
|
132
|
+
Bounds.overlaps(box1.bounds, box2.bounds)
|
|
133
|
+
})
|
|
134
|
+
->Belt.Option.getExn // Safe because we just found an overlapping pair
|
|
135
|
+
|
|
136
|
+
Error(OverlappingBoxes({box1, box2}))
|
|
137
|
+
}
|
|
138
|
+
| None => {
|
|
139
|
+
// Step 2: Sort by area (descending) for efficient parent finding
|
|
140
|
+
// Larger boxes are processed first as they are potential parents
|
|
141
|
+
let sorted =
|
|
142
|
+
boxes->Array.toSorted((a, b) => {
|
|
143
|
+
let areaA = Bounds.area(a.bounds)
|
|
144
|
+
let areaB = Bounds.area(b.bounds)
|
|
145
|
+
Int.toFloat(areaB - areaA) // Descending - largest first
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Step 3: Build parent-child relationships
|
|
149
|
+
sorted->Array.forEach(box => {
|
|
150
|
+
// Find immediate parent for this box
|
|
151
|
+
switch findParent(box, sorted) {
|
|
152
|
+
| Some(parent) => {
|
|
153
|
+
// Add this box to parent's children array (mutation)
|
|
154
|
+
parent.children->Array.push(box)->ignore
|
|
155
|
+
}
|
|
156
|
+
| None => () // No parent - this is a root box
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Step 4: Return only root boxes (boxes with no parent)
|
|
161
|
+
// A root box is one that is not in any other box's children array
|
|
162
|
+
let roots = sorted->Array.filter(box => {
|
|
163
|
+
// Check if this box is in any other box's children
|
|
164
|
+
let isChild = sorted->Array.some(candidate => {
|
|
165
|
+
candidate !== box && candidate.children->Array.includes(box)
|
|
166
|
+
})
|
|
167
|
+
!isChild
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
Ok(roots)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get the depth of nesting for a box (0 for root, 1 for first level child, etc.)
|
|
177
|
+
* Useful for validation and testing.
|
|
178
|
+
*
|
|
179
|
+
* @param box - The box to calculate depth for
|
|
180
|
+
* @param allBoxes - All boxes in the hierarchy (to find parent)
|
|
181
|
+
* @return int - Nesting depth
|
|
182
|
+
*/
|
|
183
|
+
let rec getDepth = (box: box, allBoxes: array<box>): int => {
|
|
184
|
+
// Find this box's parent
|
|
185
|
+
switch findParent(box, allBoxes) {
|
|
186
|
+
| None => 0 // Root box
|
|
187
|
+
| Some(parent) => 1 + getDepth(parent, allBoxes)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Deep Nesting Warning Detection
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Recursively collect deep nesting warnings for boxes exceeding depth threshold.
|
|
197
|
+
*
|
|
198
|
+
* This function traverses the box hierarchy and generates warnings for any box
|
|
199
|
+
* that is nested deeper than the specified threshold (default: 4 levels).
|
|
200
|
+
*
|
|
201
|
+
* Algorithm:
|
|
202
|
+
* 1. Check if current box depth exceeds threshold
|
|
203
|
+
* 2. If yes, create a DeepNesting warning with depth and position
|
|
204
|
+
* 3. Recursively check all children with incremented depth
|
|
205
|
+
* 4. Collect and return all warnings found
|
|
206
|
+
*
|
|
207
|
+
* @param box - The box to check (and its children)
|
|
208
|
+
* @param currentDepth - Current nesting depth (0 for root)
|
|
209
|
+
* @param threshold - Maximum allowed depth before warning (default: 4)
|
|
210
|
+
* @return array<ErrorTypes.t> - Array of deep nesting warnings
|
|
211
|
+
*
|
|
212
|
+
* Example:
|
|
213
|
+
* Root (depth 0) - OK
|
|
214
|
+
* Child1 (depth 1) - OK
|
|
215
|
+
* Child1-1 (depth 2) - OK
|
|
216
|
+
* Child1-1-1 (depth 3) - OK
|
|
217
|
+
* Child1-1-1-1 (depth 4) - OK
|
|
218
|
+
* Child1-1-1-1-1 (depth 5) - WARNING!
|
|
219
|
+
*
|
|
220
|
+
* Requirements: REQ-19 (Deep Nesting Warning)
|
|
221
|
+
*/
|
|
222
|
+
let rec collectDeepNestingWarnings = (
|
|
223
|
+
box: box,
|
|
224
|
+
currentDepth: int,
|
|
225
|
+
~threshold: int=4,
|
|
226
|
+
): array<ErrorTypes.t> => {
|
|
227
|
+
let warnings = []
|
|
228
|
+
|
|
229
|
+
// Check if current box exceeds threshold
|
|
230
|
+
if currentDepth > threshold {
|
|
231
|
+
// Create warning with depth and position information
|
|
232
|
+
let warningCode = ErrorTypes.DeepNesting({
|
|
233
|
+
depth: currentDepth,
|
|
234
|
+
position: {
|
|
235
|
+
row: box.bounds.top,
|
|
236
|
+
col: box.bounds.left,
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Create ParseError from warning code (no context needed for warnings)
|
|
241
|
+
let warning = ErrorTypes.make(warningCode, None)
|
|
242
|
+
warnings->Array.push(warning)->ignore
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Recursively check all children with incremented depth
|
|
246
|
+
box.children->Array.forEach(child => {
|
|
247
|
+
let childWarnings = collectDeepNestingWarnings(child, currentDepth + 1, ~threshold)
|
|
248
|
+
childWarnings->Array.forEach(w => warnings->Array.push(w)->ignore)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
warnings
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Scan entire hierarchy and collect all deep nesting warnings.
|
|
256
|
+
*
|
|
257
|
+
* This is the main entry point for deep nesting detection. It processes
|
|
258
|
+
* all root boxes and their entire subtrees.
|
|
259
|
+
*
|
|
260
|
+
* @param roots - Array of root boxes (from buildHierarchy)
|
|
261
|
+
* @param threshold - Maximum allowed depth before warning (default: 4)
|
|
262
|
+
* @return array<ErrorTypes.t> - All deep nesting warnings found
|
|
263
|
+
*
|
|
264
|
+
* Requirements: REQ-19 (Deep Nesting Warning)
|
|
265
|
+
*/
|
|
266
|
+
let detectDeepNesting = (roots: array<box>, ~threshold: int=4): array<ErrorTypes.t> => {
|
|
267
|
+
let allWarnings = []
|
|
268
|
+
|
|
269
|
+
// Process each root tree
|
|
270
|
+
roots->Array.forEach(root => {
|
|
271
|
+
// Root boxes start at depth 0
|
|
272
|
+
let warnings = collectDeepNestingWarnings(root, 0, ~threshold)
|
|
273
|
+
warnings->Array.forEach(w => allWarnings->Array.push(w)->ignore)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
allWarnings
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get maximum nesting depth in the entire hierarchy.
|
|
281
|
+
* Useful for metrics, validation, and testing.
|
|
282
|
+
*
|
|
283
|
+
* @param roots - Array of root boxes
|
|
284
|
+
* @return int - Maximum depth found (0 if no boxes)
|
|
285
|
+
*/
|
|
286
|
+
let getMaxDepth = (roots: array<box>): int => {
|
|
287
|
+
let maxDepth = ref(0)
|
|
288
|
+
|
|
289
|
+
let rec traverse = (box: box, depth: int) => {
|
|
290
|
+
if depth > maxDepth.contents {
|
|
291
|
+
maxDepth := depth
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
box.children->Array.forEach(child => traverse(child, depth + 1))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
roots->Array.forEach(root => traverse(root, 0))
|
|
298
|
+
|
|
299
|
+
maxDepth.contents
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Create a box for testing purposes.
|
|
304
|
+
*
|
|
305
|
+
* @param name - Optional box name
|
|
306
|
+
* @param bounds - Bounding box coordinates
|
|
307
|
+
* @return box - New box instance with empty children array
|
|
308
|
+
*/
|
|
309
|
+
let makeBox = (~name: option<string>=?, bounds: Bounds.t): box => {
|
|
310
|
+
{
|
|
311
|
+
name: name,
|
|
312
|
+
bounds: bounds,
|
|
313
|
+
children: [],
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Types from "../Core/Types.mjs";
|
|
4
|
+
import * as BoxTracer from "./BoxTracer.mjs";
|
|
5
|
+
import * as ErrorTypes from "../Errors/ErrorTypes.mjs";
|
|
6
|
+
import * as Core__Array from "@rescript/core/src/Core__Array.mjs";
|
|
7
|
+
import * as HierarchyBuilder from "./HierarchyBuilder.mjs";
|
|
8
|
+
|
|
9
|
+
function hierarchyErrorToParseError(error) {
|
|
10
|
+
if (typeof error !== "object") {
|
|
11
|
+
return ErrorTypes.make({
|
|
12
|
+
TAG: "InvalidElement",
|
|
13
|
+
content: "Circular nesting detected in box hierarchy",
|
|
14
|
+
position: {
|
|
15
|
+
row: 0,
|
|
16
|
+
col: 0
|
|
17
|
+
}
|
|
18
|
+
}, undefined);
|
|
19
|
+
}
|
|
20
|
+
let box2 = error.box2;
|
|
21
|
+
let box1 = error.box1;
|
|
22
|
+
return ErrorTypes.make({
|
|
23
|
+
TAG: "InvalidElement",
|
|
24
|
+
content: `Overlapping boxes detected at (` + box1.bounds.top.toString() + `,` + box1.bounds.left.toString() + `) and (` + box2.bounds.top.toString() + `,` + box2.bounds.left.toString() + `)`,
|
|
25
|
+
position: {
|
|
26
|
+
row: box1.bounds.top,
|
|
27
|
+
col: box1.bounds.left
|
|
28
|
+
}
|
|
29
|
+
}, undefined);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function deduplicateBoxes(boxes) {
|
|
33
|
+
let seen = [];
|
|
34
|
+
let unique = [];
|
|
35
|
+
boxes.forEach(box => {
|
|
36
|
+
let isDuplicate = seen.some(seenBounds => Types.Bounds.equals(seenBounds, box.bounds));
|
|
37
|
+
if (!isDuplicate) {
|
|
38
|
+
unique.push(box);
|
|
39
|
+
seen.push(box.bounds);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return unique;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function detect(grid) {
|
|
47
|
+
let corners = grid.cornerIndex;
|
|
48
|
+
let boxes = [];
|
|
49
|
+
let traceFailures = [];
|
|
50
|
+
corners.forEach(corner => {
|
|
51
|
+
let box = BoxTracer.traceBox(grid, corner);
|
|
52
|
+
if (box.TAG === "Ok") {
|
|
53
|
+
boxes.push(box._0);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
traceFailures.push(box._0);
|
|
57
|
+
});
|
|
58
|
+
if (boxes.length === 0) {
|
|
59
|
+
if (corners.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
TAG: "Ok",
|
|
62
|
+
_0: []
|
|
63
|
+
};
|
|
64
|
+
} else if (traceFailures.length > 0) {
|
|
65
|
+
return {
|
|
66
|
+
TAG: "Error",
|
|
67
|
+
_0: traceFailures
|
|
68
|
+
};
|
|
69
|
+
} else {
|
|
70
|
+
return {
|
|
71
|
+
TAG: "Ok",
|
|
72
|
+
_0: []
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let uniqueBoxes = deduplicateBoxes(boxes);
|
|
77
|
+
let rootBoxes = HierarchyBuilder.buildHierarchy(uniqueBoxes);
|
|
78
|
+
if (rootBoxes.TAG === "Ok") {
|
|
79
|
+
return {
|
|
80
|
+
TAG: "Ok",
|
|
81
|
+
_0: rootBoxes._0
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let parseError = hierarchyErrorToParseError(rootBoxes._0);
|
|
85
|
+
return {
|
|
86
|
+
TAG: "Error",
|
|
87
|
+
_0: [parseError]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function countBoxes(boxes) {
|
|
92
|
+
return Core__Array.reduce(boxes, 0, (acc, box) => (1 + acc | 0) + countBoxes(box.children) | 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function flattenBoxes(boxes) {
|
|
96
|
+
return Core__Array.reduce(boxes, [], (acc, box) => {
|
|
97
|
+
let childBoxes = flattenBoxes(box.children);
|
|
98
|
+
return acc.concat([box]).concat(childBoxes);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getStats(result) {
|
|
103
|
+
if (result.TAG === "Ok") {
|
|
104
|
+
let boxes = result._0;
|
|
105
|
+
let rootCount = boxes.length;
|
|
106
|
+
let totalCount = countBoxes(boxes);
|
|
107
|
+
return `Shape Detection Success:
|
|
108
|
+
Root boxes: ` + rootCount.toString() + `
|
|
109
|
+
Total boxes (including nested): ` + totalCount.toString();
|
|
110
|
+
}
|
|
111
|
+
let errors = result._0;
|
|
112
|
+
let errorCount = errors.length;
|
|
113
|
+
let warningCount = Core__Array.reduce(errors, 0, (acc, err) => {
|
|
114
|
+
if (ErrorTypes.isWarning(err)) {
|
|
115
|
+
return acc + 1 | 0;
|
|
116
|
+
} else {
|
|
117
|
+
return acc;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
let realErrorCount = errorCount - warningCount | 0;
|
|
121
|
+
return `Shape Detection Failed:
|
|
122
|
+
Errors: ` + realErrorCount.toString() + `
|
|
123
|
+
Warnings: ` + warningCount.toString();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
hierarchyErrorToParseError,
|
|
128
|
+
deduplicateBoxes,
|
|
129
|
+
detect,
|
|
130
|
+
countBoxes,
|
|
131
|
+
flattenBoxes,
|
|
132
|
+
getStats,
|
|
133
|
+
}
|
|
134
|
+
/* Types Not a pure module */
|