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 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](examples/index.html)
347
+ - [Live Demo](https://wyreframe.studio/)
348
348
 
349
349
  ## Development
350
350
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyreframe",
3
- "version": "0.7.0",
3
+ "version": "0.7.3",
4
4
  "description": "ASCII wireframe + interaction DSL to HTML converter with scene transitions",
5
5
  "author": "wickedev",
6
6
  "repository": {
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
- | Please "click here" to continue |
357
- +------------------------------------+
355
+ +---------------------------------+
356
+ | Please "click here" to continue |
357
+ +---------------------------------+
358
358
  `;
359
359
 
360
360
  const result = parse(wireframe);
@@ -402,6 +402,8 @@ function getElementType(elem) {
402
402
  return "Text";
403
403
  case "Divider" :
404
404
  return "Divider";
405
+ case "Spacer" :
406
+ return "Spacer";
405
407
  case "Row" :
406
408
  return "Row";
407
409
  case "Section" :
@@ -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$1 = Types.Position.make(row, actualCol$1);
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$1, box.bounds);
683
+ return ParserRegistry.parse(registry, linkContent, position$2, box.bounds);
661
684
  }
662
685
  }
663
- let position$2 = Types.Position.make(row, baseCol + leadingSpaces | 0);
664
- return ParserRegistry.parse(registry, trimmed, position$2, box.bounds);
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$3 = Types.Position.make(row, baseCol + leadingSpaces$1 | 0);
669
- return ParserRegistry.parse(registry, trimmed, position$3, box.bounds);
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
- // Skip empty lines
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
- // Filter noise text (box borders, etc.)
427
- if isNoiseText(content) {
428
- None
429
- } else {
430
- let p = DomBindings.document->DomBindings.createElement("p")
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")