@jitl/notion-api
Version:
The missing companion library for the official Notion public API.
172 lines • 6.24 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.uuidWithDashes = exports.buildBacklinks = exports.Backlinks = exports.isNotionDomain = void 0;
/**
* Tools for building up a set of backlinks in-memory, because the API doesn't
* provide backlink information yet.
* @category Backlink
* @module
*/
const notion_api_1 = require("./notion-api");
const NOTION_DOMAINS = ['.notion.so', '.notion.site', '.notion.com'];
/**
* @category API
*/
function isNotionDomain(domain) {
return NOTION_DOMAINS.some((suffix) => domain.endsWith(suffix));
}
exports.isNotionDomain = isNotionDomain;
const DEBUG_BACKLINKS = notion_api_1.DEBUG.extend('backlinks');
/**
* Records links from a page to other pages.
* See [[buildBacklinks]].
* @category Backlink
*/
class Backlinks {
constructor() {
this.linksToPage = new Map();
this.pageLinksToPageIds = new Map();
}
add(args) {
const { mentionedPageId, mentionedFromPageId } = args;
const backlinks = this.linksToPage.get(mentionedPageId) || [];
this.linksToPage.set(mentionedPageId, backlinks);
backlinks.push(args);
const forwardLinks = this.pageLinksToPageIds.get(mentionedFromPageId) || new Set();
this.pageLinksToPageIds.set(mentionedFromPageId, forwardLinks);
forwardLinks.add(mentionedPageId);
DEBUG_BACKLINKS('added %s <-- %s', mentionedPageId, mentionedFromPageId);
return args;
}
maybeAddUrl(url, from) {
try {
const urlObject = new URL(url, 'https://www.notion.so');
if (!isNotionDomain(urlObject.host)) {
return undefined;
}
const path = urlObject.searchParams.get('p') || urlObject.pathname;
const idWithoutDashes = path.substring(path.length - 32);
if (idWithoutDashes.length !== 32) {
return undefined;
}
const uuid = uuidWithDashes(idWithoutDashes);
DEBUG_BACKLINKS('url %s --> %s', url, uuid);
return this.add({
...from,
mentionedPageId: uuid,
});
}
catch (error) {
console.warn('Invalid URL ', url, '', error);
return undefined;
}
}
maybeAddTextToken(token, from) {
if (token.type === 'mention') {
switch (token.mention.type) {
case 'database':
return this.add({
mentionedPageId: token.mention.database.id,
...from,
});
case 'page':
return this.add({
mentionedPageId: token.mention.page.id,
...from,
});
}
}
if (token.href) {
return this.maybeAddUrl(token.href, from);
}
}
getLinksToPage(pageId) {
return this.linksToPage.get(pageId) || [];
}
/**
* When we re-fetch a page and its children, we need to invalidate the old
* backlink data from those trees
*/
deleteBacklinksFromPage(mentionedFromPageId) {
const pagesToScan = this.pageLinksToPageIds.get(mentionedFromPageId);
this.pageLinksToPageIds.delete(mentionedFromPageId);
if (!pagesToScan) {
return;
}
for (const mentionedPageId of pagesToScan) {
const backlinks = this.linksToPage.get(mentionedPageId);
if (!backlinks) {
continue;
}
const newBacklinks = backlinks.filter((backlink) => backlink.mentionedFromPageId !== mentionedFromPageId);
if (newBacklinks.length === 0) {
this.linksToPage.delete(mentionedPageId);
DEBUG_BACKLINKS('removed all %s <-- %s', mentionedPageId, mentionedFromPageId);
}
else if (newBacklinks.length !== backlinks.length) {
this.linksToPage.set(mentionedPageId, newBacklinks);
DEBUG_BACKLINKS('removed all %s <-- %s', mentionedPageId, mentionedFromPageId);
}
}
}
}
exports.Backlinks = Backlinks;
/**
* Crawl the given `pages` and build all the backlinks between them into `backlinks`.
* If no [[Backlinks]] is given, a new one will be created and returned.
* @category Backlink
*/
function buildBacklinks(pages, backlinks = new Backlinks()) {
for (const page of pages) {
const fromPage = {
mentionedFromPageId: page.id,
mentionedFromBlockId: page.id,
};
(0, notion_api_1.visitTextTokens)(page, (token) => backlinks.maybeAddTextToken(token, fromPage));
(0, notion_api_1.visitChildBlocks)(page.children, (block) => {
const fromBlock = {
...fromPage,
mentionedFromBlockId: block.id,
};
(0, notion_api_1.visitTextTokens)(block, (token) => backlinks.maybeAddTextToken(token, fromBlock));
switch (block.type) {
case 'link_to_page': {
backlinks.add({
...fromBlock,
mentionedPageId: block.link_to_page.type === 'page_id'
? block.link_to_page.page_id
: block.link_to_page.database_id,
});
break;
}
case 'bookmark':
case 'link_preview':
case 'embed': {
const blockData = (0, notion_api_1.getBlockData)(block);
backlinks.maybeAddUrl(blockData.url, fromBlock);
break;
}
}
});
}
return backlinks;
}
exports.buildBacklinks = buildBacklinks;
/**
* Ensure a UUID has dashes, since sometimes Notion IDs don't have dashes.
* @category API
*/
function uuidWithDashes(id) {
if (id.includes('-')) {
return id;
}
return [
id.slice(0, 8),
id.slice(8, 12),
id.slice(12, 16),
id.slice(16, 20),
id.slice(20, 32),
].join('-');
}
exports.uuidWithDashes = uuidWithDashes;
//# sourceMappingURL=backlinks.js.map
;