Skip to content

Commit 94a67fb

Browse files
fix: correctly escape multi-line block quotes and callouts
1 parent 4cbfbd0 commit 94a67fb

File tree

3 files changed

+113
-78
lines changed

3 files changed

+113
-78
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.5.1",
2+
"version": "0.5.2",
33
"name": "@meshcloud/notion-markdown-cms",
44
"engines": {
55
"node": ">=14"

src/BlockRenderer.ts

Lines changed: 56 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,13 @@
1-
import {
2-
Block as PublicBlock, BlockBase, Emoji, ExternalFile, ExternalFileWithCaption, File,
3-
FileWithCaption, ImageBlock, RichText
4-
} from '@notionhq/client/build/src/api-types';
5-
61
import { AssetWriter } from './AssetWriter';
2+
import {
3+
Block, Emoji, ExternalFile, ExternalFileWithCaption, File, FileWithCaption, ImageBlock
4+
} from './Blocks';
75
import { DeferredRenderer } from './DeferredRenderer';
86
import { logger } from './logger';
97
import { RichTextRenderer } from './RichTextRenderer';
108

119
const debug = require("debug")("blocks");
1210

13-
export interface CodeBlock extends BlockBase {
14-
type: "code";
15-
code: {
16-
text: RichText[];
17-
language: string;
18-
};
19-
}
20-
21-
export interface QuoteBlock extends BlockBase {
22-
type: "quote";
23-
code: {
24-
text: RichText[];
25-
language: string;
26-
};
27-
}
28-
29-
export interface CalloutBlock extends BlockBase {
30-
type: "callout";
31-
callout: {
32-
text: RichText[];
33-
icon: File | ExternalFile | Emoji;
34-
};
35-
}
36-
37-
export interface DividerBlock extends BlockBase {
38-
type: "divider";
39-
}
40-
41-
export interface ChildDatabaseBlock extends BlockBase {
42-
type: "child_database";
43-
}
44-
45-
// these are blocks that the notion API client code does not have proper typings for
46-
// for unknown reasons they removed types alltogether in v0.4 of the client
47-
// https://github.com/makenotion/notion-sdk-js/pulls?q=is%3Apr+is%3Aclosed#issuecomment-927781781
48-
export type Block =
49-
| PublicBlock
50-
| CodeBlock
51-
| QuoteBlock
52-
| CalloutBlock
53-
| DividerBlock
54-
| ChildDatabaseBlock;
55-
56-
5711
export interface BlockRenderResult {
5812
lines: string;
5913
childIndent?: number;
@@ -62,9 +16,12 @@ export class BlockRenderer {
6216
constructor(
6317
private readonly richText: RichTextRenderer,
6418
private readonly deferredRenderer: DeferredRenderer
65-
) { }
19+
) {}
6620

67-
async renderBlock(block: Block, assets: AssetWriter): Promise<BlockRenderResult> {
21+
async renderBlock(
22+
block: Block,
23+
assets: AssetWriter
24+
): Promise<BlockRenderResult> {
6825
switch (block.type) {
6926
case "paragraph":
7027
return {
@@ -73,58 +30,70 @@ export class BlockRenderer {
7330
// note: render headings +1 level, because h1 is reserved for page titles
7431
case "heading_1":
7532
return {
76-
lines: "## " + (await this.richText.renderMarkdown(block.heading_1.text))
33+
lines:
34+
"## " + (await this.richText.renderMarkdown(block.heading_1.text)),
7735
};
7836
case "heading_2":
7937
return {
80-
lines: "### " + (await this.richText.renderMarkdown(block.heading_2.text))
38+
lines:
39+
"### " + (await this.richText.renderMarkdown(block.heading_2.text)),
8140
};
8241
case "heading_3":
8342
return {
84-
lines: "#### " + (await this.richText.renderMarkdown(block.heading_3.text))
43+
lines:
44+
"#### " +
45+
(await this.richText.renderMarkdown(block.heading_3.text)),
8546
};
8647
case "bulleted_list_item":
8748
return {
88-
lines: "- " + await this.richText.renderMarkdown(block.bulleted_list_item.text),
89-
childIndent: 4
49+
lines:
50+
"- " +
51+
(await this.richText.renderMarkdown(block.bulleted_list_item.text)),
52+
childIndent: 4,
9053
};
9154
case "numbered_list_item":
9255
return {
93-
lines: "1. " + await this.richText.renderMarkdown(block.numbered_list_item.text),
94-
childIndent: 4
56+
lines:
57+
"1. " +
58+
(await this.richText.renderMarkdown(block.numbered_list_item.text)),
59+
childIndent: 4,
9560
};
9661
case "to_do":
9762
return {
98-
lines: "[ ] " + (await this.richText.renderMarkdown(block.to_do.text))
63+
lines:
64+
"[ ] " + (await this.richText.renderMarkdown(block.to_do.text)),
9965
};
10066
case "image":
10167
return {
102-
lines: await this.renderImage(block, assets)
103-
}
104-
case "quote":
105-
block as any;
106-
return {
107-
lines: "> " + (await this.richText.renderMarkdown((block as any).quote.text))
68+
lines: await this.renderImage(block, assets),
10869
};
109-
case "code":
70+
case "quote": {
71+
// it's legal for a notion block to be cmoposed of multiple lines
72+
// each of them must be prefixed with "> " to be part of the same quote block
73+
const content = await this.richText.renderMarkdown(block.quote.text);
74+
75+
return { lines: this.formatAsQuoteBlock(content) };
76+
}
77+
case "code": {
11078
const code = await this.richText.renderPlainText(block.code.text);
11179
if (code.startsWith("<!--notion-markdown-cms:raw-->")) {
11280
return { lines: code };
11381
}
11482

11583
return {
116-
lines:
117-
"```" +
118-
block.code.language +
119-
"\n" + code +
120-
"\n```"
84+
lines: "```" + block.code.language + "\n" + code + "\n```",
12185
};
122-
case "callout":
86+
}
87+
case "callout": {
88+
// render emoji as bold, this enables css to target it as `blockquote > strong:first-child`
89+
const content =
90+
`**${this.renderIcon(block.callout.icon)}** ` +
91+
(await this.richText.renderMarkdown(block.callout.text));
92+
12393
return {
124-
lines:
125-
`> **${this.renderIcon(block.callout.icon)}** `+ // render emoji as bold, this enables css to target it as `blockquote > strong:first-child`
126-
(await this.richText.renderMarkdown(block.callout.text)),
94+
lines: this.formatAsQuoteBlock(content),
12795
};
96+
}
12897
case "divider":
12998
return { lines: "---" };
13099
case "child_database":
@@ -142,8 +111,11 @@ export class BlockRenderer {
142111
case "unsupported":
143112
default:
144113
return {
145-
lines: this.renderUnsupported(`unsupported block type: ${block.type}`, block)
146-
}
114+
lines: this.renderUnsupported(
115+
`unsupported block type: ${block.type}`,
116+
block
117+
),
118+
};
147119
}
148120
}
149121

@@ -179,6 +151,13 @@ export class BlockRenderer {
179151
}
180152
}
181153

154+
private formatAsQuoteBlock(content: string) {
155+
return content
156+
.split("\n")
157+
.map((x) => "> " + x)
158+
.join("\n");
159+
}
160+
182161
private renderUnsupported(msg: string, obj: any): string {
183162
logger.warn(msg);
184163
debug(msg + "\n%O", obj);

src/Blocks.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
Block as PublicBlock, BlockBase, Emoji, ExternalFile, ExternalFileWithCaption, File,
3+
FileWithCaption, ImageBlock, RichText
4+
} from '@notionhq/client/build/src/api-types';
5+
6+
export interface CodeBlock extends BlockBase {
7+
type: "code";
8+
code: {
9+
text: RichText[];
10+
language: string;
11+
};
12+
}
13+
14+
export interface QuoteBlock extends BlockBase {
15+
type: "quote";
16+
quote: {
17+
text: RichText[];
18+
language: string;
19+
};
20+
}
21+
22+
export interface CalloutBlock extends BlockBase {
23+
type: "callout";
24+
callout: {
25+
text: RichText[];
26+
icon: File | ExternalFile | Emoji;
27+
};
28+
}
29+
30+
export interface DividerBlock extends BlockBase {
31+
type: "divider";
32+
}
33+
34+
export interface ChildDatabaseBlock extends BlockBase {
35+
type: "child_database";
36+
}
37+
38+
// these are blocks that the notion API client code does not have proper typings for
39+
// for unknown reasons they removed types alltogether in v0.4 of the client
40+
// https://github.com/makenotion/notion-sdk-js/pulls?q=is%3Apr+is%3Aclosed#issuecomment-927781781
41+
export type Block =
42+
| PublicBlock
43+
| CodeBlock
44+
| QuoteBlock
45+
| CalloutBlock
46+
| DividerBlock
47+
| ChildDatabaseBlock;
48+
49+
export {
50+
Emoji,
51+
ExternalFile,
52+
ExternalFileWithCaption,
53+
File,
54+
FileWithCaption,
55+
ImageBlock,
56+
};

0 commit comments

Comments
 (0)