From 43edb015c893b66c0794fde58b42ec163ce1da22 Mon Sep 17 00:00:00 2001 From: Alexander Alemayhu Date: Sat, 23 Apr 2022 21:44:05 +0200 Subject: [PATCH 1/4] feat: implement LinkToPage block --- server/lib/notion/BlockHandler.ts | 112 ++++++++++--------- server/lib/notion/blocks/BlockChildPage.tsx | 15 +-- server/lib/notion/blocks/LinkToPage.tsx | 16 +++ server/lib/notion/helpers/getBlockId.test.ts | 5 + server/lib/notion/helpers/getBlockId.ts | 9 ++ server/lib/notion/helpers/renderLink.tsx | 17 +++ 6 files changed, 111 insertions(+), 63 deletions(-) create mode 100644 server/lib/notion/blocks/LinkToPage.tsx create mode 100644 server/lib/notion/helpers/getBlockId.test.ts create mode 100644 server/lib/notion/helpers/getBlockId.ts create mode 100644 server/lib/notion/helpers/renderLink.tsx diff --git a/server/lib/notion/BlockHandler.ts b/server/lib/notion/BlockHandler.ts index 35781e3b4..c7cff86a8 100644 --- a/server/lib/notion/BlockHandler.ts +++ b/server/lib/notion/BlockHandler.ts @@ -40,6 +40,7 @@ import BlockEquation from './blocks/BlockEquation'; import renderFront from './helpers/renderFront'; import perserveNewlinesIfApplicable from './helpers/perserveNewlinesIfApplicable'; import getDeckName from '../anki/getDeckname'; +import LinkToPage from './blocks/LinkToPage'; class BlockHandler { api: NotionAPIWrapper; @@ -49,7 +50,11 @@ class BlockHandler { useAll: boolean = false; settings: Settings; - constructor(exporter: CustomExporter, api: NotionAPIWrapper, settings: Settings) { + constructor( + exporter: CustomExporter, + api: NotionAPIWrapper, + settings: Settings + ) { this.exporter = exporter; this.api = api; this.skip = []; @@ -198,6 +203,9 @@ class BlockHandler { case 'equation': back += BlockEquation(c); break; + case 'link_to_page': + back += await LinkToPage(c, this); + break; default: /* @ts-ignore */ back += `unsupported: ${c.type}`; @@ -257,62 +265,63 @@ class BlockHandler { let counter = 0; for (const block of flashcardBlocks) { - // Assume it's a basic card then check for children - const name = await renderFront(block, this); - let back: null | string = ''; - if (isColumnList(block) && rules.useColums()) { - const secondColumn = await getColumn(block.id, this, 1); - if (secondColumn) { - back = await BlockColumn(secondColumn, this) - } - } else { - back = await this.getBackSide(block); + // Assume it's a basic card then check for children + const name = await renderFront(block, this); + let back: null | string = ''; + if (isColumnList(block) && rules.useColums()) { + const secondColumn = await getColumn(block.id, this, 1); + if (secondColumn) { + back = await BlockColumn(secondColumn, this); } - const ankiNote = new Note(name, back || ''); - ankiNote.media = this.exporter.media; - let isBasicType = true; - // Look for cloze deletion cards - if (settings.isCloze) { - const clozeCard = await getClozeDeletionCard(rules, block); - if (clozeCard) { - isBasicType = false; - } - clozeCard && ankiNote.copyValues(clozeCard); + } else { + back = await this.getBackSide(block); + } + const ankiNote = new Note(name, back || ''); + ankiNote.media = this.exporter.media; + let isBasicType = true; + // Look for cloze deletion cards + if (settings.isCloze) { + const clozeCard = await getClozeDeletionCard(rules, block); + if (clozeCard) { + isBasicType = false; } - // Look for input cards - if (settings.useInput) { - const inputCard = await getInputCard(rules, block); - if (inputCard) { - isBasicType = false; - } - inputCard && ankiNote.copyValues(inputCard); + clozeCard && ankiNote.copyValues(clozeCard); + } + // Look for input cards + if (settings.useInput) { + const inputCard = await getInputCard(rules, block); + if (inputCard) { + isBasicType = false; } + inputCard && ankiNote.copyValues(inputCard); + } - ankiNote.back = back!; - ankiNote.notionLink = this.__notionLink(block.id, notionBaseLink); - if (settings.addNotionLink) { - ankiNote.back += RenderNotionLink(ankiNote.notionLink!, this); - } - ankiNote.notionId = settings.useNotionId ? block.id : undefined; - ankiNote.media = this.exporter.media; - this.exporter.media = []; + ankiNote.back = back!; + ankiNote.notionLink = this.__notionLink(block.id, notionBaseLink); + if (settings.addNotionLink) { + ankiNote.back += RenderNotionLink(ankiNote.notionLink!, this); + } + ankiNote.notionId = settings.useNotionId ? block.id : undefined; + ankiNote.media = this.exporter.media; + this.exporter.media = []; - const tr = TagRegistry.getInstance(); - ankiNote.tags = - rules.TAGS === 'heading' ? tr.headings : tr.strikethroughs; - ankiNote.number = counter++; + const tr = TagRegistry.getInstance(); + ankiNote.tags = + rules.TAGS === 'heading' ? tr.headings : tr.strikethroughs; + ankiNote.number = counter++; - ankiNote.name = perserveNewlinesIfApplicable(ankiNote.name, settings); - ankiNote.back = perserveNewlinesIfApplicable(ankiNote.back, settings); + ankiNote.name = perserveNewlinesIfApplicable(ankiNote.name, settings); + ankiNote.back = perserveNewlinesIfApplicable(ankiNote.back, settings); - cards.push(ankiNote); - if ( - !settings.isCherry && - (settings.basicReversed || ankiNote.hasRefreshIcon()) - && isBasicType) { - cards.push(ankiNote.reversed(ankiNote)); - } - tr.clear(); + cards.push(ankiNote); + if ( + !settings.isCherry && + (settings.basicReversed || ankiNote.hasRefreshIcon()) && + isBasicType + ) { + cards.push(ankiNote.reversed(ankiNote)); + } + tr.clear(); } if (settings.isCherry) { @@ -335,7 +344,6 @@ class BlockHandler { return cards; // .filter((c) => !c.isValid()); } - async findFlashcards( topLevelId: string, rules: ParserRules, @@ -412,7 +420,7 @@ class BlockHandler { if (settings.isAll) { /* @ts-ignore */ const subDecks = blocks.filter((b) => b.type === rules.SUB_DECKS); - for (const sd of subDecks) { + for (const sd of subDecks) { const subPage = await this.api.getPage(sd.id); if (subPage) { const nested = await this.findFlashcardsFromPage( diff --git a/server/lib/notion/blocks/BlockChildPage.tsx b/server/lib/notion/blocks/BlockChildPage.tsx index 7f0bb5216..1f3d1c526 100644 --- a/server/lib/notion/blocks/BlockChildPage.tsx +++ b/server/lib/notion/blocks/BlockChildPage.tsx @@ -1,15 +1,15 @@ import { GetBlockResponse } from "@notionhq/client/build/src/api-endpoints"; -import ReactDOMServer from "react-dom/server"; import BlockHandler from "../BlockHandler"; +import renderLink from "../helpers/renderLink"; -export const BlockChildPage = ( +export const BlockChildPage = async ( block: GetBlockResponse, handler: BlockHandler ) => { /* @ts-ignore */ const childPage = block.child_page; const api = handler.api; - const page = api.getPage(block.id) || {}; + const page = await api.getPage(block.id); /* @ts-ignore */ const icon = page.icon; @@ -17,12 +17,5 @@ export const BlockChildPage = ( return childPage.title; } - return ReactDOMServer.renderToStaticMarkup( - - {icon && icon.type === "emoji" && ( - {icon.emoji} - )} - {childPage.title} - - ); + return renderLink(childPage.title, block, icon); }; diff --git a/server/lib/notion/blocks/LinkToPage.tsx b/server/lib/notion/blocks/LinkToPage.tsx new file mode 100644 index 000000000..21a8e6e37 --- /dev/null +++ b/server/lib/notion/blocks/LinkToPage.tsx @@ -0,0 +1,16 @@ +import { GetBlockResponse } from '@notionhq/client/build/src/api-endpoints'; +import BlockHandler from '../BlockHandler'; +import renderLink from '../helpers/renderLink'; + +export default async function LinkToPage( + block: GetBlockResponse, + handler: BlockHandler +) { + /* @ts-ignore */ + const linkToPage = block.link_to_page; + const page = await handler.api.getPage(linkToPage.page_id); + const title = await handler.api.getPageTitle(page, handler.settings); + /* @ts-ignore */ + const icon = page.icon; + return renderLink(title, block, icon); +} diff --git a/server/lib/notion/helpers/getBlockId.test.ts b/server/lib/notion/helpers/getBlockId.test.ts new file mode 100644 index 000000000..fcc8f74fd --- /dev/null +++ b/server/lib/notion/helpers/getBlockId.test.ts @@ -0,0 +1,5 @@ +import { GetBlockResponse } from "@notionhq/client/build/src/api-endpoints"; + +export default function getBlockId(block: GetBlockResponse): string { + return block.id.replace(/-/g, ""); +}; \ No newline at end of file diff --git a/server/lib/notion/helpers/getBlockId.ts b/server/lib/notion/helpers/getBlockId.ts new file mode 100644 index 000000000..8470ad67c --- /dev/null +++ b/server/lib/notion/helpers/getBlockId.ts @@ -0,0 +1,9 @@ +import getBlockId from './getBlockId.test'; + +describe('getBlockId', () => { + test('should return the block id', () => { + expect( + getBlockId({ object: 'block', id: '1590db54-99fe-467c-a656-be319fe6ca8b' }) + ).toBe('1590db5499fe467ca656be319fe6ca8b'); + }); +}); diff --git a/server/lib/notion/helpers/renderLink.tsx b/server/lib/notion/helpers/renderLink.tsx new file mode 100644 index 000000000..935b0ed12 --- /dev/null +++ b/server/lib/notion/helpers/renderLink.tsx @@ -0,0 +1,17 @@ +import { GetBlockResponse } from "@notionhq/client/build/src/api-endpoints"; +import ReactDOMServer from "react-dom/server"; + +export default function renderLink( + title: string, + block: GetBlockResponse, + icon?: {type: string, emoji: string}, +) { + return ReactDOMServer.renderToStaticMarkup( + + {icon && icon.type === "emoji" && ( + {icon.emoji} + )} + {title} + + ); +} \ No newline at end of file From c2133d29de93b5312bed2debcdd8a3b84b178dfc Mon Sep 17 00:00:00 2001 From: Alexander Alemayhu Date: Sun, 24 Apr 2022 10:08:00 +0200 Subject: [PATCH 2/4] feat: implement LinkToPage block --- server/lib/notion/helpers/getBlockId.test.ts | 12 ++++++++---- server/lib/notion/helpers/getBlockId.ts | 14 ++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/server/lib/notion/helpers/getBlockId.test.ts b/server/lib/notion/helpers/getBlockId.test.ts index fcc8f74fd..350694304 100644 --- a/server/lib/notion/helpers/getBlockId.test.ts +++ b/server/lib/notion/helpers/getBlockId.test.ts @@ -1,5 +1,9 @@ -import { GetBlockResponse } from "@notionhq/client/build/src/api-endpoints"; +import getBlockId from './getBlockId'; -export default function getBlockId(block: GetBlockResponse): string { - return block.id.replace(/-/g, ""); -}; \ No newline at end of file +describe('getBlockId', () => { + test('should return the block id', () => { + expect( + getBlockId({ object: 'block', id: '1590db54-99fe-467c-a656-be319fe6ca8b' }) + ).toBe('1590db5499fe467ca656be319fe6ca8b'); + }); +}); diff --git a/server/lib/notion/helpers/getBlockId.ts b/server/lib/notion/helpers/getBlockId.ts index 8470ad67c..6c8356edf 100644 --- a/server/lib/notion/helpers/getBlockId.ts +++ b/server/lib/notion/helpers/getBlockId.ts @@ -1,9 +1,7 @@ -import getBlockId from './getBlockId.test'; +import { GetBlockResponse } from "@notionhq/client/build/src/api-endpoints"; -describe('getBlockId', () => { - test('should return the block id', () => { - expect( - getBlockId({ object: 'block', id: '1590db54-99fe-467c-a656-be319fe6ca8b' }) - ).toBe('1590db5499fe467ca656be319fe6ca8b'); - }); -}); +export default function getBlockId( + block: GetBlockResponse +): string { + return block.id.replace(/-/g, ''); +} \ No newline at end of file From 1b52bf0824cca161b079e97206b9a82f8766c824 Mon Sep 17 00:00:00 2001 From: Alexander Alemayhu Date: Sun, 24 Apr 2022 10:08:41 +0200 Subject: [PATCH 3/4] fix: bug in youtube embeds --- server/lib/notion/blocks/media/BlockEmbed.tsx | 7 ++++++ server/lib/notion/blocks/media/BlockVideo.tsx | 7 ++++-- server/lib/parser/DeckParser.ts | 23 +++++-------------- .../lib/parser/helpers/getYouTubeEmbedLink.ts | 3 +++ server/lib/parser/helpers/getYouTubeID.ts | 13 +++++++++++ 5 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 server/lib/parser/helpers/getYouTubeEmbedLink.ts create mode 100644 server/lib/parser/helpers/getYouTubeID.ts diff --git a/server/lib/notion/blocks/media/BlockEmbed.tsx b/server/lib/notion/blocks/media/BlockEmbed.tsx index 5e3d23b54..9e9895e3e 100644 --- a/server/lib/notion/blocks/media/BlockEmbed.tsx +++ b/server/lib/notion/blocks/media/BlockEmbed.tsx @@ -1,5 +1,7 @@ import { GetBlockResponse } from "@notionhq/client/build/src/api-endpoints"; import { renderToStaticMarkup } from "react-dom/server"; +import getYouTubeEmbedLink from "../../../parser/helpers/getYouTubeEmbedLink"; +import getYouTubeID from "../../../parser/helpers/getYouTubeID"; import BlockHandler from "../../BlockHandler"; export const BlockEmbed = (c: GetBlockResponse, handler: BlockHandler) => { @@ -18,6 +20,11 @@ export const BlockEmbed = (c: GetBlockResponse, handler: BlockHandler) => { {url} ); + } + + const yt = getYouTubeID(url); + if (yt) { + url = getYouTubeEmbedLink(yt); } } return renderToStaticMarkup( diff --git a/server/lib/notion/blocks/media/BlockVideo.tsx b/server/lib/notion/blocks/media/BlockVideo.tsx index bcf5b779e..4227cee02 100644 --- a/server/lib/notion/blocks/media/BlockVideo.tsx +++ b/server/lib/notion/blocks/media/BlockVideo.tsx @@ -1,5 +1,7 @@ import { GetBlockResponse } from "@notionhq/client/build/src/api-endpoints"; import { renderToStaticMarkup } from "react-dom/server"; +import getYouTubeEmbedLink from "../../../parser/helpers/getYouTubeEmbedLink"; +import getYouTubeID from "../../../parser/helpers/getYouTubeID"; import BlockHandler from "../../BlockHandler"; export const BlockVideo = (c: GetBlockResponse, handler: BlockHandler) => { @@ -10,8 +12,9 @@ export const BlockVideo = (c: GetBlockResponse, handler: BlockHandler) => { const video = c.video; let url = video.external.url; if (url) { - if (url.match("youtube.com/watch")) { - url = url.replace("watch?v=", "embed/"); + const yt = getYouTubeID(url); + if (yt) { + url = getYouTubeEmbedLink(yt); } else if (url.match("vimeo.com")) { url = url.replace("vimeo.com/", "player.vimeo.com/video/"); const videoId = url.split("/").pop().split("?")[0]; diff --git a/server/lib/parser/DeckParser.ts b/server/lib/parser/DeckParser.ts index a3fa514c8..cda076456 100644 --- a/server/lib/parser/DeckParser.ts +++ b/server/lib/parser/DeckParser.ts @@ -12,6 +12,8 @@ import handleClozeDeletions from "./helpers/handleClozeDeletions"; import sanitizeTags from "../anki/sanitizeTags"; import preserveNewlinesIfApplicable from "../notion/helpers/perserveNewlinesIfApplicable"; +import getYouTubeID from "./helpers/getYouTubeID"; +import getYouTubeEmbedLink from "./helpers/getYouTubeEmbedLink"; export class DeckParser { globalTags: cheerio.Cheerio | null; firstDeckName: string; @@ -338,20 +340,10 @@ export class DeckParser { } // https://stackoverflow.com/questions/6903823/regex-for-youtube-id - getYouTubeID(input: string) { + _getYouTubeID(input: string) { return this.ensureNotNull(input, () => { try { - const m = input.match( - /(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/user\/\S+|\/ytscreeningroom\?v=|\/sandalsResorts#\w\/\w\/.*\/))([^/&]{10,12})/ - ); - if (!m || m.length === 0) { - return null; - } - // prevent swallowing of soundcloud embeds - if (m[0].match(/https:\/\/soundcloud.com/)) { - return null; - } - return m[1]; + return getYouTubeID(input); } catch (error) { console.debug("error in getYouTubeID"); console.error(error); @@ -520,12 +512,9 @@ export class DeckParser { } } // Check YouTube - const id = this.getYouTubeID(card.back); + const id = this._getYouTubeID(card.back); if (id) { - const ytSrc = `https://www.youtube.com/embed/${id}?`.replace( - /"/, - "" - ); + const ytSrc = getYouTubeEmbedLink(id); const video = ``; card.back += video; } diff --git a/server/lib/parser/helpers/getYouTubeEmbedLink.ts b/server/lib/parser/helpers/getYouTubeEmbedLink.ts new file mode 100644 index 000000000..233945b3f --- /dev/null +++ b/server/lib/parser/helpers/getYouTubeEmbedLink.ts @@ -0,0 +1,3 @@ +export default function getYouTubeEmbedLink(id: string): string { + return `https://www.youtube.com/embed/${id}?`.replace(/"/, ''); +} diff --git a/server/lib/parser/helpers/getYouTubeID.ts b/server/lib/parser/helpers/getYouTubeID.ts new file mode 100644 index 000000000..4ca5e561c --- /dev/null +++ b/server/lib/parser/helpers/getYouTubeID.ts @@ -0,0 +1,13 @@ +export default function getYouTubeID(input: string): string | null { + const m = input.match( + /(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/user\/\S+|\/ytscreeningroom\?v=|\/sandalsResorts#\w\/\w\/.*\/))([^/&]{10,12})/ + ); + if (!m || m.length === 0) { + return null; + } + // prevent swallowing of soundcloud embeds + if (m[0].match(/https:\/\/soundcloud.com/)) { + return null; + } + return m[1]; +} From d98724ec18448596cd2efff0c5d37f9c6b79987f Mon Sep 17 00:00:00 2001 From: Alexander Alemayhu Date: Sat, 23 Apr 2022 22:29:29 +0200 Subject: [PATCH 4/4] feat: render the todo blocks with children --- server/lib/notion/BlockHandler.ts | 2 +- .../lib/notion/blocks/lists/BlockTodoList.tsx | 47 +++++++------------ server/lib/notion/helpers/getListItems.tsx | 22 ++++++--- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/server/lib/notion/BlockHandler.ts b/server/lib/notion/BlockHandler.ts index c7cff86a8..67b734af7 100644 --- a/server/lib/notion/BlockHandler.ts +++ b/server/lib/notion/BlockHandler.ts @@ -174,7 +174,7 @@ class BlockHandler { back += await BlockChildPage(c, this); break; case 'to_do': - back += BlockTodoList(c, this); + back += await BlockTodoList(c, response, this); break; case 'callout': back += BlockCallout(c, this); diff --git a/server/lib/notion/blocks/lists/BlockTodoList.tsx b/server/lib/notion/blocks/lists/BlockTodoList.tsx index 9fde3d945..f0e095bed 100644 --- a/server/lib/notion/blocks/lists/BlockTodoList.tsx +++ b/server/lib/notion/blocks/lists/BlockTodoList.tsx @@ -1,39 +1,26 @@ -import { GetBlockResponse } from '@notionhq/client/build/src/api-endpoints'; +import { GetBlockResponse, ListBlockChildrenResponse } from '@notionhq/client/build/src/api-endpoints'; import ReactDOMServer from 'react-dom/server'; import BlockHandler from '../../BlockHandler'; import { styleWithColors } from '../../NotionColors'; -import HandleBlockAnnotations from '../utils'; import { convert } from "html-to-text" +import getListItems from '../../helpers/getListItems'; -export const BlockTodoList = ( +export const BlockTodoList = async ( block: GetBlockResponse, + response: ListBlockChildrenResponse, handler: BlockHandler ) => { - /* @ts-ignore */ - const todo = block.to_do; - const text = todo.text; - - const markup = ReactDOMServer.renderToStaticMarkup( -
    - {text.map((t: GetBlockResponse) => { - /* @ts-ignore */ - const annotations = t.annotations; - /* @ts-ignore */ - return ( -
  • -
    - {/* @ts-ignore */} - {HandleBlockAnnotations(annotations, t.text)} -
  • - ); - })} -
- ); - if (handler.settings?.isTextOnlyBack) { - return convert(markup); - } - return markup; + /* @ts-ignore */ + const list = block.to_do; + const items = await getListItems(response, handler, "to_do"); + const listItems = items.filter(Boolean); + const markup = ReactDOMServer.renderToStaticMarkup( +
    + {listItems} +
+ ); + if (handler.settings?.isTextOnlyBack) { + return convert(markup); + } + return markup; }; diff --git a/server/lib/notion/helpers/getListItems.tsx b/server/lib/notion/helpers/getListItems.tsx index d2ff69d1c..92d48960c 100644 --- a/server/lib/notion/helpers/getListItems.tsx +++ b/server/lib/notion/helpers/getListItems.tsx @@ -5,7 +5,7 @@ import { styleWithColors } from '../NotionColors'; import BlockHandler from '../BlockHandler'; import getChildren from './getChildren'; -type ListType = "numbered_list_item" | "bulleted_list_item"; +type ListType = 'numbered_list_item' | 'bulleted_list_item' | 'to_do'; export default async function getListItems( response: ListBlockChildrenResponse, @@ -21,14 +21,24 @@ export default async function getListItems( } const backSide = await getChildren(result, handler); handler.skip.push(result.id); + const isTodo = type === 'to_do'; + const checked = isTodo && list.checked ? 'to-do-children-checked' : 'to-do-children-unchecked' + const checkedClass = isTodo ? checked : ''; + return ( -
  • +
  • + {isTodo && ( +
    + )} {renderTextChildren(list.text, handler.settings)} {backSide && ( -
    +
    )}
  • );