notionic 0.0.0-stage → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- package/LICENSE +21 -0
- package/README.md +261 -2
- package/dist/fetch.d.ts +62 -0
- package/dist/fetch.js +259 -0
- package/dist/files-DIc0m6LM.js +104 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +1266 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +162 -0
- package/dist/page-BmeweSIt.js +27 -0
- package/dist/styles.css +108 -0
- package/dist/types-DOxr5KpT.d.ts +1305 -0
- package/dist/utils-BL_oAR33.js +18 -0
- package/package.json +73 -4
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { n as toLinkedPage, r as toNotionPage } from "./page-BmeweSIt.js";
|
|
2
|
+
//#region src/bookmark-preview.ts
|
|
3
|
+
/** Long enough for a slow site, short enough not to stall a build. */
|
|
4
|
+
const TIMEOUT_MS = 5e3;
|
|
5
|
+
/** Open Graph tags live in <head>; stop reading after this much HTML. */
|
|
6
|
+
const MAX_BYTES = 524288;
|
|
7
|
+
const MAX_REDIRECTS = 5;
|
|
8
|
+
const USER_AGENT = "Mozilla/5.0 (compatible; notionic; +https://notionic.dev)";
|
|
9
|
+
/**
|
|
10
|
+
* Hosts a server shouldn't be asked to fetch on a page's behalf: loopback,
|
|
11
|
+
* private and link-local addresses (which include cloud metadata endpoints).
|
|
12
|
+
* Only literal addresses are checked; a public name that resolves to a private
|
|
13
|
+
* address still gets through.
|
|
14
|
+
*/
|
|
15
|
+
function isPrivateHost(hostname) {
|
|
16
|
+
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
17
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true;
|
|
18
|
+
const ipv4 = host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
19
|
+
if (ipv4) {
|
|
20
|
+
const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
|
|
21
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
22
|
+
}
|
|
23
|
+
if (host.includes(":")) return host === "::" || host === "::1" || host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80") || host.startsWith("::ffff:");
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
/** A public http(s) URL, or null. */
|
|
27
|
+
function fetchableUrl(url) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(url);
|
|
30
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
31
|
+
return isPrivateHost(parsed.hostname) ? null : parsed;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const ENTITIES = {
|
|
37
|
+
amp: "&",
|
|
38
|
+
lt: "<",
|
|
39
|
+
gt: ">",
|
|
40
|
+
quot: "\"",
|
|
41
|
+
apos: "'",
|
|
42
|
+
nbsp: " "
|
|
43
|
+
};
|
|
44
|
+
function decodeEntities(text) {
|
|
45
|
+
return text.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi, (match, entity) => {
|
|
46
|
+
if (entity[0] === "#") {
|
|
47
|
+
const code = entity[1] === "x" || entity[1] === "X" ? Number.parseInt(entity.slice(2), 16) : Number(entity.slice(1));
|
|
48
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
49
|
+
}
|
|
50
|
+
return ENTITIES[entity.toLowerCase()] ?? match;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function clean(value) {
|
|
54
|
+
return (value ? decodeEntities(value).replace(/\s+/g, " ").trim() : "") || void 0;
|
|
55
|
+
}
|
|
56
|
+
/** The attributes of each tag with the given name, lowercased keys. */
|
|
57
|
+
function tags(html, name) {
|
|
58
|
+
const found = [];
|
|
59
|
+
for (const [tag] of html.matchAll(new RegExp(`<${name}\\b[^>]*>`, "gi"))) {
|
|
60
|
+
const attributes = {};
|
|
61
|
+
for (const [, key, , quoted, bare] of tag.matchAll(/([\w:-]+)\s*=\s*(?:(["'])(.*?)\2|([^\s"'>]+))/g)) if (key) attributes[key.toLowerCase()] = quoted ?? bare ?? "";
|
|
62
|
+
found.push(attributes);
|
|
63
|
+
}
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
/** An absolute http(s) URL resolved against the page, or undefined. */
|
|
67
|
+
function absolute(value, base) {
|
|
68
|
+
if (!value) return void 0;
|
|
69
|
+
try {
|
|
70
|
+
const url = new URL(decodeEntities(value.trim()), base);
|
|
71
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.href : void 0;
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The preview a page describes in its <head>: Open Graph and Twitter tags,
|
|
78
|
+
* falling back to <title>, the meta description and the favicon. Null
|
|
79
|
+
* without a title.
|
|
80
|
+
*/
|
|
81
|
+
function parseBookmarkPreview(html, pageUrl) {
|
|
82
|
+
const base = new URL(pageUrl);
|
|
83
|
+
const headEnd = html.search(/<\/head>/i);
|
|
84
|
+
const head = headEnd === -1 ? html : html.slice(0, headEnd);
|
|
85
|
+
const meta = {};
|
|
86
|
+
for (const attributes of tags(head, "meta")) {
|
|
87
|
+
const key = (attributes.property ?? attributes.name)?.toLowerCase();
|
|
88
|
+
if (key && attributes.content !== void 0 && !(key in meta)) meta[key] = attributes.content;
|
|
89
|
+
}
|
|
90
|
+
const titleTag = head.match(/<title\b[^>]*>([\s\S]*?)<\/title>/i)?.[1];
|
|
91
|
+
const title = clean(meta["og:title"] ?? meta["twitter:title"] ?? titleTag);
|
|
92
|
+
if (!title) return null;
|
|
93
|
+
const icons = tags(head, "link").filter((link) => (link.rel ?? "").toLowerCase().split(/\s+/).includes("icon"));
|
|
94
|
+
return {
|
|
95
|
+
title,
|
|
96
|
+
description: clean(meta["og:description"] ?? meta["twitter:description"] ?? meta.description),
|
|
97
|
+
image: absolute(meta["og:image"] ?? meta["og:image:url"] ?? meta["twitter:image"], base),
|
|
98
|
+
favicon: absolute(icons[0]?.href, base) ?? new URL("/favicon.ico", base).href,
|
|
99
|
+
siteName: clean(meta["og:site_name"])
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Reads up to MAX_BYTES of a response body as text. */
|
|
103
|
+
async function readHead(response) {
|
|
104
|
+
if (!response.body) return "";
|
|
105
|
+
const reader = response.body.getReader();
|
|
106
|
+
const decoder = new TextDecoder();
|
|
107
|
+
let html = "";
|
|
108
|
+
let bytes = 0;
|
|
109
|
+
while (bytes < MAX_BYTES) {
|
|
110
|
+
const { done, value } = await reader.read();
|
|
111
|
+
if (done) break;
|
|
112
|
+
bytes += value.byteLength;
|
|
113
|
+
html += decoder.decode(value, { stream: true });
|
|
114
|
+
if (/<\/head>/i.test(html)) break;
|
|
115
|
+
}
|
|
116
|
+
await reader.cancel().catch(() => {});
|
|
117
|
+
return html;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The preview card data for a bookmarked page, read from its HTML, or null
|
|
121
|
+
* when the page can't be fetched, isn't HTML, or has no title.
|
|
122
|
+
*/
|
|
123
|
+
async function fetchBookmarkPreview(url) {
|
|
124
|
+
let target = fetchableUrl(url);
|
|
125
|
+
const signal = AbortSignal.timeout(TIMEOUT_MS);
|
|
126
|
+
try {
|
|
127
|
+
for (let hop = 0; target && hop <= MAX_REDIRECTS; hop++) {
|
|
128
|
+
const response = await fetch(target, {
|
|
129
|
+
headers: {
|
|
130
|
+
accept: "text/html,application/xhtml+xml",
|
|
131
|
+
"user-agent": USER_AGENT
|
|
132
|
+
},
|
|
133
|
+
redirect: "manual",
|
|
134
|
+
signal
|
|
135
|
+
});
|
|
136
|
+
const location = response.headers.get("location");
|
|
137
|
+
if (response.status >= 300 && response.status < 400 && location) {
|
|
138
|
+
await response.body?.cancel().catch(() => {});
|
|
139
|
+
target = fetchableUrl(new URL(location, target).href);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!response.ok || !/html/i.test(response.headers.get("content-type") ?? "")) {
|
|
143
|
+
await response.body?.cancel().catch(() => {});
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return parseBookmarkPreview(await readHead(response), target.href);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/fetch.ts
|
|
155
|
+
/** The URL of a block that links to another site: a bookmark, embed or link preview. */
|
|
156
|
+
function linkedUrl(block) {
|
|
157
|
+
if (!("type" in block)) return null;
|
|
158
|
+
if (block.type === "bookmark") return block.bookmark.url;
|
|
159
|
+
if (block.type === "embed") return block.embed.url;
|
|
160
|
+
if (block.type === "link_preview") return block.link_preview.url;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function isFullBlock(result) {
|
|
164
|
+
return "type" in result;
|
|
165
|
+
}
|
|
166
|
+
async function listChildren(client, blockId) {
|
|
167
|
+
const blocks = [];
|
|
168
|
+
let cursor;
|
|
169
|
+
do {
|
|
170
|
+
const page = await client.blocks.children.list({
|
|
171
|
+
block_id: blockId,
|
|
172
|
+
start_cursor: cursor
|
|
173
|
+
});
|
|
174
|
+
blocks.push(...page.results.filter(isFullBlock));
|
|
175
|
+
cursor = page.has_more ? page.next_cursor ?? void 0 : void 0;
|
|
176
|
+
} while (cursor);
|
|
177
|
+
return blocks;
|
|
178
|
+
}
|
|
179
|
+
/** A full page, or null when the integration can't read it. */
|
|
180
|
+
async function retrievePage(client, pageId) {
|
|
181
|
+
if (!client.pages) return null;
|
|
182
|
+
try {
|
|
183
|
+
const page = await client.pages.retrieve({ page_id: pageId });
|
|
184
|
+
return "properties" in page ? page : null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Maps items with at most `limit` mapper calls in flight. */
|
|
190
|
+
async function mapPool(items, limit, mapper) {
|
|
191
|
+
const results = new Array(items.length);
|
|
192
|
+
let next = 0;
|
|
193
|
+
async function worker() {
|
|
194
|
+
while (next < items.length) {
|
|
195
|
+
const i = next++;
|
|
196
|
+
results[i] = await mapper(items[i]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const workers = Math.min(Math.max(1, limit), items.length);
|
|
200
|
+
await Promise.all(Array.from({ length: workers }, worker));
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Every block of a page (or any block), with nested blocks attached as
|
|
205
|
+
* `children`, ready for `<NotionRenderer>`.
|
|
206
|
+
*
|
|
207
|
+
* Child pages and databases are separate documents, so their content is not
|
|
208
|
+
* fetched. Caching is left to your framework.
|
|
209
|
+
*/
|
|
210
|
+
async function fetchBlocks(client, blockId, options = {}) {
|
|
211
|
+
const { maxDepth = 5, concurrency = 3, bookmarkPreviews = true } = options;
|
|
212
|
+
return mapPool(await listChildren(client, blockId), concurrency, async (block) => {
|
|
213
|
+
if (block.type === "link_to_page" && block.link_to_page.type === "page_id") {
|
|
214
|
+
const page = await retrievePage(client, block.link_to_page.page_id);
|
|
215
|
+
return page ? {
|
|
216
|
+
...block,
|
|
217
|
+
linkedPage: toLinkedPage(page)
|
|
218
|
+
} : block;
|
|
219
|
+
}
|
|
220
|
+
if (block.type === "child_page") {
|
|
221
|
+
const page = await retrievePage(client, block.id);
|
|
222
|
+
return page ? {
|
|
223
|
+
...block,
|
|
224
|
+
linkedPage: toLinkedPage(page)
|
|
225
|
+
} : block;
|
|
226
|
+
}
|
|
227
|
+
const previewUrl = bookmarkPreviews ? linkedUrl(block) : null;
|
|
228
|
+
if (previewUrl) {
|
|
229
|
+
const preview = await fetchBookmarkPreview(previewUrl);
|
|
230
|
+
return preview ? {
|
|
231
|
+
...block,
|
|
232
|
+
bookmarkPreview: preview
|
|
233
|
+
} : block;
|
|
234
|
+
}
|
|
235
|
+
if (!block.has_children || maxDepth <= 0 || block.type === "child_database") return block;
|
|
236
|
+
return {
|
|
237
|
+
...block,
|
|
238
|
+
children: await fetchBlocks(client, block.id, {
|
|
239
|
+
maxDepth: maxDepth - 1,
|
|
240
|
+
concurrency,
|
|
241
|
+
bookmarkPreviews
|
|
242
|
+
})
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* A page's metadata (title, icon, cover, properties…) and every block of it.
|
|
248
|
+
* Throws if the page doesn't exist or isn't shared with the integration.
|
|
249
|
+
*/
|
|
250
|
+
async function fetchPage(client, pageId, options = {}) {
|
|
251
|
+
const [page, blocks] = await Promise.all([client.pages.retrieve({ page_id: pageId }), fetchBlocks(client, pageId, options)]);
|
|
252
|
+
if (!("properties" in page)) throw new Error(`notionic: page ${pageId} returned without its properties`);
|
|
253
|
+
return {
|
|
254
|
+
page: toNotionPage(page),
|
|
255
|
+
blocks
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
export { fetchBlocks, fetchPage };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//#region src/links.ts
|
|
2
|
+
const PLAIN_ID = /[0-9a-f]{32}$/i;
|
|
3
|
+
const DASHED_ID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
/** A page id as 32 lowercase hex characters, without dashes. */
|
|
5
|
+
function normalizePageId(id) {
|
|
6
|
+
return id.replace(/-/g, "").toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/** Links to a page on notion.so, which works for pages shared to the web. */
|
|
9
|
+
function defaultPageHref(pageId) {
|
|
10
|
+
return `https://www.notion.so/${normalizePageId(pageId)}`;
|
|
11
|
+
}
|
|
12
|
+
function matchPageId(segment) {
|
|
13
|
+
const match = segment.match(PLAIN_ID) ?? segment.match(DASHED_ID);
|
|
14
|
+
return match ? normalizePageId(match[0]) : null;
|
|
15
|
+
}
|
|
16
|
+
function isNotionHost(hostname) {
|
|
17
|
+
const host = hostname.toLowerCase();
|
|
18
|
+
return host === "notion.so" || host === "www.notion.so" || host === "notion.com" || host.endsWith(".notion.com") || host.endsWith(".notion.site");
|
|
19
|
+
}
|
|
20
|
+
/** The page id in a Notion page URL, or null for any other URL. */
|
|
21
|
+
function notionPageIdFromUrl(url) {
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = new URL(url);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (!isNotionHost(parsed.hostname)) return null;
|
|
29
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
30
|
+
return matchPageId(segments[segments.length - 1] ?? "");
|
|
31
|
+
}
|
|
32
|
+
/** A same-site path; never `//host` or `/\host`, which browsers read as another site. */
|
|
33
|
+
function internalPath(path) {
|
|
34
|
+
return /^\/(?![/\\])/.test(path) ? {
|
|
35
|
+
href: path,
|
|
36
|
+
external: false
|
|
37
|
+
} : null;
|
|
38
|
+
}
|
|
39
|
+
/** A plain URL or path as it should render, or null when empty or unsafe. */
|
|
40
|
+
function classify(href) {
|
|
41
|
+
const value = href.replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
42
|
+
if (!value) return null;
|
|
43
|
+
if (value.startsWith("#")) return {
|
|
44
|
+
href: value,
|
|
45
|
+
external: false
|
|
46
|
+
};
|
|
47
|
+
if (value.startsWith("/")) return internalPath(value);
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(value);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (url.protocol === "mailto:" || url.protocol === "tel:") return {
|
|
55
|
+
href: value,
|
|
56
|
+
external: false
|
|
57
|
+
};
|
|
58
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
59
|
+
return {
|
|
60
|
+
href: value,
|
|
61
|
+
external: true
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** The page id a Notion link points to: a notion.so URL or `/<id>` path. */
|
|
65
|
+
function linkedPageId(href) {
|
|
66
|
+
const value = href.trim();
|
|
67
|
+
if (value.startsWith("/")) return matchPageId(value.slice(1).split(/[?#/]/)[0] ?? "");
|
|
68
|
+
return notionPageIdFromUrl(value);
|
|
69
|
+
}
|
|
70
|
+
/** Where a link to a Notion page renders, or null to show its text without a link. */
|
|
71
|
+
function resolvePageLink(pageId, context) {
|
|
72
|
+
const href = context.pageHref(normalizePageId(pageId));
|
|
73
|
+
return href ? classify(href) : null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* How a link from Notion content should render, or null for text without a
|
|
77
|
+
* link (empty or unsafe). Notion page links go through `context.pageHref`.
|
|
78
|
+
*/
|
|
79
|
+
function resolveLink(href, context) {
|
|
80
|
+
if (!href) return null;
|
|
81
|
+
const pageId = linkedPageId(href);
|
|
82
|
+
return pageId ? resolvePageLink(pageId, context) : classify(href);
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/files.ts
|
|
86
|
+
/**
|
|
87
|
+
* The URL to render for a Notion file object (a media block's payload, or a
|
|
88
|
+
* callout's icon). Files uploaded to Notion go through `transformFileUrl`;
|
|
89
|
+
* external URLs are returned as they are. Null for other kinds.
|
|
90
|
+
*/
|
|
91
|
+
function resolveFileUrl(file, blockId, context) {
|
|
92
|
+
if (!file) return null;
|
|
93
|
+
if ("external" in file) return file.external.url;
|
|
94
|
+
if ("file" in file) {
|
|
95
|
+
const { url } = file.file;
|
|
96
|
+
return context.transformFileUrl ? context.transformFileUrl({
|
|
97
|
+
blockId,
|
|
98
|
+
url
|
|
99
|
+
}) : url;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { resolvePageLink as a, resolveLink as i, defaultPageHref as n, normalizePageId as r, resolveFileUrl as t };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { _ as PageHeading, a as DemoFrameProps, b as resolveFileUrl, c as InlineEquationProps, d as NotionBlockType, f as NotionPage, g as RichText, h as ResolvedLink, i as BookmarkPreview, l as LinkedPage, m as RenderContext, n as BlockOf, o as DemoProps, p as PageIcon, r as BlockProps, s as Demos, t as BlockComponents, u as NotionBlock, v as collectHeadings, w as PageObjectResponse, y as HostedFile } from "./types-DOxr5KpT.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/renderer.d.ts
|
|
4
|
+
export declare const defaultComponents: BlockComponents;
|
|
5
|
+
type ListKind = "bulleted" | "numbered" | "todo";
|
|
6
|
+
type BlockGroup = {
|
|
7
|
+
kind: "block";
|
|
8
|
+
block: NotionBlock;
|
|
9
|
+
} | {
|
|
10
|
+
kind: ListKind;
|
|
11
|
+
items: [NotionBlock, ...NotionBlock[]];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Merges consecutive list items of one kind into a group, since the API
|
|
15
|
+
* returns each item as its own block. Other blocks stand alone.
|
|
16
|
+
*/
|
|
17
|
+
export declare function groupBlocks(blocks: NotionBlock[]): BlockGroup[];
|
|
18
|
+
/** Blocks in order, with list items grouped into lists. */
|
|
19
|
+
export declare function BlockList({ blocks, context }: {
|
|
20
|
+
blocks: NotionBlock[] | undefined;
|
|
21
|
+
context: RenderContext;
|
|
22
|
+
}): import("react").JSX.Element[] | null;
|
|
23
|
+
/** One block, rendered by its component, with its nested blocks as children. */
|
|
24
|
+
export declare function Block({ block, context }: {
|
|
25
|
+
block: NotionBlock;
|
|
26
|
+
context: RenderContext;
|
|
27
|
+
}): import("react").JSX.Element | null;
|
|
28
|
+
type NotionRendererProps = {
|
|
29
|
+
/** Blocks from the Notion API, with nested blocks as `children` (see `fetchBlocks`). */
|
|
30
|
+
blocks: NotionBlock[];
|
|
31
|
+
/** Replace the built-in component for any block type. */
|
|
32
|
+
components?: BlockComponents;
|
|
33
|
+
/**
|
|
34
|
+
* Where links to Notion pages point, by page id (32 hex characters, no
|
|
35
|
+
* dashes). Return null to render the text without a link. Defaults to the
|
|
36
|
+
* page on notion.so.
|
|
37
|
+
*/
|
|
38
|
+
pageHref?: (pageId: string) => string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Rewrites the URL of each file uploaded to Notion (images, videos, files,
|
|
41
|
+
* icons). Notion's signed URLs expire after an hour, so point them at a
|
|
42
|
+
* route that re-signs them, e.g. `({ blockId }) => \`/api/notion/file/\${blockId}\``.
|
|
43
|
+
* External URLs aren't passed through it.
|
|
44
|
+
*/
|
|
45
|
+
transformFileUrl?: (file: HostedFile) => string;
|
|
46
|
+
/**
|
|
47
|
+
* Your demo components, by name. An embed of a URL ending in
|
|
48
|
+
* `/demos/<name>` renders `demos[name]` in its place, with the embed's
|
|
49
|
+
* caption below. They can be Client Components.
|
|
50
|
+
*/
|
|
51
|
+
demos?: Demos;
|
|
52
|
+
className?: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Renders Notion blocks. Works as a Server Component; it ships no client
|
|
56
|
+
* JavaScript of its own (demos ship theirs).
|
|
57
|
+
*/
|
|
58
|
+
export declare function NotionRenderer({ blocks, components, pageHref, transformFileUrl, demos, className }: NotionRendererProps): import("react").JSX.Element;
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/rich-text.d.ts
|
|
61
|
+
/** An anchor with notionic's link styling; external links open in a new tab. */
|
|
62
|
+
export declare function NotionLink({ link, className, children }: {
|
|
63
|
+
link: ResolvedLink;
|
|
64
|
+
className?: string;
|
|
65
|
+
children: ReactNode;
|
|
66
|
+
}): import("react").JSX.Element;
|
|
67
|
+
/** A Notion rich text array: annotations, colors, links and mentions. */
|
|
68
|
+
declare function RichText$1({ text, context }: {
|
|
69
|
+
text: RichText;
|
|
70
|
+
context: RenderContext;
|
|
71
|
+
}): import("react").JSX.Element[];
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/links.d.ts
|
|
74
|
+
/** A page id as 32 lowercase hex characters, without dashes. */
|
|
75
|
+
export declare function normalizePageId(id: string): string;
|
|
76
|
+
/** Links to a page on notion.so, which works for pages shared to the web. */
|
|
77
|
+
export declare function defaultPageHref(pageId: string): string;
|
|
78
|
+
/** Where a link to a Notion page renders, or null to show its text without a link. */
|
|
79
|
+
export declare function resolvePageLink(pageId: string, context: RenderContext): ResolvedLink | null;
|
|
80
|
+
/**
|
|
81
|
+
* How a link from Notion content should render, or null for text without a
|
|
82
|
+
* link (empty or unsafe). Notion page links go through `context.pageHref`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveLink(href: string | null | undefined, context: RenderContext): ResolvedLink | null;
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/colors.d.ts
|
|
87
|
+
/** The class for a Notion color (`red`, `red_background`…), or undefined for `default`. */
|
|
88
|
+
export declare function colorClass(color: string | undefined): string | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* A block's color: text color, or a background that bleeds past the text, so
|
|
91
|
+
* the text stays on the same edge as the blocks around it. Blocks with a
|
|
92
|
+
* frame of their own (quote, callout) use `colorClass` instead.
|
|
93
|
+
*/
|
|
94
|
+
export declare function blockColorClass(color: string | undefined): string | undefined;
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/utils.d.ts
|
|
97
|
+
/** The text of a rich text array, without formatting. */
|
|
98
|
+
export declare function plainText(text: RichText): string;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/page.d.ts
|
|
101
|
+
/** A page's title: the text of its `title` property. */
|
|
102
|
+
export declare function pageTitle(page: PageObjectResponse): string;
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/blocks/media.d.ts
|
|
105
|
+
/** The default frame around a demo: a bordered stage with the caption below. */
|
|
106
|
+
export declare function DemoFrame({ caption, context, children }: DemoFrameProps): import("react").JSX.Element;
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/blocks/code.d.ts
|
|
109
|
+
/** A Notion code language as people write it: "typescript" is "TypeScript". */
|
|
110
|
+
export declare function languageName(language: string): string;
|
|
111
|
+
//#endregion
|
|
112
|
+
export { type BlockComponents, type BlockOf, type BlockProps, type BookmarkPreview, type DemoFrameProps, type DemoProps, type Demos, type HostedFile, type InlineEquationProps, type LinkedPage, type NotionBlock, type NotionBlockType, type NotionPage, type NotionRendererProps, type RichText as NotionRichText, type PageHeading, type PageIcon, type RenderContext, type ResolvedLink, RichText$1 as RichText, collectHeadings, resolveFileUrl };
|