diff --git a/packages/core/src/collections/notes.ts b/packages/core/src/collections/notes.ts index 5a6b8d5f0..0d5ea11f2 100644 --- a/packages/core/src/collections/notes.ts +++ b/packages/core/src/collections/notes.ts @@ -395,16 +395,21 @@ export class Notes implements ICollection { }); } - async getBlocks(id: string) { - const note = await this.collection.get(id); - if (note?.locked || !note?.contentId) return []; - const rawContent = await this.db.content.get(note.contentId); - if (!rawContent || rawContent.locked) return []; + async contentBlocks(id: string) { + const content = await this.db.content.findByNoteId(id); + if (!content || content.locked) return []; - return getContentFromData( - rawContent.type, - rawContent?.data - ).extractBlocks(); + return getContentFromData(content.type, content.data).extract("blocks") + .blocks; + } + + async internalLinks(id: string) { + const content = await this.db.content.findByNoteId(id); + if (!content || content.locked) return []; + + return getContentFromData(content.type, content.data).extract( + "internalLinks" + ).internalLinks; } } diff --git a/packages/core/src/content-types/tiptap.ts b/packages/core/src/content-types/tiptap.ts index fe2e0f577..0ce393d8f 100644 --- a/packages/core/src/content-types/tiptap.ts +++ b/packages/core/src/content-types/tiptap.ts @@ -18,8 +18,7 @@ along with this program. If not, see . */ import showdown from "@streetwriters/showdown"; -import render from "dom-serializer"; -import { find, isTag } from "domutils"; +import { findAll, isTag } from "domutils"; import { DomNode, FormatOptions, @@ -37,11 +36,19 @@ import { import { HTMLRewriter } from "../utils/html-rewriter"; import { ContentBlock } from "../types"; import { InternalLink, parseInternalLink } from "../utils/internal-link"; +import { Element } from "domhandler"; export type ResolveHashes = ( hashes: string[] ) => Promise>; +const ExtractableTypes = ["blocks", "internalLinks"] as const; +type ExtractableType = (typeof ExtractableTypes)[number]; +type ExtractionResult = { + blocks: ContentBlock[]; + internalLinks: InternalLink[]; +}; + const ATTRIBUTES = { hash: "data-hash", mime: "data-mime", @@ -108,28 +115,41 @@ export class Tiptap { }).transform(this.data); } - async extractBlocks() { - const nodes: ContentBlock[] = []; - const document = parseDocument(this.data); + extract(...types: ExtractableType[]): ExtractionResult { + const result: ExtractionResult = { blocks: [], internalLinks: [] }; + const document = parseDocument(this.data, { + withEndIndices: true, + withStartIndices: true + }); - const elements = find( - (element) => { - return isTag(element) && !!element.attribs[ATTRIBUTES.blockId]; - }, - document.childNodes, - false, - Infinity - ); - - for (const node of elements) { - if (!isTag(node)) continue; - nodes.push({ - id: node.attribs[ATTRIBUTES.blockId], - type: node.tagName.toLowerCase(), - content: convertHtmlToTxt(render(node)) - }); + if (types.includes("blocks")) { + result.blocks.push( + ...document.childNodes + .filter((element): element is Element => { + return isTag(element) && !!element.attribs[ATTRIBUTES.blockId]; + }) + .map((node) => ({ + id: node.attribs[ATTRIBUTES.blockId], + type: node.tagName.toLowerCase(), + content: convertHtmlToTxt( + this.data.slice(node.startIndex || 0, node.endIndex || 0) + ) + })) + ); } - return nodes; + + if (types.includes("internalLinks")) { + result.internalLinks.push( + ...findAll( + (e) => e.tagName === "a" && e.attribs.href.startsWith("nn://"), + document.childNodes + ) + .map((e) => parseInternalLink(e.attribs.href)) + .filter((v): v is InternalLink => !!v) + ); + } + + return result; } /** @@ -253,9 +273,15 @@ function convertHtmlToTxt(html: string) { { selector: "table", format: "dataTable" }, { selector: "ul.checklist", format: "taskList" }, { selector: "ul.simple-checklist", format: "checkList" }, - { selector: "p", format: "paragraph" } + { selector: "p", format: "paragraph" }, + { selector: `a[href^="nn://"]`, format: "internalLink" } ], formatters: { + internalLink: (elem, walk, builder) => { + builder.addInline(`[[${elem.attribs.href}|`); + walk(elem.children, builder); + builder.addInline("]]"); + }, taskList: (elem, walk, builder, formatOptions) => { return formatList(elem, walk, builder, formatOptions, (elem) => { return elem.attribs.class && elem.attribs.class.includes("checked") diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2e3b5d033..f6ec7caea 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -26,3 +26,8 @@ export { parseInternalLink, type InternalLink } from "./utils/internal-link"; +export { + extractInternalLinks, + highlightInternalLinks, + type TextSlice +} from "./utils/content-block"; diff --git a/packages/core/src/utils/content-block.ts b/packages/core/src/utils/content-block.ts new file mode 100644 index 000000000..f57827e19 --- /dev/null +++ b/packages/core/src/utils/content-block.ts @@ -0,0 +1,106 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { ContentBlock } from "../types"; +import { InternalLinkWithOffset, parseInternalLink } from "./internal-link"; + +const INTERNAL_LINK_REGEX = /(?:\[\[(nn:\/\/note\/.+?)\]\])/gm; +export function extractInternalLinks(block: ContentBlock) { + const matches = block.content.matchAll(INTERNAL_LINK_REGEX); + + const links: InternalLinkWithOffset[] = []; + for (const match of matches || []) { + if (!match.index) continue; + const url = match[1].slice(0, match[1].indexOf("|")); + const text = match[1].slice(match[1].indexOf("|") + 1); + const link = parseInternalLink(url); + if (!link) continue; + links.push({ + ...link, + start: match.index, + end: match.index + match[0].length, + text + }); + } + + return links; +} + +function normalize(block: ContentBlock, links: InternalLinkWithOffset[]) { + let diff = 0; + console.log(links); + for (const link of links) { + link.start -= diff; + link.end -= diff; + block.content = + block.content.slice(0, link.start) + + link.text + + block.content.slice(link.end); + diff += link.end - link.start - link.text.length; + + link.end = link.start + link.text.length; + } + return block; +} + +export type TextSlice = { text: string; highlighted: boolean }; +export function highlightInternalLinks( + block: ContentBlock, + noteId: string +): [TextSlice, TextSlice, TextSlice][] { + const links = extractInternalLinks(block); + normalize(block, links); + const highlighted: [TextSlice, TextSlice, TextSlice][] = []; + for (const link of links) { + const start = block.content.slice(0, link.start); + const end = block.content.slice(link.end); + if (link.id !== noteId) continue; + + highlighted.push([ + { + text: ellipsize(start, 50, "start"), + highlighted: false + }, + { + highlighted: link.id === noteId, + text: link.text + }, + { + highlighted: false, + text: ellipsize(end, 50, "end") + } + ]); + } + return highlighted; +} + +function ellipsize(text: string, maxLength: number, from: "start" | "end") { + const needsTruncation = text.length > maxLength; + const offsets = needsTruncation + ? from === "start" + ? [-maxLength, undefined] + : [0, maxLength] + : [0, text.length]; + const truncated = text.slice(offsets[0], offsets[1]); + return needsTruncation + ? from === "start" + ? "..." + truncated + : truncated + "..." + : truncated; +} diff --git a/packages/core/src/utils/internal-link.ts b/packages/core/src/utils/internal-link.ts index 8acf74e51..7f6fffd80 100644 --- a/packages/core/src/utils/internal-link.ts +++ b/packages/core/src/utils/internal-link.ts @@ -24,6 +24,14 @@ export type InternalLink = { id: string; params?: Partial; }; +export type InternalLinkWithOffset< + T extends InternalLinkType = InternalLinkType +> = InternalLink & { + start: number; + end: number; + text: string; +}; + type InternalLinkParams = { note: { blockId: string }; };