wyreframe 0.7.0 → 0.7.3
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 +1 -1
- 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/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 -27
package/README.md
CHANGED
|
@@ -344,7 +344,7 @@ if (result.success) {
|
|
|
344
344
|
- [Examples](docs/examples.md)
|
|
345
345
|
- [Developer Guide](docs/developer-guide.md)
|
|
346
346
|
- [Testing Guide](docs/testing.md)
|
|
347
|
-
- [Live Demo](
|
|
347
|
+
- [Live Demo](https://wyreframe.studio/)
|
|
348
348
|
|
|
349
349
|
## Development
|
|
350
350
|
|
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
|
}
|
|
@@ -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 true;
|
|
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,21 +172,6 @@ let defaultStyles = `
|
|
|
171
172
|
// Helper Functions
|
|
172
173
|
// ============================================================================
|
|
173
174
|
|
|
174
|
-
// Noise text filter - filters out box border characters
|
|
175
|
-
let isNoiseText = (content: string): bool => {
|
|
176
|
-
let trimmed = content->String.trim
|
|
177
|
-
if trimmed == "" {
|
|
178
|
-
true
|
|
179
|
-
} else {
|
|
180
|
-
// Box border patterns: +---+, |, ===, etc.
|
|
181
|
-
let borderPattern = %re("/^[+|=\-\s]+$/")
|
|
182
|
-
let hasPipeOrPlus = %re("/[+|]/")
|
|
183
|
-
|
|
184
|
-
Js.Re.test_(borderPattern, trimmed) ||
|
|
185
|
-
Js.Re.test_(hasPipeOrPlus, trimmed)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
175
|
// Check if a box contains only inputs (should unwrap and render as inputs directly)
|
|
190
176
|
let isInputOnlyBox = (elem: element): bool => {
|
|
191
177
|
switch elem {
|
|
@@ -423,19 +409,15 @@ let rec renderElement = (
|
|
|
423
409
|
}
|
|
424
410
|
|
|
425
411
|
| Text({content, emphasis, align, _}) => {
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
p->DomBindings.setClassName("wf-text")
|
|
432
|
-
if emphasis {
|
|
433
|
-
p->DomBindings.classList->DomBindings.add("emphasis")
|
|
434
|
-
}
|
|
435
|
-
applyAlignment(p, align)
|
|
436
|
-
p->DomBindings.setTextContent(content)
|
|
437
|
-
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")
|
|
438
417
|
}
|
|
418
|
+
applyAlignment(p, align)
|
|
419
|
+
p->DomBindings.setTextContent(content)
|
|
420
|
+
Some(p)
|
|
439
421
|
}
|
|
440
422
|
|
|
441
423
|
| Divider(_) => {
|
|
@@ -444,6 +426,12 @@ let rec renderElement = (
|
|
|
444
426
|
Some(hr)
|
|
445
427
|
}
|
|
446
428
|
|
|
429
|
+
| Spacer(_) => {
|
|
430
|
+
let spacer = DomBindings.document->DomBindings.createElement("div")
|
|
431
|
+
spacer->DomBindings.setClassName("wf-spacer")
|
|
432
|
+
Some(spacer)
|
|
433
|
+
}
|
|
434
|
+
|
|
447
435
|
| Row({children, align}) => {
|
|
448
436
|
let row = DomBindings.document->DomBindings.createElement("div")
|
|
449
437
|
row->DomBindings.setClassName("wf-row")
|