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);