From bbceb6251a78820413010384f43566a73dbd1028 Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 1 Dec 2021 23:12:54 +0100 Subject: [PATCH] backlinks WIP, #2349 --- .idea/runConfigurations.xml | 10 -- package-lock.json | 32 ++-- package.json | 4 +- src/public/app/layouts/desktop_layout.js | 2 + .../app/services/frontend_script_api.js | 1 + src/public/app/widgets/backlinks.js | 143 ++++++++++++++++++ src/routes/api/note_map.js | 127 +++++++++++++++- src/routes/routes.js | 1 + 8 files changed, 291 insertions(+), 29 deletions(-) delete mode 100644 .idea/runConfigurations.xml create mode 100644 src/public/app/widgets/backlinks.js diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 797acea53..000000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 625163323..ba35cc1d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1920,9 +1920,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001282", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz", - "integrity": "sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==", + "version": "1.0.30001283", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz", + "integrity": "sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg==", "dev": true }, "caseless": { @@ -2903,9 +2903,9 @@ } }, "electron": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.1.tgz", - "integrity": "sha512-6TSDBcoKGgmKL/+W+LyaXidRVeRl1V4I81ZOWcqsVksdTMfM4AlxTgfaoYdK/nUhqBrUtuPDcqOyJE6Bc4qMpw==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.3.tgz", + "integrity": "sha512-MzCYuEqrvyEtPSUWQwr88xWBrsbhmyOKp4wqP9WfAJTEDeUfBcrQYswHuYe17Gi00gRirQb9htoC/anYfaw20w==", "dev": true, "requires": { "@electron/get": "^1.13.0", @@ -3702,9 +3702,9 @@ } }, "electron-to-chromium": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.0.tgz", - "integrity": "sha512-+oXCt6SaIu8EmFTPx8wNGSB0tHQ5biDscnlf6Uxuz17e9CjzMRtGk9B8705aMPnj0iWr3iC74WuIkngCsLElmA==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.8.tgz", + "integrity": "sha512-Cu5+dbg55+1E3ohlsa8HT0s4b8D0gBewXEGG8s5wBl8ynWv60VuvYW25GpsOeTVXpulhyU/U8JYZH+yxASSJBQ==", "dev": true }, "electron-window-state": { @@ -5100,9 +5100,9 @@ "dev": true }, "jest-worker": { - "version": "27.3.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz", - "integrity": "sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g==", + "version": "27.4.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.2.tgz", + "integrity": "sha512-0QMy/zPovLfUPyHuOuuU4E+kGACXXE84nRnq6lBVI9GJg5DCBiA97SATi+ZP8CpiJwEQy1oCPjRBf8AnLjN+Ag==", "dev": true, "requires": { "@types/node": "*", @@ -8118,9 +8118,9 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" }, "webpack": { - "version": "5.64.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.3.tgz", - "integrity": "sha512-XF6/IL9Bw2PPQioiR1UYA8Bs4tX3QXJtSelezKECdLFeSFzWoe44zqTzPW5N+xI3fACaRl2/G3sNA4WYHD7Iww==", + "version": "5.64.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.4.tgz", + "integrity": "sha512-LWhqfKjCLoYJLKJY8wk2C3h77i8VyHowG3qYNZiIqD6D0ZS40439S/KVuc/PY48jp2yQmy0mhMknq8cys4jFMw==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.0", @@ -8145,7 +8145,7 @@ "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.2.0", + "watchpack": "^2.3.0", "webpack-sources": "^3.2.2" } }, diff --git a/package.json b/package.json index b8119a2b0..2f3a79f6b 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ }, "devDependencies": { "cross-env": "7.0.3", - "electron": "16.0.1", + "electron": "16.0.3", "@electron/remote": "2.0.1", "electron-builder": "22.14.5", "electron-packager": "15.4.0", @@ -92,7 +92,7 @@ "jsdoc": "3.6.7", "lorem-ipsum": "2.0.4", "rcedit": "3.0.1", - "webpack": "5.64.3", + "webpack": "5.64.4", "webpack-cli": "4.9.1" }, "optionalDependencies": { diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index fc06413d4..242356dfc 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -46,6 +46,7 @@ import OpenNoteButtonWidget from "../widgets/buttons/open_note_button_widget.js" import MermaidWidget from "../widgets/mermaid.js"; import BookmarkButtons from "../widgets/bookmark_buttons.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; +import BacklinksWidget from "../widgets/backlinks.js"; export default class DesktopLayout { constructor(customWidgets) { @@ -147,6 +148,7 @@ export default class DesktopLayout { .button(new NoteActionsWidget()) ) .child(new NoteUpdateStatusWidget()) + .child(new BacklinksWidget()) .child(new MermaidWidget()) .child( new ScrollingContainer() diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index a4d5b04af..db8bc2d07 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -313,6 +313,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @param {object} [params] * @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link * @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link + * @param {boolean} [params.showNoteIcon=false] - show also note icon before the title * @param {string} [title=] - custom link tile with note's title as default */ this.createNoteLink = linkService.createNoteLink; diff --git a/src/public/app/widgets/backlinks.js b/src/public/app/widgets/backlinks.js new file mode 100644 index 000000000..2b1353b6a --- /dev/null +++ b/src/public/app/widgets/backlinks.js @@ -0,0 +1,143 @@ +import NoteContextAwareWidget from "./note_context_aware_widget.js"; +import linkService from "../services/link.js"; +import server from "../services/server.js"; +import froca from "../services/froca.js"; + +const TPL = ` + +`; + +export default class BacklinksWidget extends NoteContextAwareWidget { + doRender() { + this.$widget = $(TPL); + this.$count = this.$widget.find('.backlinks-count'); + this.$items = this.$widget.find('.backlinks-items'); + this.$ticker = this.$widget.find('.backlinks-ticker'); + + this.$count.on("click", () => { + this.$items.toggle(); + this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10); + + if (this.$items.is(":visible")) { + this.renderBacklinks(); + } + }); + + this.$closeTickerButton = this.$widget.find('.backlinks-close-ticker'); + this.$closeTickerButton.on("click", () => { + this.$ticker.hide(); + + this.clearItems(); + }); + + this.contentSized(); + } + + async refreshWithNote(note) { + this.clearItems(); + + const targetRelationCount = note.getTargetRelations().length; + if (targetRelationCount === 0) { + this.$ticker.hide(); + } + else { + this.$ticker.show(); + this.$count.text( + `${targetRelationCount} backlink` + + (targetRelationCount === 1 ? '' : 's') + ); + } + } + + clearItems() { + this.$items.empty().hide(); + } + + async renderBacklinks() { + if (!this.note) { + return; + } + + this.$items.empty(); + + const backlinks = await server.get(`note-map/${this.noteId}/backlinks`); + + if (!backlinks.length) { + return; + } + + await froca.getNotes(backlinks.map(bl => bl.noteId)); // prefetch all + + for (const backlink of backlinks) { + this.$items.append(await linkService.createNoteLink(backlink.noteId, { + showNoteIcon: true, + showNotePath: true, + showTooltip: false + })); + + this.$items.append("
"); + this.$items.append(...backlink.excerpts); + } + } +} diff --git a/src/routes/api/note_map.js b/src/routes/api/note_map.js index a14922b1d..5e14a7140 100644 --- a/src/routes/api/note_map.js +++ b/src/routes/api/note_map.js @@ -1,6 +1,7 @@ "use strict"; const becca = require("../../becca/becca"); +const { JSDOM } = require("jsdom"); function buildDescendantCountMap() { const noteIdToCountMap = {}; @@ -174,7 +175,131 @@ function getTreeMap(req) { }; } +function removeImages(document) { + const images = document.getElementsByTagName('img'); + while (images.length > 0) { + images[0].parentNode.removeChild(images[0]); + } +} + +function getBacklinks(req) { + const {noteId} = req.params; + const note = becca.getNote(noteId); + + if (!note) { + return [404, `Note ${noteId} was not found`]; + } + + let backlinks = note.getTargetRelations(); + + if (backlinks.length > 50) { + backlinks = backlinks.slice(0, 50); + } + + return backlinks.map(backlink => { + const sourceNote = backlink.note; + + const html = sourceNote.getContent(); + const dom = new JSDOM(html); + + const excerpts = []; + + const document = dom.window.document; + + removeImages(document); + + for (const linkEl of document.querySelectorAll("a")) { + const href = linkEl.getAttribute("href"); + + if (!href || !href.includes(noteId)) { + continue; + } + + linkEl.style.fontWeight = "bold"; + linkEl.style.backgroundColor = "yellow"; + + const LIMIT = 200; + let centerEl = linkEl; + + while (centerEl.tagName !== 'BODY' && centerEl.parentElement.textContent.length < LIMIT) { + centerEl = centerEl.parentElement; + } + + const sub = [centerEl]; + let counter = centerEl.textContent.length; + let left = centerEl; + let right = centerEl; + + while (true) { + let added = false; + + const prev = left.previousElementSibling; + + if (prev) { + const prevText = prev.textContent; + + if (prevText.length + counter > LIMIT) { + const prefix = prevText.substr(prevText.length - (LIMIT - counter)); + + const textNode = document.createTextNode("…" + prefix); + sub.unshift(textNode); + + break; + } + + left = prev; + sub.unshift(left); + counter += prevText.length; + added = true; + } + + const next = right.nextElementSibling; + + if (next) { + const nextText = next.textContent; + + if (nextText.length + counter > LIMIT) { + const suffix = nextText.substr(nextText.length - (LIMIT - counter)); + + const textNode = document.createTextNode(suffix + "…"); + sub.push(textNode); + + break; + } + + right = next; + sub.push(right); + counter += nextText.length; + added = true; + } + + if (!added) { + break; + } + } + + const div = document.createElement('div'); + div.classList.add("ck-content"); + div.classList.add("backlink-excerpt"); + + for (const childEl of sub) { + div.appendChild(childEl); + } + + const subHtml = div.outerHTML; + + excerpts.push(subHtml); + } + + return { + noteId: sourceNote.noteId, + excerpts + }; + }); +} + module.exports = { getLinkMap, - getTreeMap + getTreeMap, + getBacklinks }; diff --git a/src/routes/routes.js b/src/routes/routes.js index ce1871cfc..9cc3f5a83 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -260,6 +260,7 @@ function register(app) { apiRoute(POST, '/api/note-map/:noteId/tree', noteMapRoute.getTreeMap); apiRoute(POST, '/api/note-map/:noteId/link', noteMapRoute.getLinkMap); + apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks); apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote);