Skip to content

Commit e23954b

Browse files
feat: add support for rendering page mentions
1 parent c35f694 commit e23954b

12 files changed

+243
-96
lines changed

src/BlockRenderer.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Database } from "./Database";
1616
import { DeferredRenderer } from "./DeferredRenderer";
1717
import { RichTextRenderer } from "./RichTextRenderer";
1818
import { logger } from "./logger";
19+
import { LinkRenderer } from "./LinkRenderer";
1920

2021
const debug = require("debug")("blocks");
2122

@@ -65,48 +66,49 @@ export type Block =
6566
export class BlockRenderer {
6667
constructor(
6768
private readonly deferredRenderer: DeferredRenderer,
68-
private readonly richText: RichTextRenderer
69+
private readonly richText: RichTextRenderer,
70+
private readonly linkRenderer: LinkRenderer
6971
) {}
7072

7173
async renderBlock(block: Block, assets: AssetWriter): Promise<string> {
7274
switch (block.type) {
7375
case "paragraph":
74-
return this.richText.renderMarkdown(block.paragraph.text);
76+
return await this.richText.renderMarkdown(block.paragraph.text);
7577
case "heading_1":
76-
return "# " + this.richText.renderMarkdown(block.heading_1.text);
78+
return "# " + await this.richText.renderMarkdown(block.heading_1.text);
7779
case "heading_2":
78-
return "## " + this.richText.renderMarkdown(block.heading_2.text);
80+
return "## " + await this.richText.renderMarkdown(block.heading_2.text);
7981
case "heading_3":
80-
return "### " + this.richText.renderMarkdown(block.heading_3.text);
82+
return "### " + await this.richText.renderMarkdown(block.heading_3.text);
8183
case "bulleted_list_item":
8284
return (
83-
"- " + this.richText.renderMarkdown(block.bulleted_list_item.text)
85+
"- " + await this.richText.renderMarkdown(block.bulleted_list_item.text)
8486
);
8587
case "numbered_list_item":
8688
return (
87-
"1. " + this.richText.renderMarkdown(block.numbered_list_item.text)
89+
"1. " + await this.richText.renderMarkdown(block.numbered_list_item.text)
8890
);
8991
case "to_do":
90-
return "[ ] " + this.richText.renderMarkdown(block.to_do.text);
92+
return "[ ] " + await this.richText.renderMarkdown(block.to_do.text);
9193
case "image":
9294
return await this.renderImage(block, assets);
9395
case "quote":
9496
block as any;
95-
return "> " + this.richText.renderMarkdown((block as any).quote.text);
97+
return "> " + await this.richText.renderMarkdown((block as any).quote.text);
9698
case "code":
9799
return (
98100
"```" +
99101
block.code.language +
100102
"\n" +
101-
this.richText.renderMarkdown(block.code.text) +
103+
await this.richText.renderMarkdown(block.code.text) +
102104
"\n```"
103105
);
104106
case "callout":
105107
return (
106108
"> " +
107109
this.renderIcon(block.callout.icon) +
108110
" " +
109-
this.richText.renderMarkdown(block.callout.text)
111+
await this.richText.renderMarkdown(block.callout.text)
110112
);
111113
case "divider":
112114
return "---";
@@ -154,7 +156,7 @@ export class BlockRenderer {
154156
cols.map((c, i) => {
155157
const content = escapeTableCell(r.properties.values[c]);
156158
return i == 0
157-
? this.renderMarkdownLink(content, r.file.substr("docs".length)) // make the first cell a relative link to the page
159+
? this.linkRenderer.renderPageLink(content, r) // make the first cell a relative link to the page
158160
: content;
159161
})
160162
)
@@ -195,10 +197,6 @@ export class BlockRenderer {
195197
}
196198
}
197199

198-
private renderMarkdownLink(text: string, url: string): string {
199-
return `[${text}](${url})`;
200-
}
201-
202200
private renderUnsupported(msg: string, obj: any): string {
203201
logger.warn(msg);
204202
debug(msg + "\n%O", obj);

src/Database.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DatabaseConfig } from "./SyncConfig";
2-
import { RenderPageTask } from "./RenderedPageTask";
2+
import { RenderPageTask } from "./RenderPageTask";
33

44
export interface Database {
55
config: DatabaseConfig;

src/DatabasePageRenderer.ts renamed to src/DatabaseRenderer.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,18 @@
11
import { Client } from "@notionhq/client";
2-
import { DatabaseConfig } from "./SyncConfig";
32
import { Database } from "./Database";
43
import { DeferredRenderer } from "./DeferredRenderer";
54
import { SyncConfig } from ".";
5+
import { lookupDatabaseConfig } from "./config";
66

7-
export class DatabasePageRenderer {
7+
export class DatabaseRenderer {
88
constructor(
99
readonly publicApi: Client,
1010
readonly deferredRenderer: DeferredRenderer,
1111
readonly config: SyncConfig
1212
) {}
1313

1414
async renderDatabase(databaseId: string): Promise<Database> {
15-
const fallbackDbConfig: DatabaseConfig = {
16-
outDir:
17-
databaseId === this.config.cmsDatabaseId
18-
? this.config.outDir
19-
: this.config.outDir + "/" + databaseId,
20-
properties: {
21-
category: "Category",
22-
},
23-
};
24-
const dbConfig: DatabaseConfig =
25-
this.config.databases[databaseId] || fallbackDbConfig;
15+
const dbConfig = lookupDatabaseConfig(this.config, databaseId);
2616

2717
const db = await this.publicApi.databases.retrieve({
2818
database_id: databaseId,
@@ -40,12 +30,15 @@ export class DatabasePageRenderer {
4030
);
4131
}
4232

43-
const tasks = allPages.results.map((x) =>
33+
const prepareRenderPageTasks = allPages.results.map((x) =>
4434
this.deferredRenderer.renderPage(x, dbConfig)
4535
);
4636

37+
// note: the await here is not actually starting to render the pages, however it prepares the page render task
38+
const renderPageTasks = await Promise.all(prepareRenderPageTasks);
39+
4740
return {
48-
pages: tasks,
41+
pages: renderPageTasks,
4942
config: dbConfig,
5043
};
5144
}

src/DeferredRenderer.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { Page } from "@notionhq/client/build/src/api-types";
22
import { DatabaseConfig } from "./SyncConfig";
33
import { Database } from "./Database";
4-
import { DatabasePageRenderer } from "./DatabasePageRenderer";
4+
import { DatabaseRenderer } from "./DatabaseRenderer";
55
import { logger } from "./logger";
66
import { PageRenderer } from "./PageRenderer";
7-
import { RenderPageTask as RenderPageTask } from "./RenderedPageTask";
7+
import { RenderPageTask as RenderPageTask } from "./RenderPageTask";
88
import { RenderedPage } from "./RenderedPage";
99

1010
const debug = require("debug")("rendering");
1111

1212
export class DeferredRenderer {
13-
private dbRenderer!: DatabasePageRenderer;
13+
private dbRenderer!: DatabaseRenderer;
1414
private pageRenderer!: PageRenderer;
1515

1616
private pageQueue: (() => Promise<any>)[] = [];
@@ -19,7 +19,7 @@ export class DeferredRenderer {
1919
private readonly renderedPages = new Map<string, RenderPageTask>();
2020

2121
public initialize(
22-
dbRenderer: DatabasePageRenderer,
22+
dbRenderer: DatabaseRenderer,
2323
pageRenderer: PageRenderer
2424
) {
2525
this.dbRenderer = dbRenderer;
@@ -40,14 +40,15 @@ export class DeferredRenderer {
4040
return fetched;
4141
}
4242

43-
public renderPage(page: Page, config: DatabaseConfig): RenderPageTask {
43+
public async renderPage(page: Page, config: DatabaseConfig): Promise<RenderPageTask> {
4444
const cached = this.renderedPages.get(page.id);
4545
if (cached) {
4646
debug("page cache hit " + page.id);
4747
return cached;
4848
}
4949

50-
const task = this.pageRenderer.renderPage(page, config);
50+
const task = await this.pageRenderer.renderPage(page, config);
51+
5152
this.renderedPages.set(page.id, task);
5253
this.pageQueue.push(task.render);
5354

src/LinkRenderer.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { SyncConfig } from ".";
2+
import { RenderPageTask } from "./RenderPageTask";
3+
4+
export class LinkRenderer {
5+
constructor(private readonly config: SyncConfig) {}
6+
7+
renderUrlLink(text: string, url: string): string {
8+
return `[${text}](${url})`;
9+
}
10+
11+
renderPageLink(text: string, page: RenderPageTask): string {
12+
const url = page.file.substr(this.config.outDir.length);
13+
14+
return this.renderUrlLink(text, url);
15+
}
16+
}

src/MentionedPageRenderer.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { APIErrorCode, Client } from "@notionhq/client";
2+
import { DeferredRenderer } from "./DeferredRenderer";
3+
import { SyncConfig } from ".";
4+
import { RenderPageTask } from "./RenderPageTask";
5+
import { lookupDatabaseConfig } from "./config";
6+
7+
export class MentionedPageRenderer {
8+
constructor(
9+
readonly publicApi: Client,
10+
readonly deferredRenderer: DeferredRenderer,
11+
readonly config: SyncConfig
12+
) {}
13+
14+
async renderPage(pageId: string): Promise<RenderPageTask | null> {
15+
const page = await this.tryFindPage(pageId);
16+
17+
if (!page) {
18+
return null;
19+
}
20+
21+
let databaseId: string | null = null;
22+
if (page.parent.type === "database_id") {
23+
databaseId = page.parent.database_id;
24+
}
25+
26+
const dbConfig = lookupDatabaseConfig(this.config, databaseId);
27+
28+
return this.deferredRenderer.renderPage(page, dbConfig);
29+
}
30+
31+
async tryFindPage(pageId: string) {
32+
try {
33+
return await this.publicApi.pages.retrieve({ page_id: pageId });
34+
} catch (error: any) {
35+
if (error.code === APIErrorCode.ObjectNotFound) {
36+
return null;
37+
} else {
38+
throw error;
39+
}
40+
}
41+
}
42+
}

src/PageRenderer.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { AssetWriter } from "./AssetWriter";
66
import { FrontmatterRenderer } from "./FrontmatterRenderer";
77
import { RecursiveBodyRenderer } from "./RecursiveBodyRenderer";
88
import { slugify } from "./slugify";
9-
import { RenderPageTask as RenderPageTask } from "./RenderedPageTask";
9+
import { RenderPageTask as RenderPageTask } from "./RenderPageTask";
1010
import { DatabaseConfig } from "./SyncConfig";
1111
import { PropertiesParser } from "./PropertiesParser";
1212
import { logger } from "./logger";
@@ -20,8 +20,12 @@ export class PageRenderer {
2020
readonly bodyRenderer: RecursiveBodyRenderer
2121
) {}
2222

23-
renderPage(page: Page, config: DatabaseConfig): RenderPageTask {
24-
const props = this.propertiesParser.parse(page, config);
23+
async renderPage(page: Page, config: DatabaseConfig): Promise<RenderPageTask> {
24+
if (page.archived){
25+
logger.warn(`rendering archived page ${page.url}`);
26+
}
27+
28+
const props = await this.propertiesParser.parse(page, config);
2529

2630
const categorySlug = slugify(props.meta.category);
2731
const destDir = `${config.outDir}/${categorySlug}`;

src/PropertiesParser.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ const debug = require("debug")("properties");
1111
export class PropertiesParser {
1212
constructor(private readonly richText: RichTextRenderer) {}
1313

14-
public parse(page: Page, config: DatabaseConfig): PageProperties {
14+
public async parse(page: Page, config: DatabaseConfig): Promise<PageProperties> {
1515
const properties: Record<string, any> = {};
1616
const keys = new Map<string, string>();
1717

1818
let title: string | null = null;
1919
let category: string | null = null;
2020
let order: number | undefined = undefined;
2121

22-
Object.entries(page.properties).forEach(([name, value]) => {
23-
const parsedValue = this.parsePropertyValue(value);
22+
for (const [name, value] of Object.entries(page.properties)) {
23+
const parsedValue = await this.parsePropertyValue(value);
2424

2525
if (
2626
!config.properties.include ||
@@ -42,7 +42,7 @@ export class PropertiesParser {
4242
if (name === "order") {
4343
order = parsedValue;
4444
}
45-
});
45+
}
4646

4747
if (!title) {
4848
throw this.errorMissingRequiredProperty("of type 'title'", page);
@@ -59,21 +59,21 @@ export class PropertiesParser {
5959
title: title, // notion API always calls it name
6060
category: category,
6161
order: order,
62-
...config.additionalPageFrontmatter
62+
...config.additionalPageFrontmatter,
6363
},
6464
values: properties,
6565
keys: this.sortKeys(config, keys),
6666
};
6767
}
6868

69-
private parsePropertyValue(value: PropertyValue): any {
69+
private async parsePropertyValue(value: PropertyValue): Promise<any> {
7070
switch (value.type) {
7171
case "number":
7272
return value.number;
7373
case "title":
74-
return this.richText.renderMarkdown(value.title);
74+
return await this.richText.renderMarkdown(value.title);
7575
case "rich_text":
76-
return this.richText.renderMarkdown(value.rich_text);
76+
return await this.richText.renderMarkdown(value.rich_text);
7777
case "select":
7878
return value.select?.name;
7979
case "multi_select":
File renamed without changes.

0 commit comments

Comments
 (0)