Skip to content

Commit 762a4c3

Browse files
cdervclaude
andauthored
Fix YAML validation with CR-only line endings (#13999)
The regex `/\r?\n/g` for line splitting matched LF and CRLF but not standalone CR (old Mac format). Files with CR-only line endings were treated as a single line, causing YAML validation to fail. Changed regex to `/\r\n?|\n/g` in all 4 locations that split lines. Fixes #13998 Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 54c7493 commit 762a4c3

5 files changed

Lines changed: 61 additions & 5 deletions

File tree

news/changelog-1.9.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,4 @@ All changes included in 1.9:
163163
- ([#13890](https://github.com/quarto-dev/quarto-cli/issues/13890)): Fix render failure when using `embed-resources: true` with input path through a symlinked directory. The cleanup now resolves symlinks before comparing paths.
164164
- ([#13907](https://github.com/quarto-dev/quarto-cli/issues/13907)): Ignore AI assistant configuration files (`CLAUDE.md`, `AGENTS.md`) when scanning for project input files and in extension templates, similar to how `README.md` is handled.
165165
- ([#13935](https://github.com/quarto-dev/quarto-cli/issues/13935)): Fix `quarto install`, `quarto update`, and `quarto uninstall` interactive tool selection.
166+
- ([#13998](https://github.com/quarto-dev/quarto-cli/issues/13998)): Fix YAML validation error with CR-only line terminators (old Mac format). Documents using `\r` line endings no longer fail with "Expected YAML front matter to contain at least 2 lines".

src/core/lib/ranged-text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function rangedLines(
4040
text: string,
4141
includeNewLines = false,
4242
): RangedSubstring[] {
43-
const regex = /\r?\n/g;
43+
const regex = /\r\n?|\n/g;
4444
const result: RangedSubstring[] = [];
4545

4646
let startOffset = 0;

src/core/lib/text.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { InternalError } from "./error.ts";
99
import { quotedStringColor } from "./errors.ts";
1010

1111
export function lines(text: string): string[] {
12-
return text.split(/\r?\n/);
12+
return text.split(/\r\n?|\n/);
1313
}
1414

1515
export function normalizeNewlines(text: string) {
@@ -62,13 +62,13 @@ export function* matchAll(text: string, regexp: RegExp) {
6262

6363
export function* lineOffsets(text: string) {
6464
yield 0;
65-
for (const match of matchAll(text, /\r?\n/g)) {
65+
for (const match of matchAll(text, /\r\n?|\n/g)) {
6666
yield match.index + match[0].length;
6767
}
6868
}
6969

7070
export function* lineBreakPositions(text: string) {
71-
for (const match of matchAll(text, /\r?\n/g)) {
71+
for (const match of matchAll(text, /\r\n?|\n/g)) {
7272
yield match.index;
7373
}
7474
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* yaml-cr-line-endings.test.ts
3+
*
4+
* Test YAML validation with CR-only line endings (old Mac format)
5+
* See: https://github.com/quarto-dev/quarto-cli/issues/13998
6+
*
7+
* Copyright (C) 2025 Posit Software, PBC
8+
*/
9+
10+
import { testRender } from "../render/render.ts";
11+
import { noErrorsOrWarnings } from "../../verify.ts";
12+
import { join } from "../../../src/deno_ral/path.ts";
13+
14+
// Create test file with CR-only line endings programmatically
15+
const dir = Deno.makeTempDirSync({ prefix: "quarto-cr-test-" });
16+
const crContent = "---\rtitle: \"CR Test\"\rauthor: \"Test Author\"\r---\r\rContent here.\r";
17+
const inputFile = join(dir, "cr-only.qmd");
18+
Deno.writeFileSync(inputFile, new TextEncoder().encode(crContent));
19+
20+
testRender(inputFile, "html", false, [noErrorsOrWarnings], {
21+
teardown: async () => {
22+
Deno.removeSync(dir, { recursive: true });
23+
},
24+
});

tests/unit/core/lib/text.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { unitTest } from "../../../test.ts";
88
import { assertEquals } from "testing/asserts";
9-
import { getEndingNewlineCount } from "../../../../src/core/lib/text.ts";
9+
import { getEndingNewlineCount, lines } from "../../../../src/core/lib/text.ts";
1010

1111
unitTest("core/lib/text.ts - getEndingNewlineCount", async () => {
1212
// Test case 1: No trailing newlines
@@ -64,3 +64,34 @@ unitTest("core/lib/text.ts - getEndingNewlineCount", async () => {
6464
3,
6565
);
6666
});
67+
68+
// Test for lines() function with different line endings
69+
// See: https://github.com/quarto-dev/quarto-cli/issues/13998
70+
unitTest("core/lib/text.ts - lines() with different line endings", async () => {
71+
// LF (Unix/Linux)
72+
assertEquals(lines("a\nb\nc"), ["a", "b", "c"]);
73+
74+
// CRLF (Windows)
75+
assertEquals(lines("a\r\nb\r\nc"), ["a", "b", "c"]);
76+
77+
// CR-only (old Mac) - the fix for #13998
78+
assertEquals(lines("a\rb\rc"), ["a", "b", "c"]);
79+
80+
// Mixed endings
81+
assertEquals(lines("a\rb\nc\r\nd"), ["a", "b", "c", "d"]);
82+
83+
// YAML front matter with CR-only
84+
const yaml = "---\rtitle: \"Test\"\r---";
85+
assertEquals(lines(yaml), ["---", "title: \"Test\"", "---"]);
86+
87+
// Empty string
88+
assertEquals(lines(""), [""]);
89+
90+
// Single line without newline
91+
assertEquals(lines("single"), ["single"]);
92+
93+
// Trailing newlines
94+
assertEquals(lines("a\n"), ["a", ""]);
95+
assertEquals(lines("a\r"), ["a", ""]);
96+
assertEquals(lines("a\r\n"), ["a", ""]);
97+
});

0 commit comments

Comments
 (0)