Skip to content

Commit bd9538a

Browse files
authored
fix(compiler): fix remove whitespaces from inside the element (#997)
1 parent 3bb58e3 commit bd9538a

File tree

3 files changed

+178
-13
lines changed

3 files changed

+178
-13
lines changed

.changeset/large-zoos-explain.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@lingo.dev/_compiler": minor
3+
---
4+
5+
### Whitespace Normalization Fix
6+
7+
- Improved `normalizeJsxWhitespace` logic to preserve leading spaces inside JSX elements while removing unnecessary formatting whitespace and extra lines.
8+
- Ensured explicit whitespace (e.g., `{" "}`) is handled correctly without introducing double spaces.
9+
- Added targeted tests (`jsx-content-whitespace.spec.ts`) to verify whitespace handling.
10+
- Cleaned up unnecessary debug/test files created during development.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { describe, it, expect } from "vitest";
2+
import { extractJsxContent } from "./jsx-content";
3+
import * as t from "@babel/types";
4+
import traverse, { NodePath } from "@babel/traverse";
5+
import { parse } from "@babel/parser";
6+
7+
describe("Whitespace Issue Test", () => {
8+
function parseJSX(code: string): t.File {
9+
return parse(code, {
10+
plugins: ["jsx"],
11+
sourceType: "module",
12+
});
13+
}
14+
15+
function getJSXElementPath(code: string): NodePath<t.JSXElement> {
16+
const ast = parseJSX(code);
17+
let result: NodePath<t.JSXElement>;
18+
19+
traverse(ast, {
20+
JSXElement(path) {
21+
result = path;
22+
path.stop();
23+
},
24+
});
25+
26+
return result!;
27+
}
28+
29+
it("should preserve leading space in nested elements", () => {
30+
const path = getJSXElementPath(`
31+
<h1 className="text-5xl md:text-7xl font-bold text-white mb-6 leading-tight">
32+
Hello World
33+
<span className="bg-gradient-to-r from-purple-400 via-pink-400 to-yellow-400 bg-clip-text text-transparent"> From Lingo.dev Compiler</span>
34+
</h1>
35+
`);
36+
37+
const content = extractJsxContent(path);
38+
console.log("Extracted content:", JSON.stringify(content));
39+
40+
// Let's also check the raw JSX structure to understand what's happening
41+
let jsxTexts: string[] = [];
42+
path.traverse({
43+
JSXText(textPath) {
44+
jsxTexts.push(JSON.stringify(textPath.node.value));
45+
},
46+
});
47+
console.log("JSXText nodes found:", jsxTexts);
48+
49+
// The span should have " From Lingo.dev Compiler" with the leading space
50+
expect(content).toContain(
51+
"<element:span> From Lingo.dev Compiler</element:span>",
52+
);
53+
});
54+
55+
it("should handle explicit whitespace correctly", () => {
56+
const path = getJSXElementPath(`
57+
<div>
58+
Hello{" "}
59+
<span> World</span>
60+
</div>
61+
`);
62+
63+
const content = extractJsxContent(path);
64+
console.log("Explicit whitespace test:", JSON.stringify(content));
65+
66+
// Should preserve both the explicit space and the leading space in span
67+
expect(content).toContain("Hello <element:span> World</element:span>");
68+
});
69+
70+
it("should preserve space before nested bold element like in HeroSubtitle", () => {
71+
const path = getJSXElementPath(`
72+
<p className="text-lg sm:text-xl text-gray-600 mb-10 max-w-xl mx-auto leading-relaxed">
73+
Localize your React app in every language in minutes. Scale to millions
74+
<b> from day one</b>.
75+
</p>
76+
`);
77+
78+
const content = extractJsxContent(path);
79+
console.log("HeroSubtitle test content:", JSON.stringify(content));
80+
81+
// Let's also check the raw JSX structure
82+
let jsxTexts: string[] = [];
83+
path.traverse({
84+
JSXText(textPath) {
85+
jsxTexts.push(JSON.stringify(textPath.node.value));
86+
},
87+
});
88+
console.log("HeroSubtitle JSXText nodes found:", jsxTexts);
89+
90+
// The bold element should have " from day one" with the leading space
91+
expect(content).toContain("<element:b> from day one</element:b>");
92+
// The full content should preserve the space between "millions" and the bold element
93+
expect(content).toContain("millions <element:b> from day one</element:b>");
94+
});
95+
});

packages/compiler/src/utils/jsx-content.ts

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,25 +126,85 @@ function isWhitespace(nodePath: NodePath<t.JSXExpressionContainer>) {
126126
}
127127

128128
function normalizeJsxWhitespace(input: string) {
129+
// Handle single-line content
130+
if (!input.includes("\n")) {
131+
// For single-line content, only trim if it appears to be formatting whitespace
132+
// (e.g., " hello world " should be trimmed to "hello world")
133+
// But preserve meaningful leading/trailing spaces (e.g., " hello" should stay " hello")
134+
135+
// If the content is mostly whitespace with some text, it's likely formatting
136+
const trimmed = input.trim();
137+
if (trimmed.length === 0) return "";
138+
139+
// Check if we have excessive whitespace (more than 1 space on each side)
140+
const leadingMatch = input.match(/^\s*/);
141+
const trailingMatch = input.match(/\s*$/);
142+
const leadingSpaces = leadingMatch ? leadingMatch[0].length : 0;
143+
const trailingSpaces = trailingMatch ? trailingMatch[0].length : 0;
144+
145+
if (leadingSpaces > 1 || trailingSpaces > 1) {
146+
// This looks like formatting whitespace, collapse it
147+
return input.replace(/\s+/g, " ").trim();
148+
} else {
149+
// This looks like meaningful whitespace, preserve it but collapse internal spaces
150+
return input.replace(/\s{2,}/g, " ");
151+
}
152+
}
153+
154+
// Handle multi-line content
129155
const lines = input.split("\n");
130156
let result = "";
157+
131158
for (let i = 0; i < lines.length; i++) {
132-
const line = lines[i].trim();
133-
if (line === "") continue;
134-
if (
135-
i > 0 &&
136-
(line.startsWith("<element:") ||
137-
line.startsWith("<function:") ||
138-
line.startsWith("{") ||
139-
line.startsWith("<expression/>"))
159+
const line = lines[i];
160+
const trimmedLine = line.trim();
161+
162+
// Skip empty lines
163+
if (trimmedLine === "") continue;
164+
165+
// Check if this line contains a placeholder (explicit whitespace)
166+
if (trimmedLine.includes(WHITESPACE_PLACEHOLDER)) {
167+
// For lines with placeholders, preserve the original spacing
168+
result += trimmedLine;
169+
} else if (
170+
trimmedLine.startsWith("<element:") ||
171+
trimmedLine.startsWith("<function:") ||
172+
trimmedLine.startsWith("{") ||
173+
trimmedLine.startsWith("<expression/>")
140174
) {
141-
result += line;
175+
// When we encounter an element/function/expression
176+
// Add space only when:
177+
// 1. We have existing content AND
178+
// 2. Result doesn't already end with space or placeholder AND
179+
// 3. The result ends with a word character (indicating text) AND
180+
// 4. The element content starts with a space (indicating word continuation)
181+
const shouldAddSpace =
182+
result &&
183+
!result.endsWith(" ") &&
184+
!result.endsWith(WHITESPACE_PLACEHOLDER) &&
185+
/\w$/.test(result) &&
186+
// Check if element content starts with space by looking for "> " pattern
187+
trimmedLine.includes("> ");
188+
189+
if (shouldAddSpace) {
190+
result += " ";
191+
}
192+
result += trimmedLine;
142193
} else {
143-
if (result && !result.endsWith(" ")) result += " ";
144-
result += line;
194+
// For regular text content, ensure proper spacing
195+
// Only add space if the result doesn't already end with a space or placeholder
196+
if (
197+
result &&
198+
!result.endsWith(" ") &&
199+
!result.endsWith(WHITESPACE_PLACEHOLDER)
200+
) {
201+
result += " ";
202+
}
203+
result += trimmedLine;
145204
}
146205
}
147-
// Collapse multiple spaces
148-
result = result.replace(/\s+/g, " ");
206+
207+
// Collapse multiple spaces but preserve single spaces around placeholders
208+
result = result.replace(/\s{2,}/g, " ");
149209
return result.trim();
150210
}

0 commit comments

Comments
 (0)