Skip to content

Commit 9c338a8

Browse files
authored
fix: preserve YAML literal block scalars without backslash escaping (#1626)
* fix: preserve YAML literal block scalars without backslash escaping * chore: add changeset
1 parent 9ba5ed2 commit 9c338a8

3 files changed

Lines changed: 256 additions & 0 deletions

File tree

.changeset/cold-jobs-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": patch
3+
---
4+
5+
preserve YAML literal block scalars without backslash escaping
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { describe, expect, it } from "vitest";
2+
import createYamlLoader from "./yaml";
3+
4+
describe("yaml loader", () => {
5+
it("pull should parse valid YAML format", async () => {
6+
const loader = createYamlLoader();
7+
loader.setDefaultLocale("en");
8+
const yamlInput = `hello: Hello
9+
world: World
10+
nested:
11+
key: value`;
12+
13+
const result = await loader.pull("en", yamlInput);
14+
expect(result).toEqual({
15+
hello: "Hello",
16+
world: "World",
17+
nested: {
18+
key: "value",
19+
},
20+
});
21+
});
22+
23+
it("pull should handle empty input", async () => {
24+
const loader = createYamlLoader();
25+
loader.setDefaultLocale("en");
26+
const result = await loader.pull("en", "");
27+
expect(result).toEqual({});
28+
});
29+
30+
it("pull should parse YAML with literal block scalars", async () => {
31+
const loader = createYamlLoader();
32+
loader.setDefaultLocale("en");
33+
const yamlInput = `description: |
34+
This is a multi-line
35+
description with
36+
- bullet points
37+
- and more items`;
38+
39+
const result = await loader.pull("en", yamlInput);
40+
expect(result).toEqual({
41+
description:
42+
"This is a multi-line\ndescription with\n- bullet points\n- and more items\n",
43+
});
44+
});
45+
46+
it("push should preserve literal block scalar format when input uses it", async () => {
47+
const loader = createYamlLoader();
48+
loader.setDefaultLocale("en");
49+
50+
// Input with literal block scalar
51+
const yamlInput = `system_prompt: |
52+
You are a compliance expert.
53+
Standard Rules:
54+
1. **Rule One**
55+
- Do not make guarantees
56+
- Flag phrases: "guaranteed returns"
57+
2. **Rule Two**
58+
- Past performance disclaimer`;
59+
60+
await loader.pull("en", yamlInput);
61+
62+
const data = {
63+
system_prompt: `You are a compliance expert.
64+
Standard Rules:
65+
1. **Rule One**
66+
- Do not make guarantees
67+
- Flag phrases: "guaranteed returns"
68+
2. **Rule Two**
69+
- Past performance disclaimer`,
70+
};
71+
72+
const result = await loader.push("en", data, yamlInput);
73+
74+
// Should NOT contain backslash escaping before dashes
75+
expect(result).not.toContain("\\ -");
76+
77+
// Should use literal block scalar format (|- or |)
78+
expect(result).toMatch(/system_prompt:\s*\|[-+]?/);
79+
80+
// Verify the content is correctly formatted
81+
expect(result).toContain(" - Do not make guarantees");
82+
expect(result).toContain(" - Flag phrases:");
83+
expect(result).toContain(" - Past performance disclaimer");
84+
});
85+
86+
it("push should NOT use backslash escaping for indented dashes with literal block scalars", async () => {
87+
const loader = createYamlLoader();
88+
loader.setDefaultLocale("en");
89+
90+
// Original input uses literal block scalar
91+
const originalInput = `content: |
92+
List of items:
93+
- First item
94+
- Second item
95+
- Nested item`;
96+
97+
await loader.pull("en", originalInput);
98+
99+
const data = {
100+
content: `List of items:
101+
- First item
102+
- Second item
103+
- Nested item`,
104+
};
105+
106+
const result = await loader.push("en", data, originalInput);
107+
108+
// Critical: Should NOT have backslash escaping
109+
expect(result).not.toMatch(/^\s*\\\s+-/m);
110+
expect(result).not.toContain("\\ -");
111+
112+
// Should preserve literal block scalar
113+
expect(result).toMatch(/content:\s*\|[-+]?/);
114+
});
115+
116+
it("push should use QUOTE_DOUBLE when original input has quoted strings", async () => {
117+
const loader = createYamlLoader();
118+
loader.setDefaultLocale("en");
119+
120+
// Input with double-quoted strings (no literal block scalars)
121+
const yamlInput = `"hello": "Hello World"
122+
"world": "Another string"`;
123+
124+
await loader.pull("en", yamlInput);
125+
126+
const data = {
127+
hello: "Hello World",
128+
world: "Another string",
129+
};
130+
131+
const result = await loader.push("en", data, yamlInput);
132+
133+
// Should use double quotes
134+
expect(result).toContain('"hello"');
135+
expect(result).toContain('"Hello World"');
136+
});
137+
138+
it("push should default to PLAIN format when no style indicators", async () => {
139+
const loader = createYamlLoader();
140+
loader.setDefaultLocale("en");
141+
142+
const yamlInput = `hello: Hello World
143+
world: Another string`;
144+
145+
await loader.pull("en", yamlInput);
146+
147+
const data = {
148+
hello: "Hello World",
149+
world: "Another string",
150+
};
151+
152+
const result = await loader.push("en", data, yamlInput);
153+
154+
// Should use plain format (no quotes, no literal scalars for simple strings)
155+
expect(result).toContain("hello: Hello World");
156+
expect(result).toContain("world: Another string");
157+
});
158+
159+
it("push should handle complex content with literal block scalars and quotes", async () => {
160+
const loader = createYamlLoader();
161+
loader.setDefaultLocale("en");
162+
163+
// Input that has both literal block scalars and embedded quotes
164+
const yamlInput = `system_prompt: |
165+
Rules:
166+
1. **Rule**
167+
- Do not use "guaranteed" language
168+
- Flag: "no risk"
169+
user_prompt: Simple text`;
170+
171+
await loader.pull("en", yamlInput);
172+
173+
const data = {
174+
system_prompt: `Rules:
175+
1. **Rule**
176+
- Do not use "guaranteed" language
177+
- Flag: "no risk"`,
178+
user_prompt: "Simple text",
179+
};
180+
181+
const result = await loader.push("en", data, yamlInput);
182+
183+
// Should detect literal block scalar and use PLAIN format
184+
expect(result).toMatch(/system_prompt:\s*\|[-+]?/);
185+
186+
// Should NOT have backslash escaping
187+
expect(result).not.toContain("\\ -");
188+
189+
// Embedded quotes should be preserved in the content
190+
expect(result).toContain('"guaranteed"');
191+
expect(result).toContain('"no risk"');
192+
});
193+
194+
it("pull should handle YAML with comments", async () => {
195+
const loader = createYamlLoader();
196+
loader.setDefaultLocale("en");
197+
const yamlInput = `# This is a comment
198+
hello: Hello # inline comment
199+
world: World`;
200+
201+
const result = await loader.pull("en", yamlInput);
202+
expect(result).toEqual({
203+
hello: "Hello",
204+
world: "World",
205+
});
206+
});
207+
208+
it("push should handle empty object", async () => {
209+
const loader = createYamlLoader();
210+
loader.setDefaultLocale("en");
211+
await loader.pull("en", "{}");
212+
213+
const result = await loader.push("en", {});
214+
expect(result.trim()).toBe("{}");
215+
});
216+
217+
it("push should handle arrays", async () => {
218+
const loader = createYamlLoader();
219+
loader.setDefaultLocale("en");
220+
221+
const yamlInput = `items:
222+
- first
223+
- second
224+
- third`;
225+
226+
await loader.pull("en", yamlInput);
227+
228+
const data = {
229+
items: ["first", "second", "third"],
230+
};
231+
232+
const result = await loader.push("en", data, yamlInput);
233+
234+
// Verify it parses back correctly
235+
const reparsed = await loader.pull("en", result);
236+
expect(reparsed).toEqual(data);
237+
});
238+
});

packages/cli/src/cli/loaders/yaml.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ function getStringType(
4242
): ToStringOptions["defaultStringType"] {
4343
if (yamlString) {
4444
const lines = yamlString.split("\n");
45+
46+
// Check if the file uses literal block scalars (|, |-, |+)
47+
const hasLiteralBlockScalar = lines.find((line) => {
48+
const trimmedLine = line.trim();
49+
return trimmedLine.match(/:\s*\|[-+]?\s*$/);
50+
});
51+
52+
// If literal block scalars are used, always use PLAIN to preserve them
53+
if (hasLiteralBlockScalar) {
54+
return "PLAIN";
55+
}
56+
57+
// Otherwise, check for double quotes on string values
4558
const hasDoubleQuotes = lines.find((line) => {
4659
const trimmedLine = line.trim();
4760
return (

0 commit comments

Comments
 (0)