Skip to content

Commit 68816e1

Browse files
wickedevclaude
andcommitted
fix: complete empty line preservation in wireframe rendering (#16)
Completes the fix for issue #16 by addressing two root causes: 1. Parser fix (SemanticParser.res): - Modified parseContentLine to create Text elements with empty content for empty lines instead of skipping them - Empty lines now generate proper spacer Text elements in the AST 2. Renderer fix (Renderer.res): - Modified Text element rendering to use &nbsp; for empty content - Added 'wf-spacer' CSS class for empty line styling - Empty lines now render with visible height in the UI Tests: - Added integration test verifying spacer elements are created (>=5) - Parser now creates 15 spacer elements from login wireframe fixture - All 619 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ce3e269 commit 68816e1

File tree

3 files changed

+119
-3
lines changed

3 files changed

+119
-3
lines changed

src/parser/Semantic/SemanticParser.res

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,9 +1062,20 @@ let parseContentLine = (
10621062
): option<element> => {
10631063
let trimmed = line->String.trim
10641064

1065-
// Skip empty lines
1065+
// Issue #16: Preserve empty lines as spacer elements for vertical spacing
10661066
if trimmed === "" {
1067-
None
1067+
// Calculate position for the empty line
1068+
let row = contentStartRow + lineIndex
1069+
let baseCol = box.bounds.left + 1
1070+
let position = Position.make(row, baseCol)
1071+
1072+
// Create a Text element with empty content to act as a vertical spacer
1073+
Some(Text({
1074+
content: "",
1075+
emphasis: false,
1076+
position: position,
1077+
align: Left,
1078+
}))
10681079
} else {
10691080
// Calculate position in grid
10701081
let row = contentStartRow + lineIndex

src/renderer/Renderer.res

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,14 @@ let rec renderElement = (
435435
p->DomBindings.classList->DomBindings.add("emphasis")
436436
}
437437
applyAlignment(p, align)
438-
p->DomBindings.setTextContent(content)
438+
// Issue #16: Empty lines should render with visible height as spacers
439+
let trimmed = content->String.trim
440+
if trimmed === "" {
441+
p->DomBindings.classList->DomBindings.add("wf-spacer")
442+
p->DomBindings.setInnerHTML("&nbsp;")
443+
} else {
444+
p->DomBindings.setTextContent(content)
445+
}
439446
Some(p)
440447
}
441448
}

src/renderer/__tests__/Renderer_test.res

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,104 @@ describe("Renderer", () => {
261261
})
262262
})
263263

264+
describe("Issue #16: Empty lines should render as spacer elements", () => {
265+
let loginWireframe = `
266+
+------------------------------------------+
267+
| |
268+
| 'Welcome Back' |
269+
| |
270+
| +----------------------------+ |
271+
| | #email | |
272+
| +----------------------------+ |
273+
| |
274+
| +----------------------------+ |
275+
| | #password | |
276+
| +----------------------------+ |
277+
| |
278+
| [ Sign In ] |
279+
| |
280+
| --- |
281+
| |
282+
| [ Continue with Google ] |
283+
| |
284+
| [ Continue with GitHub ] |
285+
| |
286+
| "Forgot password?" |
287+
| |
288+
+------------------------------------------+
289+
`
290+
291+
test("parses wireframe and includes spacer elements for empty lines", t => {
292+
let parseResult = Parser.parse(loginWireframe)
293+
294+
switch parseResult {
295+
| Ok((ast, _warnings)) => {
296+
// Get first scene's elements
297+
switch ast.scenes->Array.get(0) {
298+
| Some(scene) => {
299+
// Count all Text elements and empty text elements
300+
let (totalTextCount, spacerCount) =
301+
scene.elements->Array.reduce((0, 0), ((totalAcc, spacerAcc), elem) => {
302+
switch elem {
303+
| Box({children}) =>
304+
// Count text elements inside the box
305+
children->Array.reduce((totalAcc, spacerAcc), ((tAcc, sAcc), child) => {
306+
switch child {
307+
| Text({content}) => {
308+
let isSpacer = content->String.trim == ""
309+
(tAcc + 1, isSpacer ? sAcc + 1 : sAcc)
310+
}
311+
| _ => (tAcc, sAcc)
312+
}
313+
})
314+
| Text({content}) => {
315+
let isSpacer = content->String.trim == ""
316+
(totalAcc + 1, isSpacer ? spacerAcc + 1 : spacerAcc)
317+
}
318+
| _ => (totalAcc, spacerAcc)
319+
}
320+
})
321+
322+
// Log for debugging
323+
Console.log2("Total Text elements:", totalTextCount)
324+
Console.log2("Spacer (empty) elements:", spacerCount)
325+
326+
// The wireframe has multiple empty lines that should be preserved as spacers
327+
// Expecting at least 5 empty lines between content elements
328+
t->expect(spacerCount >= 5)->Expect.toBe(true)
329+
}
330+
| None => t->expect(true)->Expect.toBe(false) // Should have at least one scene
331+
}
332+
}
333+
| Error(_) => t->expect(true)->Expect.toBe(false) // Parsing should succeed
334+
}
335+
})
336+
337+
// Note: renderElement tests require DOM environment (jsdom)
338+
// These tests verify the logic using isNoiseText which is the filtering function
339+
test("isNoiseText returns false for empty content (spacers should not be filtered)", t => {
340+
// Empty content should NOT be considered noise
341+
t->expect(Renderer.isNoiseText(""))->Expect.toBe(false)
342+
})
343+
344+
test("isNoiseText returns false for whitespace content (spacers should not be filtered)", t => {
345+
// Whitespace-only content should NOT be considered noise
346+
t->expect(Renderer.isNoiseText(" "))->Expect.toBe(false)
347+
})
348+
349+
test("empty Text elements are preserved in rendering logic", t => {
350+
// Verify the isNoiseText check used in renderElement would preserve empty lines
351+
let emptyContent = ""
352+
let whitespaceContent = " "
353+
354+
// Both should return false (NOT noise = should be rendered)
355+
let emptyIsPreserved = !Renderer.isNoiseText(emptyContent)
356+
let whitespaceIsPreserved = !Renderer.isNoiseText(whitespaceContent)
357+
358+
t->expect(emptyIsPreserved && whitespaceIsPreserved)->Expect.toBe(true)
359+
})
360+
})
361+
264362
describe("navigation action edge cases", () => {
265363
test("Goto with condition is still a navigation action", t => {
266364
let action = Goto({

0 commit comments

Comments
 (0)