|
1 | 1 | const { blockToString } = require("../block-to-string")
|
2 | 2 |
|
3 | 3 | const EOL_MD = "\n"
|
4 |
| - |
5 |
| -exports.notionBlockToMarkdown = (block, lowerTitleLevel, depth = 0) => |
6 |
| - block.children.reduce((acc, childBlock) => { |
7 |
| - let childBlocksString = "" |
8 |
| - |
9 |
| - if (childBlock.has_children) { |
10 |
| - childBlocksString = " " |
11 |
| - .repeat(depth) |
12 |
| - .concat(childBlocksString) |
13 |
| - .concat(this.notionBlockToMarkdown(childBlock, lowerTitleLevel, depth + 2)) |
14 |
| - .concat(EOL_MD) |
15 |
| - } |
16 |
| - |
17 |
| - if (childBlock.type == "paragraph") { |
18 |
| - const p = blockToString(childBlock.paragraph.text) |
19 |
| - |
20 |
| - const isTableRow = p.startsWith("|") && p.endsWith("|") |
21 |
| - |
22 |
| - return acc |
23 |
| - .concat(p) |
24 |
| - .concat(isTableRow ? EOL_MD : EOL_MD.concat(EOL_MD)) |
25 |
| - .concat(childBlocksString) |
26 |
| - } |
27 |
| - |
28 |
| - if (childBlock.type.startsWith("heading_")) { |
29 |
| - const headingLevel = Number(childBlock.type.split("_")[1]) |
30 |
| - |
31 |
| - return acc |
32 |
| - .concat(EOL_MD) |
33 |
| - .concat(lowerTitleLevel ? "#" : "") |
34 |
| - .concat("#".repeat(headingLevel)) |
35 |
| - .concat(" ") |
36 |
| - .concat(blockToString(childBlock[childBlock.type].text)) |
37 |
| - .concat(EOL_MD) |
38 |
| - .concat(childBlocksString) |
39 |
| - } |
40 |
| - |
41 |
| - if (childBlock.type == "to_do") { |
42 |
| - return acc |
43 |
| - .concat(`- [${childBlock.to_do.checked ? "x" : " "}] `) |
44 |
| - .concat(blockToString(childBlock.to_do.text)) |
45 |
| - .concat(EOL_MD) |
46 |
| - .concat(childBlocksString) |
47 |
| - } |
48 |
| - |
49 |
| - if (childBlock.type == "bulleted_list_item") { |
50 |
| - return acc |
51 |
| - .concat("* ") |
52 |
| - .concat(blockToString(childBlock.bulleted_list_item.text)) |
53 |
| - .concat(EOL_MD) |
54 |
| - .concat(childBlocksString) |
55 |
| - } |
56 |
| - |
57 |
| - if (childBlock.type == "numbered_list_item") { |
58 |
| - return acc |
59 |
| - .concat("1. ") |
60 |
| - .concat(blockToString(childBlock.numbered_list_item.text)) |
61 |
| - .concat(EOL_MD) |
62 |
| - .concat(childBlocksString) |
63 |
| - } |
64 |
| - |
65 |
| - if (childBlock.type == "toggle") { |
66 |
| - return acc |
67 |
| - .concat("<details><summary>") |
68 |
| - .concat(blockToString(childBlock.toggle.text)) |
69 |
| - .concat("</summary>") |
70 |
| - .concat(childBlocksString) |
71 |
| - .concat("</details>") |
72 |
| - } |
73 |
| - |
74 |
| - if (childBlock.type == "code") { |
75 |
| - return acc |
76 |
| - .concat(EOL_MD) |
77 |
| - .concat("```", childBlock.code.language, EOL_MD) |
78 |
| - .concat(blockToString(childBlock.code.text)) |
79 |
| - .concat(EOL_MD) |
80 |
| - .concat("```") |
81 |
| - .concat(childBlocksString) |
82 |
| - .concat(EOL_MD) |
83 |
| - } |
84 |
| - |
85 |
| - if (childBlock.type == "image") { |
86 |
| - const imageUrl = |
87 |
| - childBlock.image.type == "external" ? childBlock.image.external.url : childBlock.image.file.url |
88 |
| - |
89 |
| - return acc |
90 |
| - .concat(" |
93 |
| - .concat(imageUrl) |
94 |
| - .concat(")") |
95 |
| - .concat(EOL_MD) |
96 |
| - } |
97 |
| - |
98 |
| - if (childBlock.type == "audio") { |
99 |
| - const audioUrl = |
100 |
| - childBlock.audio.type == "external" ? childBlock.audio.external.url : childBlock.audio.file.url |
101 |
| - |
102 |
| - return acc |
103 |
| - .concat("<audio controls>") |
104 |
| - .concat(EOL_MD) |
105 |
| - .concat(`<source src="${audioUrl}" />`) |
106 |
| - .concat(EOL_MD) |
107 |
| - .concat("</audio>") |
108 |
| - .concat(EOL_MD) |
109 |
| - } |
110 |
| - |
111 |
| - if (childBlock.type == "video" && childBlock.video.type == "external") { |
112 |
| - const videoUrl = childBlock.video.external.url |
113 |
| - |
114 |
| - return acc.concat(videoUrl).concat(EOL_MD) |
115 |
| - } |
116 |
| - |
117 |
| - if (childBlock.type == "embed") { |
118 |
| - return acc.concat(childBlock.embed.url).concat(EOL_MD) |
119 |
| - } |
120 |
| - |
121 |
| - if (childBlock.type == "quote") { |
122 |
| - return acc.concat("> ").concat(blockToString(childBlock.quote.text)).concat(EOL_MD) |
123 |
| - } |
124 |
| - |
125 |
| - // TODO: Add support for callouts, internal video, andd files |
126 |
| - |
127 |
| - if (childBlock.type == "bookmark") { |
128 |
| - const bookmarkUrl = childBlock.bookmark.url |
129 |
| - |
130 |
| - const bookmarkCaption = blockToString(childBlock.bookmark.caption) || bookmarkUrl |
131 |
| - |
132 |
| - return acc |
133 |
| - .concat("[") |
134 |
| - .concat(bookmarkCaption) |
135 |
| - .concat("](") |
136 |
| - .concat(bookmarkUrl) |
137 |
| - .concat(")") |
138 |
| - .concat(EOL_MD) |
139 |
| - } |
140 |
| - |
141 |
| - if (childBlock.type == "divider") { |
142 |
| - return acc.concat("---").concat(EOL_MD) |
143 |
| - } |
144 |
| - |
145 |
| - if (childBlock.type == "unsupported") { |
146 |
| - return acc |
147 |
| - .concat(`<!-- This block is not supported by Notion API yet. -->`) |
148 |
| - .concat(EOL_MD) |
149 |
| - .concat(childBlocksString) |
150 |
| - } |
151 |
| - |
152 |
| - return acc |
153 |
| - }, "") |
| 4 | +const DOUBLE_EOL_MD = EOL_MD.repeat(2) |
| 5 | + |
| 6 | +// Inserts the string at the beginning of every line of the content. If the useSpaces flag is set to |
| 7 | +// true, the lines after the first will instead be prepended with two spaces. |
| 8 | +function prependToLines(content, string, useSpaces = true) { |
| 9 | + let [head, ...tail] = content.split("\n") |
| 10 | + |
| 11 | + return [ |
| 12 | + `${string} ${head}`, |
| 13 | + ...tail.map((line) => { |
| 14 | + return `${useSpaces ? " " : string} ${line}` |
| 15 | + }), |
| 16 | + ].join("\n") |
| 17 | +} |
| 18 | + |
| 19 | +// Converts a notion block to a markdown string. |
| 20 | +exports.notionBlockToMarkdown = (block, lowerTitleLevel) => { |
| 21 | + // Get the child content of the block. |
| 22 | + let childMarkdown = (block.children ?? []) |
| 23 | + .map((block) => this.notionBlockToMarkdown(block, lowerTitleLevel)) |
| 24 | + .join("") |
| 25 | + .trim() |
| 26 | + |
| 27 | + // If the block is a page, return the child content. |
| 28 | + if (block.object === "page") { |
| 29 | + return childMarkdown |
| 30 | + } |
| 31 | + |
| 32 | + // Extract the remaining content of the block and combine it with its children. |
| 33 | + let blockMarkdown = block[block.type]?.text ? blockToString(block[block.type]?.text).trim() : null |
| 34 | + let markdown = [blockMarkdown, childMarkdown].filter((text) => text).join(DOUBLE_EOL_MD) |
| 35 | + |
| 36 | + // Table row |
| 37 | + // TODO: This should be moved to the new Notion type. |
| 38 | + if (block.type == "paragraph" && blockMarkdown.startsWith("|") && blockMarkdown.endsWith("|")) { |
| 39 | + return markdown.concat(EOL_MD) |
| 40 | + } |
| 41 | + |
| 42 | + // Paragraph |
| 43 | + if (block.type == "paragraph") { |
| 44 | + return [EOL_MD, markdown, EOL_MD].join("") |
| 45 | + } |
| 46 | + |
| 47 | + // Heading |
| 48 | + if (block.type.startsWith("heading_")) { |
| 49 | + const headingLevel = Number(block.type.split("_")[1]) |
| 50 | + let symbol = (lowerTitleLevel ? "#" : "") + "#".repeat(headingLevel) |
| 51 | + return [EOL_MD, prependToLines(markdown, symbol), EOL_MD].join("") |
| 52 | + } |
| 53 | + |
| 54 | + // To do list item |
| 55 | + if (block.type == "to_do") { |
| 56 | + let symbol = `- [${block.to_do.checked ? "x" : " "}] ` |
| 57 | + return prependToLines(markdown, symbol).concat(EOL_MD) |
| 58 | + } |
| 59 | + |
| 60 | + // Bulleted list item |
| 61 | + if (block.type == "bulleted_list_item") { |
| 62 | + return prependToLines(markdown, "*").concat(EOL_MD) |
| 63 | + } |
| 64 | + |
| 65 | + // Numbered list item |
| 66 | + if (block.type == "numbered_list_item") { |
| 67 | + return prependToLines(markdown, "1.").concat(EOL_MD) |
| 68 | + } |
| 69 | + |
| 70 | + // Toggle |
| 71 | + if (block.type == "toggle") { |
| 72 | + return [ |
| 73 | + EOL_MD, |
| 74 | + "<details><summary>", |
| 75 | + blockMarkdown, |
| 76 | + "</summary>", |
| 77 | + childMarkdown, |
| 78 | + "</details>", |
| 79 | + EOL_MD, |
| 80 | + ].join("") |
| 81 | + } |
| 82 | + |
| 83 | + // Code |
| 84 | + if (block.type == "code") { |
| 85 | + return [ |
| 86 | + EOL_MD, |
| 87 | + `\`\`\` ${block.code.language}${EOL_MD}`, |
| 88 | + blockMarkdown, |
| 89 | + EOL_MD, |
| 90 | + "```", |
| 91 | + EOL_MD, |
| 92 | + childMarkdown, |
| 93 | + EOL_MD, |
| 94 | + ].join("") |
| 95 | + } |
| 96 | + |
| 97 | + // Image |
| 98 | + if (block.type == "image") { |
| 99 | + const imageUrl = block.image.type == "external" ? block.image.external.url : block.image.file.url |
| 100 | + return `${EOL_MD}${EOL_MD}` |
| 101 | + } |
| 102 | + |
| 103 | + // Audio |
| 104 | + if (block.type == "audio") { |
| 105 | + const audioUrl = block.audio.type == "external" ? block.audio.external.url : block.audio.file.url |
| 106 | + return [EOL_MD, "<audio controls>", `<source src="${audioUrl}" />`, "</audio>", EOL_MD].join("") |
| 107 | + } |
| 108 | + |
| 109 | + // Video |
| 110 | + if (block.type == "video" && block.video.type == "external") { |
| 111 | + return [EOL_MD, block.video.external.url, EOL_MD].join("") |
| 112 | + } |
| 113 | + |
| 114 | + // Embed |
| 115 | + if (block.type == "embed") { |
| 116 | + return [EOL_MD, block.embed.url, EOL_MD].join("") |
| 117 | + } |
| 118 | + |
| 119 | + // Quote |
| 120 | + if (block.type == "quote") { |
| 121 | + return [EOL_MD, prependToLines(markdown, ">", false), EOL_MD].join("") |
| 122 | + } |
| 123 | + |
| 124 | + // Bookmark |
| 125 | + if (block.type == "bookmark") { |
| 126 | + const bookmarkUrl = block.bookmark.url |
| 127 | + const bookmarkCaption = blockToString(block.bookmark.caption) || bookmarkUrl |
| 128 | + return `${EOL_MD}[${bookmarkCaption}](${bookmarkUrl})${EOL_MD}` |
| 129 | + } |
| 130 | + |
| 131 | + // Divider |
| 132 | + if (block.type == "divider") { |
| 133 | + return `${EOL_MD}---${EOL_MD}` |
| 134 | + } |
| 135 | + |
| 136 | + // Unsupported types. |
| 137 | + // TODO: Add support for callouts, internal video, and files |
| 138 | + return [EOL_MD, `<!-- This block type '${block.type}' is not supported yet. -->`, EOL_MD].join("") |
| 139 | +} |
0 commit comments