wyreframe 0.7.1 → 0.7.4
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/package.json +1 -1
- package/src/index.test.ts +3 -3
- package/src/parser/Core/Types.mjs +2 -0
- package/src/parser/Core/Types.res +4 -0
- package/src/parser/Detector/BoxTracer.mjs +69 -1
- package/src/parser/Detector/BoxTracer.res +71 -1
- package/src/parser/Interactions/InteractionMerger.res +2 -0
- package/src/parser/Semantic/SemanticParser.mjs +39 -14
- package/src/parser/Semantic/SemanticParser.res +28 -1
- package/src/renderer/Renderer.mjs +6 -20
- package/src/renderer/Renderer.res +15 -29
package/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -352,9 +352,9 @@ describe('mixed text and link content (Issue #14)', () => {
|
|
|
352
352
|
const wireframe = `
|
|
353
353
|
@scene: test
|
|
354
354
|
|
|
355
|
-
|
|
356
|
-
|
|
|
357
|
-
|
|
355
|
+
+---------------------------------+
|
|
356
|
+
| Please "click here" to continue |
|
|
357
|
+
+---------------------------------+
|
|
358
358
|
`;
|
|
359
359
|
|
|
360
360
|
const result = parse(wireframe);
|
|
@@ -180,6 +180,9 @@ and element =
|
|
|
180
180
|
| Divider({
|
|
181
181
|
position: Position.t,
|
|
182
182
|
})
|
|
183
|
+
| Spacer({
|
|
184
|
+
position: Position.t,
|
|
185
|
+
})
|
|
183
186
|
| Row({
|
|
184
187
|
children: array<element>,
|
|
185
188
|
align: alignment,
|
|
@@ -284,6 +287,7 @@ let getElementType = (elem: element): string => {
|
|
|
284
287
|
| Checkbox(_) => "Checkbox"
|
|
285
288
|
| Text(_) => "Text"
|
|
286
289
|
| Divider(_) => "Divider"
|
|
290
|
+
| Spacer(_) => "Spacer"
|
|
287
291
|
| Row(_) => "Row"
|
|
288
292
|
| Section(_) => "Section"
|
|
289
293
|
}
|
|
@@ -87,7 +87,24 @@ function findBottomRightCorner(grid, topLeft, topRight) {
|
|
|
87
87
|
switch (match) {
|
|
88
88
|
case "Corner" :
|
|
89
89
|
lastCorner = pos;
|
|
90
|
-
|
|
90
|
+
let nextRow = row + 1 | 0;
|
|
91
|
+
if (nextRow < grid.height) {
|
|
92
|
+
let nextPos = Types.Position.make(nextRow, topRight.col);
|
|
93
|
+
let match$1 = Grid.get(grid, nextPos);
|
|
94
|
+
if (match$1 === "Corner" && typeof match$1 !== "object") {
|
|
95
|
+
let nextLeftPos = Types.Position.make(nextRow, topLeft.col);
|
|
96
|
+
let match$2 = Grid.get(grid, nextLeftPos);
|
|
97
|
+
if (match$2 === "Corner" && typeof match$2 !== "object") {
|
|
98
|
+
$$continue = false;
|
|
99
|
+
} else {
|
|
100
|
+
row = row + 1 | 0;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
row = row + 1 | 0;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
row = row + 1 | 0;
|
|
107
|
+
}
|
|
91
108
|
break;
|
|
92
109
|
case "VLine" :
|
|
93
110
|
row = row + 1 | 0;
|
|
@@ -135,6 +152,57 @@ function traceBox(grid, topLeft) {
|
|
|
135
152
|
})
|
|
136
153
|
};
|
|
137
154
|
}
|
|
155
|
+
let nextRow = topLeft.row + 1 | 0;
|
|
156
|
+
let isValidTopEdge;
|
|
157
|
+
if (nextRow < grid.height) {
|
|
158
|
+
let nextLeftPos = Types.Position.make(nextRow, topLeft.col);
|
|
159
|
+
let nextRightPos = Types.Position.make(nextRow, topRightOpt.col);
|
|
160
|
+
let leftChar = Grid.get(grid, nextLeftPos);
|
|
161
|
+
let rightChar = Grid.get(grid, nextRightPos);
|
|
162
|
+
if (leftChar !== undefined && typeof leftChar !== "object") {
|
|
163
|
+
switch (leftChar) {
|
|
164
|
+
case "Corner" :
|
|
165
|
+
if (rightChar !== undefined && typeof rightChar !== "object") {
|
|
166
|
+
switch (rightChar) {
|
|
167
|
+
case "Corner" :
|
|
168
|
+
isValidTopEdge = false;
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
isValidTopEdge = true;
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
isValidTopEdge = true;
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
case "VLine" :
|
|
178
|
+
if (rightChar !== undefined && typeof rightChar !== "object") {
|
|
179
|
+
switch (rightChar) {
|
|
180
|
+
default:
|
|
181
|
+
isValidTopEdge = true;
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
isValidTopEdge = true;
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
isValidTopEdge = true;
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
isValidTopEdge = true;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
isValidTopEdge = false;
|
|
195
|
+
}
|
|
196
|
+
if (!isValidTopEdge) {
|
|
197
|
+
return {
|
|
198
|
+
TAG: "Error",
|
|
199
|
+
_0: ErrorTypes.makeSimple({
|
|
200
|
+
TAG: "InvalidElement",
|
|
201
|
+
content: "Not a valid box top-left corner (adjacent box detected)",
|
|
202
|
+
position: topLeft
|
|
203
|
+
})
|
|
204
|
+
};
|
|
205
|
+
}
|
|
138
206
|
let topEdgeChars = topEdgeScan.map(param => param[1]);
|
|
139
207
|
if (isDividerOnlyEdge(topEdgeChars)) {
|
|
140
208
|
return {
|
|
@@ -114,6 +114,7 @@ let rowHasVLineInRange = (grid: Grid.t, row: int, leftCol: int, rightCol: int):
|
|
|
114
114
|
* - Continues through rows with misaligned VLines (records for warnings)
|
|
115
115
|
* - Stops at rows with no VLine at all in the box's column range (box boundary)
|
|
116
116
|
* - Handles internal dividers (+=====+) correctly by finding the last corner
|
|
117
|
+
* - IMPORTANT: Stops when detecting vertically adjacent boxes (Issue #18)
|
|
117
118
|
*
|
|
118
119
|
* @param grid - The 2D character grid
|
|
119
120
|
* @param topLeft - Position of top-left corner (for determining left boundary)
|
|
@@ -132,7 +133,40 @@ let findBottomRightCorner = (grid: Grid.t, topLeft: Position.t, topRight: Positi
|
|
|
132
133
|
| Some(Corner) => {
|
|
133
134
|
// Found a corner at the expected column - remember it
|
|
134
135
|
lastCorner := Some(pos)
|
|
135
|
-
|
|
136
|
+
|
|
137
|
+
// Issue #18 fix: Check if the NEXT row also has a Corner at the same column.
|
|
138
|
+
// If so, we've found the bottom of THIS box and the top of ANOTHER box.
|
|
139
|
+
// We should stop scanning here to avoid merging adjacent boxes.
|
|
140
|
+
let nextRow = row.contents + 1
|
|
141
|
+
if nextRow < grid.height {
|
|
142
|
+
let nextPos = Position.make(nextRow, topRight.col)
|
|
143
|
+
switch Grid.get(grid, nextPos) {
|
|
144
|
+
| Some(Corner) => {
|
|
145
|
+
// Next row also has a corner - this indicates adjacent boxes
|
|
146
|
+
// Also check if the next row starts a new box by checking left column
|
|
147
|
+
let nextLeftPos = Position.make(nextRow, topLeft.col)
|
|
148
|
+
switch Grid.get(grid, nextLeftPos) {
|
|
149
|
+
| Some(Corner) => {
|
|
150
|
+
// Both corners present in next row = new box starting
|
|
151
|
+
// Stop scanning - current corner is our bottom-right
|
|
152
|
+
continue := false
|
|
153
|
+
}
|
|
154
|
+
| _ => {
|
|
155
|
+
// Only right corner in next row - might be internal structure
|
|
156
|
+
// Continue scanning
|
|
157
|
+
row := row.contents + 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
| _ => {
|
|
162
|
+
// Next row doesn't have a corner - continue normally
|
|
163
|
+
row := row.contents + 1
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// No more rows - we're done
|
|
168
|
+
row := row.contents + 1
|
|
169
|
+
}
|
|
136
170
|
}
|
|
137
171
|
| Some(VLine) => {
|
|
138
172
|
// Found a VLine at the expected column - continue scanning
|
|
@@ -198,6 +232,41 @@ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
|
|
|
198
232
|
),
|
|
199
233
|
)
|
|
200
234
|
| Some(topRight) => {
|
|
235
|
+
// Issue #18 fix: Validate this is a TRUE top edge, not a bottom edge
|
|
236
|
+
// of another box. A valid top edge should have VLine characters (|)
|
|
237
|
+
// in the next row at the left and right columns, NOT Corner characters (+).
|
|
238
|
+
// If the next row has corners at both positions, this is likely the
|
|
239
|
+
// bottom edge of one box immediately followed by the top edge of another.
|
|
240
|
+
let nextRow = topLeft.row + 1
|
|
241
|
+
let isValidTopEdge = if nextRow < grid.height {
|
|
242
|
+
let nextLeftPos = Position.make(nextRow, topLeft.col)
|
|
243
|
+
let nextRightPos = Position.make(nextRow, topRight.col)
|
|
244
|
+
let leftChar = Grid.get(grid, nextLeftPos)
|
|
245
|
+
let rightChar = Grid.get(grid, nextRightPos)
|
|
246
|
+
// Valid top edge: next row has VLine on both sides (content row)
|
|
247
|
+
// Invalid: next row has Corner on both sides (another box edge)
|
|
248
|
+
switch (leftChar, rightChar) {
|
|
249
|
+
| (Some(Corner), Some(Corner)) => false // This is a bottom edge, not a top
|
|
250
|
+
| (Some(VLine), Some(VLine)) => true // This is a valid top edge
|
|
251
|
+
| (Some(VLine), Some(Corner)) => true // Could be valid with internal structure
|
|
252
|
+
| (Some(Corner), Some(VLine)) => true // Could be valid with internal structure
|
|
253
|
+
| _ => true // Be permissive for other cases
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
false // No next row means this can't be a valid box top
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if !isValidTopEdge {
|
|
260
|
+
// This corner is likely a bottom edge of another box, not a valid top-left
|
|
261
|
+
Error(
|
|
262
|
+
ErrorTypes.makeSimple(
|
|
263
|
+
ErrorTypes.InvalidElement({
|
|
264
|
+
content: "Not a valid box top-left corner (adjacent box detected)",
|
|
265
|
+
position: topLeft,
|
|
266
|
+
}),
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
} else {
|
|
201
270
|
// Step 3: Extract box name from top edge
|
|
202
271
|
let topEdgeChars = Array.map(topEdgeScan, ((_, cell)) => cell)
|
|
203
272
|
|
|
@@ -427,6 +496,7 @@ let traceBox = (grid: Grid.t, topLeft: Position.t): traceResult => {
|
|
|
427
496
|
}
|
|
428
497
|
}
|
|
429
498
|
} // Close else block for isDividerOnlyEdge
|
|
499
|
+
} // Close else block for isValidTopEdge
|
|
430
500
|
}
|
|
431
501
|
}
|
|
432
502
|
| _ =>
|
|
@@ -61,6 +61,7 @@ let rec collectElementIds = (element: element): Belt.Set.String.t => {
|
|
|
61
61
|
| Checkbox(_) => ids // Checkboxes don't have explicit IDs
|
|
62
62
|
| Text(_) => ids // Text elements don't have explicit IDs
|
|
63
63
|
| Divider(_) => ids // Dividers don't have IDs
|
|
64
|
+
| Spacer(_) => ids // Spacers don't have IDs
|
|
64
65
|
| Row({children}) => {
|
|
65
66
|
children->Array.reduce(ids, (acc, child) => {
|
|
66
67
|
Belt.Set.String.union(acc, collectElementIds(child))
|
|
@@ -337,6 +338,7 @@ let rec attachInteractionsToElement = (
|
|
|
337
338
|
| Checkbox(_) as el => el
|
|
338
339
|
| Text(_) as el => el
|
|
339
340
|
| Divider(_) as el => el
|
|
341
|
+
| Spacer(_) as el => el
|
|
340
342
|
}
|
|
341
343
|
}
|
|
342
344
|
|
|
@@ -623,17 +623,40 @@ function segmentToElement(segment, basePosition, baseCol, bounds) {
|
|
|
623
623
|
}
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
function isNoiseText(content) {
|
|
627
|
+
let trimmed = content.trim();
|
|
628
|
+
if (trimmed === "") {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
let borderPattern = /^[+|=\-\s]+$/;
|
|
632
|
+
let hasPipeOrPlus = /[+|]/;
|
|
633
|
+
if (borderPattern.test(trimmed)) {
|
|
634
|
+
return true;
|
|
635
|
+
} else {
|
|
636
|
+
return hasPipeOrPlus.test(trimmed);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
626
640
|
function parseContentLine(line, lineIndex, contentStartRow, box, registry) {
|
|
627
641
|
let trimmed = line.trim();
|
|
628
642
|
if (trimmed === "") {
|
|
643
|
+
let row = contentStartRow + lineIndex | 0;
|
|
644
|
+
let baseCol = box.bounds.left + 1 | 0;
|
|
645
|
+
let position = Types.Position.make(row, baseCol);
|
|
646
|
+
return {
|
|
647
|
+
TAG: "Spacer",
|
|
648
|
+
position: position
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (isNoiseText(trimmed)) {
|
|
629
652
|
return;
|
|
630
653
|
}
|
|
631
|
-
let row = contentStartRow + lineIndex | 0;
|
|
632
|
-
let baseCol = box.bounds.left + 1 | 0;
|
|
633
|
-
let basePosition = Types.Position.make(row, baseCol);
|
|
654
|
+
let row$1 = contentStartRow + lineIndex | 0;
|
|
655
|
+
let baseCol$1 = box.bounds.left + 1 | 0;
|
|
656
|
+
let basePosition = Types.Position.make(row$1, baseCol$1);
|
|
634
657
|
let segments = splitInlineSegments(trimmed);
|
|
635
658
|
if (segments.length > 1) {
|
|
636
|
-
let rowChildren = segments.map(segment => segmentToElement(segment, basePosition, baseCol, box.bounds));
|
|
659
|
+
let rowChildren = segments.map(segment => segmentToElement(segment, basePosition, baseCol$1, box.bounds));
|
|
637
660
|
return {
|
|
638
661
|
TAG: "Row",
|
|
639
662
|
children: rowChildren,
|
|
@@ -649,24 +672,24 @@ function parseContentLine(line, lineIndex, contentStartRow, box, registry) {
|
|
|
649
672
|
case "TextSegment" :
|
|
650
673
|
break;
|
|
651
674
|
case "ButtonSegment" :
|
|
652
|
-
let actualCol = (baseCol + leadingSpaces | 0) + match._1 | 0;
|
|
653
|
-
let position = Types.Position.make(row, actualCol);
|
|
675
|
+
let actualCol = (baseCol$1 + leadingSpaces | 0) + match._1 | 0;
|
|
676
|
+
let position$1 = Types.Position.make(row$1, actualCol);
|
|
654
677
|
let buttonContent = "[ " + match._0 + " ]";
|
|
655
|
-
return ParserRegistry.parse(registry, buttonContent, position, box.bounds);
|
|
678
|
+
return ParserRegistry.parse(registry, buttonContent, position$1, box.bounds);
|
|
656
679
|
case "LinkSegment" :
|
|
657
|
-
let actualCol$1 = (baseCol + leadingSpaces | 0) + match._1 | 0;
|
|
658
|
-
let position$
|
|
680
|
+
let actualCol$1 = (baseCol$1 + leadingSpaces | 0) + match._1 | 0;
|
|
681
|
+
let position$2 = Types.Position.make(row$1, actualCol$1);
|
|
659
682
|
let linkContent = "\"" + match._0 + "\"";
|
|
660
|
-
return ParserRegistry.parse(registry, linkContent, position$
|
|
683
|
+
return ParserRegistry.parse(registry, linkContent, position$2, box.bounds);
|
|
661
684
|
}
|
|
662
685
|
}
|
|
663
|
-
let position$
|
|
664
|
-
return ParserRegistry.parse(registry, trimmed, position$
|
|
686
|
+
let position$3 = Types.Position.make(row$1, baseCol$1 + leadingSpaces | 0);
|
|
687
|
+
return ParserRegistry.parse(registry, trimmed, position$3, box.bounds);
|
|
665
688
|
}
|
|
666
689
|
let trimmedStart$1 = line.trimStart();
|
|
667
690
|
let leadingSpaces$1 = line.length - trimmedStart$1.length | 0;
|
|
668
|
-
let position$
|
|
669
|
-
return ParserRegistry.parse(registry, trimmed, position$
|
|
691
|
+
let position$4 = Types.Position.make(row$1, baseCol$1 + leadingSpaces$1 | 0);
|
|
692
|
+
return ParserRegistry.parse(registry, trimmed, position$4, box.bounds);
|
|
670
693
|
}
|
|
671
694
|
|
|
672
695
|
function parseBoxContent(box, gridCells, registry) {
|
|
@@ -831,6 +854,7 @@ function getElementRow(_elem) {
|
|
|
831
854
|
case "Box" :
|
|
832
855
|
return elem.bounds.top;
|
|
833
856
|
case "Divider" :
|
|
857
|
+
case "Spacer" :
|
|
834
858
|
return elem.position.row;
|
|
835
859
|
case "Row" :
|
|
836
860
|
let child = elem.children[0];
|
|
@@ -969,6 +993,7 @@ export {
|
|
|
969
993
|
isSectionFooter,
|
|
970
994
|
stripSectionBorders,
|
|
971
995
|
segmentToElement,
|
|
996
|
+
isNoiseText,
|
|
972
997
|
parseContentLine,
|
|
973
998
|
parseBoxContent,
|
|
974
999
|
getBoxColumn,
|
|
@@ -1049,9 +1049,29 @@ let segmentToElement = (
|
|
|
1049
1049
|
}
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
|
+
/**
|
|
1053
|
+
* Noise text filter - filters out box border characters.
|
|
1054
|
+
* This identifies text that is part of ASCII art borders, not actual content.
|
|
1055
|
+
* Examples: "|", "+---+", "===", etc.
|
|
1056
|
+
*/
|
|
1057
|
+
let isNoiseText = (content: string): bool => {
|
|
1058
|
+
let trimmed = content->String.trim
|
|
1059
|
+
if trimmed === "" {
|
|
1060
|
+
// Empty lines are not noise - they become Spacer elements
|
|
1061
|
+
false
|
|
1062
|
+
} else {
|
|
1063
|
+
// Box border patterns: +---+, |, ===, etc.
|
|
1064
|
+
let borderPattern = %re("/^[+|=\-\s]+$/")
|
|
1065
|
+
let hasPipeOrPlus = %re("/[+|]/")
|
|
1066
|
+
|
|
1067
|
+
Js.Re.test_(borderPattern, trimmed) || Js.Re.test_(hasPipeOrPlus, trimmed)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1052
1071
|
/**
|
|
1053
1072
|
* Parse a single content line into an element.
|
|
1054
1073
|
* Handles buttons, links, inline elements, and regular text.
|
|
1074
|
+
* Filters out noise (border characters) and creates Spacer for empty lines.
|
|
1055
1075
|
*/
|
|
1056
1076
|
let parseContentLine = (
|
|
1057
1077
|
line: string,
|
|
@@ -1062,8 +1082,14 @@ let parseContentLine = (
|
|
|
1062
1082
|
): option<element> => {
|
|
1063
1083
|
let trimmed = line->String.trim
|
|
1064
1084
|
|
|
1065
|
-
//
|
|
1085
|
+
// Issue #16: Preserve empty lines as Spacer elements for vertical spacing
|
|
1066
1086
|
if trimmed === "" {
|
|
1087
|
+
let row = contentStartRow + lineIndex
|
|
1088
|
+
let baseCol = box.bounds.left + 1
|
|
1089
|
+
let position = Position.make(row, baseCol)
|
|
1090
|
+
Some(Spacer({position: position}))
|
|
1091
|
+
} else if isNoiseText(trimmed) {
|
|
1092
|
+
// Filter out border/noise text - don't create any element
|
|
1067
1093
|
None
|
|
1068
1094
|
} else {
|
|
1069
1095
|
// Calculate position in grid
|
|
@@ -1349,6 +1375,7 @@ let rec getElementRow = (elem: element): int => {
|
|
|
1349
1375
|
| Checkbox({position, _}) => position.row
|
|
1350
1376
|
| Text({position, _}) => position.row
|
|
1351
1377
|
| Divider({position}) => position.row
|
|
1378
|
+
| Spacer({position}) => position.row
|
|
1352
1379
|
| Row({children, _}) => {
|
|
1353
1380
|
// Use the first child's row position
|
|
1354
1381
|
switch children->Array.get(0) {
|
|
@@ -42,6 +42,7 @@ let defaultStyles = `
|
|
|
42
42
|
.wf-row .wf-link { display:inline; margin:0 8px; }
|
|
43
43
|
.wf-text { margin:4px 0; line-height:1.4; }
|
|
44
44
|
.wf-text.emphasis { font-weight:bold; }
|
|
45
|
+
.wf-spacer { min-height:1em; }
|
|
45
46
|
.wf-divider { border:none; border-top:1px solid #333; margin:12px 0; }
|
|
46
47
|
.wf-section { border:1px solid #333; margin:8px 0; }
|
|
47
48
|
.wf-section-header { background:#fff; padding:4px 8px; font-size:12px; color:#666; border-bottom:1px solid #333; }
|
|
@@ -55,20 +56,6 @@ let defaultStyles = `
|
|
|
55
56
|
.wf-button.align-right, .wf-link.align-right { margin-left:auto; margin-right:0; }
|
|
56
57
|
`;
|
|
57
58
|
|
|
58
|
-
function isNoiseText(content) {
|
|
59
|
-
let trimmed = content.trim();
|
|
60
|
-
if (trimmed === "") {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
let borderPattern = /^[+|=\-\s]+$/;
|
|
64
|
-
let hasPipeOrPlus = /[+|]/;
|
|
65
|
-
if (borderPattern.test(trimmed)) {
|
|
66
|
-
return true;
|
|
67
|
-
} else {
|
|
68
|
-
return hasPipeOrPlus.test(trimmed);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
59
|
function isInputOnlyBox(elem) {
|
|
73
60
|
if (elem.TAG !== "Box") {
|
|
74
61
|
return false;
|
|
@@ -253,22 +240,22 @@ function renderElement(_elem, onAction, onDeadEnd) {
|
|
|
253
240
|
labelEl.appendChild(span);
|
|
254
241
|
return Primitive_option.some(labelEl);
|
|
255
242
|
case "Text" :
|
|
256
|
-
let content = elem.content;
|
|
257
|
-
if (isNoiseText(content)) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
243
|
let p = document.createElement("p");
|
|
261
244
|
p.className = "wf-text";
|
|
262
245
|
if (elem.emphasis) {
|
|
263
246
|
p.classList.add("emphasis");
|
|
264
247
|
}
|
|
265
248
|
applyAlignment(p, elem.align);
|
|
266
|
-
p.textContent = content;
|
|
249
|
+
p.textContent = elem.content;
|
|
267
250
|
return Primitive_option.some(p);
|
|
268
251
|
case "Divider" :
|
|
269
252
|
let hr = document.createElement("hr");
|
|
270
253
|
hr.className = "wf-divider";
|
|
271
254
|
return Primitive_option.some(hr);
|
|
255
|
+
case "Spacer" :
|
|
256
|
+
let spacer = document.createElement("div");
|
|
257
|
+
spacer.className = "wf-spacer";
|
|
258
|
+
return Primitive_option.some(spacer);
|
|
272
259
|
case "Row" :
|
|
273
260
|
let row = document.createElement("div");
|
|
274
261
|
row.className = "wf-row";
|
|
@@ -574,7 +561,6 @@ export {
|
|
|
574
561
|
DomBindings,
|
|
575
562
|
defaultOptions,
|
|
576
563
|
defaultStyles,
|
|
577
|
-
isNoiseText,
|
|
578
564
|
isInputOnlyBox,
|
|
579
565
|
getInputsFromBox,
|
|
580
566
|
alignmentToClass,
|
|
@@ -154,6 +154,7 @@ let defaultStyles = `
|
|
|
154
154
|
.wf-row .wf-link { display:inline; margin:0 8px; }
|
|
155
155
|
.wf-text { margin:4px 0; line-height:1.4; }
|
|
156
156
|
.wf-text.emphasis { font-weight:bold; }
|
|
157
|
+
.wf-spacer { min-height:1em; }
|
|
157
158
|
.wf-divider { border:none; border-top:1px solid #333; margin:12px 0; }
|
|
158
159
|
.wf-section { border:1px solid #333; margin:8px 0; }
|
|
159
160
|
.wf-section-header { background:#fff; padding:4px 8px; font-size:12px; color:#666; border-bottom:1px solid #333; }
|
|
@@ -171,23 +172,6 @@ let defaultStyles = `
|
|
|
171
172
|
// Helper Functions
|
|
172
173
|
// ============================================================================
|
|
173
174
|
|
|
174
|
-
// Noise text filter - filters out box border characters
|
|
175
|
-
// Note: Empty lines are NOT noise - they represent intentional vertical spacing (Issue #16)
|
|
176
|
-
let isNoiseText = (content: string): bool => {
|
|
177
|
-
let trimmed = content->String.trim
|
|
178
|
-
if trimmed == "" {
|
|
179
|
-
// Empty lines should be preserved as vertical spacing
|
|
180
|
-
false
|
|
181
|
-
} else {
|
|
182
|
-
// Box border patterns: +---+, |, ===, etc.
|
|
183
|
-
let borderPattern = %re("/^[+|=\-\s]+$/")
|
|
184
|
-
let hasPipeOrPlus = %re("/[+|]/")
|
|
185
|
-
|
|
186
|
-
Js.Re.test_(borderPattern, trimmed) ||
|
|
187
|
-
Js.Re.test_(hasPipeOrPlus, trimmed)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
175
|
// Check if a box contains only inputs (should unwrap and render as inputs directly)
|
|
192
176
|
let isInputOnlyBox = (elem: element): bool => {
|
|
193
177
|
switch elem {
|
|
@@ -425,19 +409,15 @@ let rec renderElement = (
|
|
|
425
409
|
}
|
|
426
410
|
|
|
427
411
|
| Text({content, emphasis, align, _}) => {
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
p->DomBindings.setClassName("wf-text")
|
|
434
|
-
if emphasis {
|
|
435
|
-
p->DomBindings.classList->DomBindings.add("emphasis")
|
|
436
|
-
}
|
|
437
|
-
applyAlignment(p, align)
|
|
438
|
-
p->DomBindings.setTextContent(content)
|
|
439
|
-
Some(p)
|
|
412
|
+
// Note: Noise filtering is now done in SemanticParser
|
|
413
|
+
let p = DomBindings.document->DomBindings.createElement("p")
|
|
414
|
+
p->DomBindings.setClassName("wf-text")
|
|
415
|
+
if emphasis {
|
|
416
|
+
p->DomBindings.classList->DomBindings.add("emphasis")
|
|
440
417
|
}
|
|
418
|
+
applyAlignment(p, align)
|
|
419
|
+
p->DomBindings.setTextContent(content)
|
|
420
|
+
Some(p)
|
|
441
421
|
}
|
|
442
422
|
|
|
443
423
|
| Divider(_) => {
|
|
@@ -446,6 +426,12 @@ let rec renderElement = (
|
|
|
446
426
|
Some(hr)
|
|
447
427
|
}
|
|
448
428
|
|
|
429
|
+
| Spacer(_) => {
|
|
430
|
+
let spacer = DomBindings.document->DomBindings.createElement("div")
|
|
431
|
+
spacer->DomBindings.setClassName("wf-spacer")
|
|
432
|
+
Some(spacer)
|
|
433
|
+
}
|
|
434
|
+
|
|
449
435
|
| Row({children, align}) => {
|
|
450
436
|
let row = DomBindings.document->DomBindings.createElement("div")
|
|
451
437
|
row->DomBindings.setClassName("wf-row")
|