Skip to content

Commit 20fe79b

Browse files
feat: render database entries to index
refactor to make rendering paths between pages and entries symmetrical
1 parent a18ca77 commit 20fe79b

21 files changed

+343
-213
lines changed

src/BlockRenderer.ts

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import {
2-
Block as PublicBlock, BlockBase, Emoji, ExternalFile, ExternalFileWithCaption, File,
3-
FileWithCaption, ImageBlock, RichText
4-
} from '@notionhq/client/build/src/api-types';
5-
6-
import { AssetWriter } from './AssetWriter';
7-
import { DeferredRenderer } from './DeferredRenderer';
8-
import { logger } from './logger';
9-
import { RichTextRenderer } from './RichTextRenderer';
2+
Block as PublicBlock,
3+
BlockBase,
4+
Emoji,
5+
ExternalFile,
6+
ExternalFileWithCaption,
7+
File,
8+
FileWithCaption,
9+
ImageBlock,
10+
RichText,
11+
} from "@notionhq/client/build/src/api-types";
12+
13+
import { AssetWriter } from "./AssetWriter";
14+
import { DeferredRenderer } from "./DeferredRenderer";
15+
import { logger } from "./logger";
16+
import { RichTextRenderer } from "./RichTextRenderer";
1017

1118
const debug = require("debug")("blocks");
1219

@@ -56,7 +63,7 @@ export type Block =
5663
export class BlockRenderer {
5764
constructor(
5865
private readonly richText: RichTextRenderer,
59-
private readonly deferredRenderer: DeferredRenderer,
66+
private readonly deferredRenderer: DeferredRenderer
6067
) {}
6168

6269
async renderBlock(block: Block, assets: AssetWriter): Promise<string> {
@@ -65,49 +72,57 @@ export class BlockRenderer {
6572
return await this.richText.renderMarkdown(block.paragraph.text);
6673
// note: render headings +1 level, because h1 is reserved for page titles
6774
case "heading_1":
68-
return "## " + await this.richText.renderMarkdown(block.heading_1.text);
75+
return (
76+
"## " + (await this.richText.renderMarkdown(block.heading_1.text))
77+
);
6978
case "heading_2":
70-
return "### " + await this.richText.renderMarkdown(block.heading_2.text);
79+
return (
80+
"### " + (await this.richText.renderMarkdown(block.heading_2.text))
81+
);
7182
case "heading_3":
72-
return "#### " +
73-
await this.richText.renderMarkdown(block.heading_3.text);
83+
return (
84+
"#### " + (await this.richText.renderMarkdown(block.heading_3.text))
85+
);
7486
case "bulleted_list_item":
7587
return (
7688
"- " +
77-
await this.richText.renderMarkdown(block.bulleted_list_item.text)
89+
(await this.richText.renderMarkdown(block.bulleted_list_item.text))
7890
);
7991
case "numbered_list_item":
8092
return (
8193
"1. " +
82-
await this.richText.renderMarkdown(block.numbered_list_item.text)
94+
(await this.richText.renderMarkdown(block.numbered_list_item.text))
8395
);
8496
case "to_do":
85-
return "[ ] " + await this.richText.renderMarkdown(block.to_do.text);
97+
return "[ ] " + (await this.richText.renderMarkdown(block.to_do.text));
8698
case "image":
8799
return await this.renderImage(block, assets);
88100
case "quote":
89101
block as any;
90-
return "> " +
91-
await this.richText.renderMarkdown((block as any).quote.text);
102+
return (
103+
"> " + (await this.richText.renderMarkdown((block as any).quote.text))
104+
);
92105
case "code":
93106
return (
94107
"```" +
95108
block.code.language +
96109
"\n" +
97-
await this.richText.renderMarkdown(block.code.text) +
110+
(await this.richText.renderMarkdown(block.code.text)) +
98111
"\n```"
99112
);
100113
case "callout":
101114
return (
102115
"> " +
103116
this.renderIcon(block.callout.icon) +
104117
" " +
105-
await this.richText.renderMarkdown(block.callout.text)
118+
(await this.richText.renderMarkdown(block.callout.text))
106119
);
107120
case "divider":
108121
return "---";
109122
case "child_database":
110-
return await this.deferredRenderer.renderChildDatabase(block.id);
123+
const msg = `<!-- included database ${block.id} -->\n`;
124+
const db = await this.deferredRenderer.renderChildDatabase(block.id);
125+
return msg + db.markdown;
111126
case "toggle":
112127
case "child_page":
113128
case "embed":
@@ -120,7 +135,7 @@ export class BlockRenderer {
120135
default:
121136
return this.renderUnsupported(
122137
`unsupported block type: ${block.type}`,
123-
block,
138+
block
124139
);
125140
}
126141
}
@@ -133,7 +148,7 @@ export class BlockRenderer {
133148
case "external":
134149
return this.renderUnsupported(
135150
`unsupported icon type: ${icon.type}`,
136-
icon,
151+
icon
137152
);
138153
}
139154
}

src/ChildDatabaseRenderer.ts

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,81 @@
1-
import { Client } from '@notionhq/client/build/src';
2-
import { Page } from '@notionhq/client/build/src/api-types';
1+
import { Client } from "@notionhq/client/build/src";
2+
import { Page } from "@notionhq/client/build/src/api-types";
33

4-
import { SyncConfig } from './';
5-
import { lookupDatabaseConfig } from './config';
6-
import { DatabaseViewRenderer } from './DatabaseViewRenderer';
7-
import { DeferredRenderer } from './DeferredRenderer';
8-
import { DatabaseConfig } from './SyncConfig';
9-
import { TableRenderer } from './TableRenderer';
4+
import { SyncConfig } from "./";
5+
import { lookupDatabaseConfig } from "./config";
6+
import { Database } from "./Database";
7+
import { DatabaseViewRenderer } from "./DatabaseViewRenderer";
8+
import { DeferredRenderer } from "./DeferredRenderer";
9+
import { RenderDatabasePageTask } from "./RenderDatabasePageTask";
10+
import { DatabaseConfig } from "./SyncConfig";
11+
import { DatabaseTableRenderer } from "./DatabaseTableRenderer";
1012

1113
export class ChildDatabaseRenderer {
1214
constructor(
1315
private readonly config: SyncConfig,
1416
private readonly publicApi: Client,
1517
private readonly deferredRenderer: DeferredRenderer,
16-
private readonly tableRenderer: TableRenderer,
17-
private readonly viewRenderer: DatabaseViewRenderer,
18+
private readonly tableRenderer: DatabaseTableRenderer,
19+
private readonly viewRenderer: DatabaseViewRenderer
1820
) {}
1921

20-
async renderChildDatabase(databaseId: string): Promise<string> {
22+
async renderChildDatabase(databaseId: string): Promise<Database> {
2123
const dbConfig = lookupDatabaseConfig(this.config, databaseId);
2224

23-
const msg = `<!-- included database ${databaseId} -->\n`;
24-
2525
// no view was defined for this database, render as a plain inline table
2626
const allPages = await this.fetchPages(databaseId, dbConfig);
2727

28-
const isCmsDb = this.config.cmsDatabaseId !== databaseId;
29-
if (isCmsDb && !dbConfig.views) {
30-
return msg + await this.tableRenderer.renderTable(allPages, dbConfig);
28+
const isCmsDb = this.config.cmsDatabaseId === databaseId;
29+
const renderPages = isCmsDb || dbConfig.views;
30+
31+
if (renderPages) {
32+
const entries = await this.queuePageRendering(allPages, dbConfig);
33+
const markdown = await this.viewRenderer.renderViews(entries, dbConfig);
34+
35+
return {
36+
config: dbConfig,
37+
entries,
38+
markdown,
39+
};
3140
}
3241

33-
// queue all pages in the database for individual, deferred rendering
34-
const prepareRenderPageTasks = allPages.map((x) =>
35-
this.deferredRenderer.renderPage(x, dbConfig)
42+
const entries = await this.queueEntryRendering(allPages, dbConfig);
43+
const markdown = this.tableRenderer.renderTable(entries);
44+
45+
return {
46+
config: dbConfig,
47+
entries,
48+
markdown,
49+
};
50+
}
51+
52+
private async queueEntryRendering(
53+
allPages: Page[],
54+
dbConfig: DatabaseConfig
55+
) {
56+
const prepareRenderEntryTasks = allPages.map((x) =>
57+
this.deferredRenderer.renderEntry(x, dbConfig)
3658
);
3759

3860
// note: the await here is not actually starting to render the pages, however it prepares the page render task
39-
const renderPageTasks = await Promise.all(prepareRenderPageTasks);
61+
return await Promise.all(prepareRenderEntryTasks);
62+
}
4063

41-
const db = {
42-
pages: renderPageTasks,
43-
config: dbConfig,
44-
};
64+
private async queuePageRendering(
65+
allPages: Page[],
66+
dbConfig: DatabaseConfig
67+
): Promise<RenderDatabasePageTask[]> {
68+
const prepareRenderPageTasks = allPages.map((x) =>
69+
this.deferredRenderer.renderPage(x, dbConfig)
70+
);
4571

46-
return this.viewRenderer.renderViews(db);
72+
// note: the await here is not actually starting to render the pages, however it prepares the page render task
73+
return await Promise.all(prepareRenderPageTasks);
4774
}
4875

4976
private async fetchPages(
5077
databaseId: string,
51-
dbConfig: DatabaseConfig,
78+
dbConfig: DatabaseConfig
5279
): Promise<Page[]> {
5380
const db = await this.publicApi.databases.retrieve({
5481
database_id: databaseId,
@@ -62,7 +89,7 @@ export class ChildDatabaseRenderer {
6289

6390
if (allPages.next_cursor) {
6491
throw new Error(
65-
`Paging not implemented, db ${db.id} has more than 100 entries`,
92+
`Paging not implemented, db ${db.id} has more than 100 entries`
6693
);
6794
}
6895

src/Database.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { DatabaseConfig } from "./SyncConfig";
2-
import { RenderPageTask } from "./RenderPageTask";
2+
import { RenderDatabasePageTask } from "./RenderDatabasePageTask";
3+
import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask";
34

45
export interface Database {
56
config: DatabaseConfig;
6-
pages: RenderPageTask[];
7+
entries: (RenderDatabaseEntryTask | RenderDatabasePageTask)[];
8+
markdown: string;
79
}

src/DatabaseEntryRenderer.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Page } from "@notionhq/client/build/src/api-types";
2+
3+
import { PropertiesParser } from "./PropertiesParser";
4+
import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask";
5+
import { DatabaseConfig } from "./SyncConfig";
6+
7+
export class DatabaseEntryRenderer {
8+
constructor(private readonly propertiesParser: PropertiesParser) {}
9+
10+
async renderEntry(
11+
page: Page,
12+
config: DatabaseConfig
13+
): Promise<RenderDatabaseEntryTask> {
14+
const props = await this.propertiesParser.parseProperties(page, config);
15+
16+
return {
17+
id: page.id,
18+
properties: {
19+
keys: props.keys,
20+
values: props.properties,
21+
},
22+
};
23+
}
24+
}

src/PageRenderer.ts renamed to src/DatabasePageRenderer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ import { FrontmatterRenderer } from './FrontmatterRenderer';
77
import { logger } from './logger';
88
import { PropertiesParser } from './PropertiesParser';
99
import { RecursiveBodyRenderer } from './RecursiveBodyRenderer';
10-
import { RenderPageTask as RenderPageTask } from './RenderPageTask';
10+
import { RenderDatabasePageTask as RenderDatabasePageTask } from "./RenderDatabasePageTask";
1111
import { slugify } from './slugify';
1212
import { DatabaseConfig } from './SyncConfig';
1313

1414
const fs = fsc.promises;
1515

16-
export class PageRenderer {
16+
export class DatabasePageRenderer {
1717
constructor(
1818
readonly propertiesParser: PropertiesParser,
1919
readonly frontmatterRenderer: FrontmatterRenderer,
2020
readonly bodyRenderer: RecursiveBodyRenderer
2121
) {}
2222

23-
async renderPage(page: Page, config: DatabaseConfig): Promise<RenderPageTask> {
23+
async renderPage(page: Page, config: DatabaseConfig): Promise<RenderDatabasePageTask> {
2424
if (page.archived){
2525
logger.warn(`rendering archived page ${page.url}`);
2626
}

src/DatabaseTableRenderer.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as markdownTable from "./markdown-table";
2+
import { RenderDatabaseEntryTask } from "./RenderDatabaseEntryTask";
3+
4+
export class DatabaseTableRenderer {
5+
public renderTable(entries: RenderDatabaseEntryTask[]): string {
6+
const table: any[][] = [];
7+
8+
for (const page of entries) {
9+
if (table.length === 0) {
10+
const headers = Array.from(page.properties.keys.keys());
11+
table[0] = headers;
12+
}
13+
14+
const cols = Array.from(page.properties.keys.values()).map((c, i) =>
15+
DatabaseTableRenderer.escapeTableCell(
16+
page.properties.values[c]
17+
)
18+
);
19+
20+
table.push(cols);
21+
}
22+
23+
return markdownTable.markdownTable(table);
24+
}
25+
26+
static escapeTableCell(content: string | number | any): string {
27+
// markdown table cells do not support newlines, however we can insert <br> elements instead
28+
if (typeof content === "string") {
29+
return content.replace(/\n/g, "<br>");
30+
}
31+
32+
return content.toString();
33+
}
34+
}

0 commit comments

Comments
 (0)