diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index 1d9fe8db3ba..3061fc3e0c2 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -163,3 +163,4 @@ All changes included in 1.9: - ([#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. - ([#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. - ([#13935](https://github.com/quarto-dev/quarto-cli/issues/13935)): Fix `quarto install`, `quarto update`, and `quarto uninstall` interactive tool selection. +- ([#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". diff --git a/src/core/lib/ranged-text.ts b/src/core/lib/ranged-text.ts index d9c93849707..e84e9912e22 100644 --- a/src/core/lib/ranged-text.ts +++ b/src/core/lib/ranged-text.ts @@ -40,7 +40,7 @@ export function rangedLines( text: string, includeNewLines = false, ): RangedSubstring[] { - const regex = /\r?\n/g; + const regex = /\r\n?|\n/g; const result: RangedSubstring[] = []; let startOffset = 0; diff --git a/src/core/lib/text.ts b/src/core/lib/text.ts index 7bdeda461aa..eec3f931633 100644 --- a/src/core/lib/text.ts +++ b/src/core/lib/text.ts @@ -9,7 +9,7 @@ import { InternalError } from "./error.ts"; import { quotedStringColor } from "./errors.ts"; export function lines(text: string): string[] { - return text.split(/\r?\n/); + return text.split(/\r\n?|\n/); } export function normalizeNewlines(text: string) { @@ -62,13 +62,13 @@ export function* matchAll(text: string, regexp: RegExp) { export function* lineOffsets(text: string) { yield 0; - for (const match of matchAll(text, /\r?\n/g)) { + for (const match of matchAll(text, /\r\n?|\n/g)) { yield match.index + match[0].length; } } export function* lineBreakPositions(text: string) { - for (const match of matchAll(text, /\r?\n/g)) { + for (const match of matchAll(text, /\r\n?|\n/g)) { yield match.index; } } diff --git a/tests/smoke/yaml/yaml-cr-line-endings.test.ts b/tests/smoke/yaml/yaml-cr-line-endings.test.ts new file mode 100644 index 00000000000..43ea27c4638 --- /dev/null +++ b/tests/smoke/yaml/yaml-cr-line-endings.test.ts @@ -0,0 +1,24 @@ +/* +* yaml-cr-line-endings.test.ts +* +* Test YAML validation with CR-only line endings (old Mac format) +* See: https://github.com/quarto-dev/quarto-cli/issues/13998 +* +* Copyright (C) 2025 Posit Software, PBC +*/ + +import { testRender } from "../render/render.ts"; +import { noErrorsOrWarnings } from "../../verify.ts"; +import { join } from "../../../src/deno_ral/path.ts"; + +// Create test file with CR-only line endings programmatically +const dir = Deno.makeTempDirSync({ prefix: "quarto-cr-test-" }); +const crContent = "---\rtitle: \"CR Test\"\rauthor: \"Test Author\"\r---\r\rContent here.\r"; +const inputFile = join(dir, "cr-only.qmd"); +Deno.writeFileSync(inputFile, new TextEncoder().encode(crContent)); + +testRender(inputFile, "html", false, [noErrorsOrWarnings], { + teardown: async () => { + Deno.removeSync(dir, { recursive: true }); + }, +}); diff --git a/tests/unit/core/lib/text.test.ts b/tests/unit/core/lib/text.test.ts index a75eeeb2b97..792a95c8874 100644 --- a/tests/unit/core/lib/text.test.ts +++ b/tests/unit/core/lib/text.test.ts @@ -6,7 +6,7 @@ import { unitTest } from "../../../test.ts"; import { assertEquals } from "testing/asserts"; -import { getEndingNewlineCount } from "../../../../src/core/lib/text.ts"; +import { getEndingNewlineCount, lines } from "../../../../src/core/lib/text.ts"; unitTest("core/lib/text.ts - getEndingNewlineCount", async () => { // Test case 1: No trailing newlines @@ -64,3 +64,34 @@ unitTest("core/lib/text.ts - getEndingNewlineCount", async () => { 3, ); }); + +// Test for lines() function with different line endings +// See: https://github.com/quarto-dev/quarto-cli/issues/13998 +unitTest("core/lib/text.ts - lines() with different line endings", async () => { + // LF (Unix/Linux) + assertEquals(lines("a\nb\nc"), ["a", "b", "c"]); + + // CRLF (Windows) + assertEquals(lines("a\r\nb\r\nc"), ["a", "b", "c"]); + + // CR-only (old Mac) - the fix for #13998 + assertEquals(lines("a\rb\rc"), ["a", "b", "c"]); + + // Mixed endings + assertEquals(lines("a\rb\nc\r\nd"), ["a", "b", "c", "d"]); + + // YAML front matter with CR-only + const yaml = "---\rtitle: \"Test\"\r---"; + assertEquals(lines(yaml), ["---", "title: \"Test\"", "---"]); + + // Empty string + assertEquals(lines(""), [""]); + + // Single line without newline + assertEquals(lines("single"), ["single"]); + + // Trailing newlines + assertEquals(lines("a\n"), ["a", ""]); + assertEquals(lines("a\r"), ["a", ""]); + assertEquals(lines("a\r\n"), ["a", ""]); +});