Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions apps/roam/src/utils/__tests__/pageToMarkdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it, vi } from "vitest";
import type { TreeNode, ViewType } from "roamjs-components/types";

vi.mock("roamjs-components/queries/getFullTreeByParentUid", () => ({
default: () => ({ children: [] }),
}));

// Runs before the imports below: getDiscourseNodes calls generateUID at module load.
vi.hoisted(() => {
(globalThis as { window?: unknown }).window = {
roamAlphaAPI: { util: { generateUID: () => "someUid" } },
};
});

import { toMarkdown } from "~/utils/pageToMarkdown";

const block = (text: string, children: TreeNode[] = []): TreeNode =>
({ text, children, order: 0, uid: "" }) as unknown as TreeNode;

const opts = {
refs: false,
embeds: false,
simplifiedFilename: false,
maxFilenameLength: 64,
allNodes: [],
removeSpecialCharacters: false,
linkType: "wikilinks",
};

const render = ({
c,
v,
flatten,
}: {
c: TreeNode;
v?: ViewType;
flatten?: boolean;
}): string => toMarkdown({ c, v, opts: { ...opts, flatten } });

const codeBlock = "```js\nconst a = 1;\n\nconst b = 2;\n```";

describe("toMarkdown multi-line blocks", () => {
it("indents every line of a top-level bullet to its content column", () => {
expect(render({ c: block(`intro\n${codeBlock}`) })).toBe(
"- intro\n ```js\n const a = 1;\n\n const b = 2;\n ```",
);
});

it("indents every line of a nested bullet to its content column", () => {
expect(render({ c: block("parent", [block(`intro\n${codeBlock}`)]) })).toBe(
"- parent\n - intro\n ```js\n const a = 1;\n\n const b = 2;\n ```",
);
});

it("indents to the wider content column of a numbered item", () => {
expect(render({ c: block("first\nsecond"), v: "numbered" })).toBe(
"1. first\n second",
);
});

it("leaves continuation lines alone when flattening", () => {
expect(
render({ c: block("parent", [block("first\nsecond")]), flatten: true }),
).toBe("- parent\n- first\nsecond");
});

it("leaves continuation lines alone in the document view type", () => {
expect(
render({ c: block("parent", [block("first\nsecond")]), v: "document" }),
).toBe("parent\n\n first\nsecond\n");
});
});
25 changes: 24 additions & 1 deletion apps/roam/src/utils/pageToMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ const handleFrontmatter = ({
return output;
};

const indentContinuationLines = ({
text,
width,
}: {
text: string;
width: number;
}): string => {
const padding = "".padStart(width, " ");
return text
.split("\n")
.map((line, index) => (index === 0 || !line ? line : `${padding}${line}`))
.join("\n");
};

export const toMarkdown = ({
c,
i = 0,
Expand Down Expand Up @@ -191,6 +205,15 @@ export const toMarkdown = ({
: v;
const viewTypePrefix = viewTypeToPrefix[effectiveViewType];
const headingPrefix = c.heading ? `${"".padStart(c.heading, "#")} ` : "";
// CommonMark ends a list item at a column-0 fence, heading, quote or table,
// so continuation lines must sit at the item's content column.
const blockText =
flatten || effectiveViewType === "document"
? finalProcessedText
: indentContinuationLines({
text: finalProcessedText,
width: indentation.length + viewTypePrefix.length,
});
const childrenMarkdown = (c.children || [])
.filter((nested) => !!nested.text || !!nested.children?.length)
.map((nested) => {
Expand All @@ -206,7 +229,7 @@ export const toMarkdown = ({
.join("");
const lineBreak = v === "document" ? "\n" : "";

return `${indentation}${viewTypePrefix}${headingPrefix}${finalProcessedText}${lineBreak}${childrenMarkdown}`;
return `${indentation}${viewTypePrefix}${headingPrefix}${blockText}${lineBreak}${childrenMarkdown}`;
};

export const pageToMarkdown = async (
Expand Down
Loading