Skip to content

Commit d21b91e

Browse files
committed
convert - ensure enough linebreaks between cells
1 parent 2fb2c6d commit d21b91e

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed

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,
@@ -64,9 +65,17 @@ export async function jupyterNotebookToMarkdown(
6465
cell,
6566
);
6667

68+
const endingNewLineCount = getEndingNewlineCount(md);
69+
if (i > 0 && endingNewLineCount < 2) {
70+
md.push("\n\n");
71+
}
72+
6773
// write markdown
6874
switch (cell.cell_type) {
6975
case "markdown":
76+
// does the previous line have enough newlines?
77+
// if not, add sufficient newlines so we have at least two
78+
// between the last cell and this one
7079
md.push(...mdFromContentCell(cellWithOptions));
7180
break;
7281
case "raw":
@@ -107,6 +116,8 @@ export async function jupyterNotebookToMarkdown(
107116
}
108117
}
109118

119+
console.log({ md });
120+
110121
// join into source
111122
const mdSource = md.join("");
112123

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)