Skip to content

Commit 1975167

Browse files
authored
Merge pull request #12417 from quarto-dev/bugfix/12318
convert - ensure enough linebreaks between cells.
2 parents 1b82bf3 + 377e669 commit 1975167

File tree

5 files changed

+142
-0
lines changed

5 files changed

+142
-0
lines changed

news/changelog-1.7.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ All changes included in 1.7:
4040
## `quarto convert`
4141

4242
- ([#12042](https://github.com/quarto-dev/quarto-cli/issues/12042)): Preserve Markdown content that follows YAML metadata in a `raw` .ipynb cell.
43+
- ([#12318](https://github.com/quarto-dev/quarto-cli/issues/12318)): Ensure enough line breaks between cells that might be trimmed.
4344

4445
## `quarto inspect`
4546

src/command/convert/jupyter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
jupyterCellSrcAsStr,
3232
} from "../../core/jupyter/jupyter-shared.ts";
3333
import { assert } from "testing/asserts";
34+
import { getEndingNewlineCount } from "../../core/lib/text.ts";
3435

3536
export async function markdownToJupyterNotebook(
3637
file: string,
@@ -80,9 +81,17 @@ export async function jupyterNotebookToMarkdown(
8081
cell,
8182
);
8283

84+
const endingNewLineCount = getEndingNewlineCount(md);
85+
if (i > 0 && endingNewLineCount < 2) {
86+
md.push("\n\n");
87+
}
88+
8389
// write markdown
8490
switch (cell.cell_type) {
8591
case "markdown":
92+
// does the previous line have enough newlines?
93+
// if not, add sufficient newlines so we have at least two
94+
// between the last cell and this one
8695
md.push(...mdFromContentCell(cellWithOptions));
8796
break;
8897
case "raw":
@@ -123,6 +132,8 @@ export async function jupyterNotebookToMarkdown(
123132
}
124133
}
125134

135+
console.log({ md });
136+
126137
// join into source
127138
const mdSource = md.join("");
128139

src/core/lib/text.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,25 @@ export function normalizeCaseConvention(str: string): CaseConvention {
315315
}
316316
return result;
317317
}
318+
319+
export const getEndingNewlineCount = (lines: string[]) => {
320+
let count = 0;
321+
for (let i = lines.length - 1; i >= 0; i--) {
322+
if (lines[i].match(/^\n+$/)) { // only newlines
323+
count += lines[i].length;
324+
continue;
325+
}
326+
const m = lines[i].match(/\n+$/);
327+
if (m) {
328+
// newlines + other content
329+
count += m[0].length;
330+
break;
331+
}
332+
if (lines[i].length) {
333+
// non-newline content
334+
break;
335+
}
336+
// otherwise, continue
337+
}
338+
return count;
339+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* convert-backticks.test.ts
3+
*
4+
* Copyright (C) 2020-2024 Posit Software, PBC
5+
*
6+
*/
7+
8+
import { existsSync } from "../../../src/deno_ral/fs.ts";
9+
import {
10+
ExecuteOutput,
11+
test,
12+
} from "../../test.ts";
13+
import { assert } from "testing/asserts";
14+
import { quarto } from "../../../src/quarto.ts";
15+
16+
(() => {
17+
const input = "docs/convert/issue-12318";
18+
test({
19+
// The name of the test
20+
name: "issue-12318",
21+
22+
// Sets up the test
23+
context: {
24+
teardown: async () => {
25+
if (existsSync(input + '.ipynb')) {
26+
Deno.removeSync(input + '.ipynb');
27+
}
28+
}
29+
},
30+
31+
// Executes the test
32+
execute: async () => {
33+
await quarto(["convert", "docs/convert/issue-12318.qmd"]);
34+
await quarto(["convert", "docs/convert/issue-12318.ipynb", "--output", "issue-12318-2.qmd"]);
35+
const txt = Deno.readTextFileSync("issue-12318-2.qmd");
36+
assert(!txt.includes('}```'), "Triple backticks found not at beginning of line");
37+
},
38+
39+
verify: [],
40+
type: "unit"
41+
});
42+
})();

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* core/lib/text.test.ts
3+
*
4+
* Copyright (C) 2025 Posit Software, PBC
5+
*/
6+
7+
import { unitTest } from "../../../test.ts";
8+
import { assertEquals } from "testing/asserts";
9+
import { getEndingNewlineCount } from "../../../../src/core/lib/text.ts";
10+
11+
unitTest("core/lib/text.ts - getEndingNewlineCount", async () => {
12+
// Test case 1: No trailing newlines
13+
assertEquals(getEndingNewlineCount(["content without newlines"]), 0);
14+
assertEquals(getEndingNewlineCount(["line1", "line2", "line3"]), 0);
15+
16+
// Test case 2: Single line with trailing newlines
17+
assertEquals(getEndingNewlineCount(["content\n"]), 1);
18+
assertEquals(getEndingNewlineCount(["content\n\n\n"]), 3);
19+
20+
// Test case 3: Multiple lines with the last line having trailing newlines
21+
assertEquals(getEndingNewlineCount(["line1", "line2", "line3\n\n"]), 2);
22+
assertEquals(getEndingNewlineCount(["line1\n", "line2\n", "line3\n\n\n"]), 3);
23+
24+
// Test case 4: Empty lines at the end
25+
assertEquals(getEndingNewlineCount(["content", "", ""]), 0);
26+
27+
// Test case 5: Lines with only newlines at the end
28+
assertEquals(getEndingNewlineCount(["content", "\n", "\n\n"]), 3);
29+
assertEquals(getEndingNewlineCount(["content", "\n\n\n"]), 3);
30+
31+
// Test case 6: Mixed scenario with empty lines and lines with only newlines
32+
assertEquals(getEndingNewlineCount(["content\n", "", "\n\n", ""]), 3);
33+
assertEquals(getEndingNewlineCount(["content", "", "\n", "\n\n", ""]), 3);
34+
35+
// Test case 7: Edge case with only empty strings
36+
assertEquals(getEndingNewlineCount(["", "", ""]), 0);
37+
38+
// Test case 8: Edge case with an empty array
39+
assertEquals(getEndingNewlineCount([]), 0);
40+
41+
// Test case 9: Content after newlines breaks the counting
42+
assertEquals(getEndingNewlineCount(["line1\n\n", "line2"]), 0);
43+
assertEquals(getEndingNewlineCount(["line1", "line2\n\n", "line3"]), 0);
44+
45+
// Test case 10: Complex scenario with mixed content
46+
assertEquals(
47+
getEndingNewlineCount([
48+
"some content",
49+
"more content\n",
50+
"\n",
51+
"",
52+
"final line\n\n",
53+
]),
54+
2,
55+
);
56+
assertEquals(
57+
getEndingNewlineCount([
58+
"line1",
59+
"line2",
60+
"line3\nwith content",
61+
"\n",
62+
"\n\n",
63+
]),
64+
3,
65+
);
66+
});

0 commit comments

Comments
 (0)