backlinks WIP, #2349

This commit is contained in:
zadam 2021-12-01 23:12:54 +01:00
parent 89f117da5b
commit bbceb6251a
8 changed files with 291 additions and 29 deletions

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

32
package-lock.json generated
View File

@ -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"
}
},

View File

@ -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": {

View File

@ -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()

View File

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

View File

@ -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 = `
<div class="backlinks-widget">
<style>
.backlinks-widget {
position: relative;
}
.backlinks-ticker {
position: absolute;
top: 10px;
right: 10px;
width: 130px;
border-radius: 10px;
border-color: var(--main-border-color);
background-color: var(--more-accented-background-color);
padding: 4px 10px 4px 10px;
opacity: 70%;
display: flex;
justify-content: space-between;
align-items: center;
}
.backlinks-count {
cursor: pointer;
}
.backlinks-close-ticker {
cursor: pointer;
}
.backlinks-ticker:hover {
opacity: 100%;
}
.backlinks-items {
z-index: 10;
position: absolute;
top: 50px;
right: 10px;
width: 400px;
border-radius: 10px;
background-color: #eeeeee;
color: #444;
padding: 20px;
overflow-y: auto;
}
.backlink-excerpt {
border-left: 2px solid var(--main-border-color);
padding-left: 10px;
opacity: 80%;
font-size: 90%;
}
</style>
<div class="backlinks-ticker">
<span class="backlinks-count"></span>
<span class="bx bx-x backlinks-close-ticker"></span>
</div>
<div class="backlinks-items" style="display: none;"></div>
</div>
`;
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("<br/>");
this.$items.append(...backlink.excerpts);
}
}
}

View File

@ -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
};

View File

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