UNPKG

@jitl/notion-api

Version:

The missing companion library for the official Notion public API.

172 lines 6.24 kB
"use strict"; 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