2019-06-03 22:55:59 +02:00
|
|
|
"use strict";
|
|
|
|
|
2024-02-17 11:42:19 +02:00
|
|
|
const becca = require('../../becca/becca');
|
2021-12-01 23:12:54 +01:00
|
|
|
const { JSDOM } = require("jsdom");
|
2023-05-08 00:02:08 +02:00
|
|
|
|
2023-04-23 15:36:34 +08:00
|
|
|
function buildDescendantCountMap(noteIdsToCount) {
|
|
|
|
if (!Array.isArray(noteIdsToCount)) {
|
|
|
|
throw new Error('noteIdsToCount: type error');
|
|
|
|
}
|
2019-06-03 22:55:59 +02:00
|
|
|
|
2023-04-23 15:36:34 +08:00
|
|
|
const noteIdToCountMap = Object.create(null);
|
2021-09-17 22:34:23 +02:00
|
|
|
|
|
|
|
function getCount(noteId) {
|
|
|
|
if (!(noteId in noteIdToCountMap)) {
|
|
|
|
const note = becca.getNote(noteId);
|
|
|
|
|
2022-07-09 13:27:57 +02:00
|
|
|
const hiddenImageNoteIds = note.getRelations('imageLink').map(rel => rel.value);
|
|
|
|
const childNoteIds = note.children.map(child => child.noteId);
|
|
|
|
const nonHiddenNoteIds = childNoteIds.filter(childNoteId => !hiddenImageNoteIds.includes(childNoteId));
|
|
|
|
|
|
|
|
noteIdToCountMap[noteId] = nonHiddenNoteIds.length;
|
2021-09-17 22:34:23 +02:00
|
|
|
|
|
|
|
for (const child of note.children) {
|
|
|
|
noteIdToCountMap[noteId] += getCount(child.noteId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return noteIdToCountMap[noteId];
|
|
|
|
}
|
2023-04-24 13:43:19 +08:00
|
|
|
noteIdsToCount.forEach((noteId) => {
|
|
|
|
getCount(noteId);
|
2023-04-23 15:36:34 +08:00
|
|
|
});
|
2021-09-17 22:34:23 +02:00
|
|
|
|
|
|
|
return noteIdToCountMap;
|
|
|
|
}
|
2023-01-15 21:04:17 +01:00
|
|
|
/**
|
|
|
|
* @param {BNote} note
|
2023-06-29 23:32:19 +02:00
|
|
|
* @param {int} depth
|
2023-01-15 21:04:17 +01:00
|
|
|
* @returns {string[]} noteIds
|
|
|
|
*/
|
2021-10-21 22:52:52 +02:00
|
|
|
function getNeighbors(note, depth) {
|
|
|
|
if (depth === 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const retNoteIds = [];
|
|
|
|
|
2021-11-01 21:11:16 +01:00
|
|
|
function isIgnoredRelation(relation) {
|
2023-01-06 20:31:55 +01:00
|
|
|
return ['relationMapLink', 'template', 'inherit', 'image', 'ancestor'].includes(relation.name);
|
2021-11-01 21:11:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// forward links
|
2021-10-21 22:52:52 +02:00
|
|
|
for (const relation of note.getRelations()) {
|
2021-11-01 21:11:16 +01:00
|
|
|
if (isIgnoredRelation(relation)) {
|
2021-10-21 22:52:52 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const targetNote = relation.getTargetNote();
|
2021-12-04 13:45:15 +01:00
|
|
|
|
2023-06-29 00:04:30 +02:00
|
|
|
if (!targetNote || targetNote.isLabelTruthy('excludeFromNoteMap')) {
|
2021-12-04 13:45:15 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:52:52 +02:00
|
|
|
retNoteIds.push(targetNote.noteId);
|
|
|
|
|
|
|
|
for (const noteId of getNeighbors(targetNote, depth - 1)) {
|
|
|
|
retNoteIds.push(noteId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 21:11:16 +01:00
|
|
|
// backward links
|
|
|
|
for (const relation of note.getTargetRelations()) {
|
|
|
|
if (isIgnoredRelation(relation)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const sourceNote = relation.getNote();
|
2021-12-04 13:45:15 +01:00
|
|
|
|
2023-06-29 00:04:30 +02:00
|
|
|
if (!sourceNote || sourceNote.isLabelTruthy('excludeFromNoteMap')) {
|
2021-12-04 13:45:15 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-11-01 21:11:16 +01:00
|
|
|
retNoteIds.push(sourceNote.noteId);
|
|
|
|
|
|
|
|
for (const noteId of getNeighbors(sourceNote, depth - 1)) {
|
|
|
|
retNoteIds.push(noteId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:52:52 +02:00
|
|
|
return retNoteIds;
|
|
|
|
}
|
|
|
|
|
2021-09-22 23:02:38 +02:00
|
|
|
function getLinkMap(req) {
|
2021-09-20 23:04:41 +02:00
|
|
|
const mapRootNote = becca.getNote(req.params.noteId);
|
2023-06-30 11:18:34 +02:00
|
|
|
// if the map root itself has "excludeFromNoteMap" attribute (journal typically) then there wouldn't be anything
|
|
|
|
// to display, so we'll just ignore it
|
2023-06-29 00:04:30 +02:00
|
|
|
const ignoreExcludeFromNoteMap = mapRootNote.isLabelTruthy('excludeFromNoteMap');
|
2022-11-07 23:56:53 +01:00
|
|
|
let unfilteredNotes;
|
|
|
|
|
|
|
|
if (mapRootNote.type === 'search') {
|
2023-06-30 11:18:34 +02:00
|
|
|
// for search notes, we want to consider the direct search results only without the descendants
|
2022-11-07 23:56:53 +01:00
|
|
|
unfilteredNotes = mapRootNote.getSearchResultNotes();
|
|
|
|
} else {
|
2022-12-23 15:32:11 +01:00
|
|
|
unfilteredNotes = mapRootNote.getSubtree({
|
|
|
|
includeArchived: false,
|
|
|
|
resolveSearch: true,
|
|
|
|
includeHidden: mapRootNote.isInHiddenSubtree()
|
|
|
|
}).notes;
|
2022-11-07 23:56:53 +01:00
|
|
|
}
|
2021-09-20 23:04:41 +02:00
|
|
|
|
2021-10-21 22:52:52 +02:00
|
|
|
const noteIds = new Set(
|
2022-11-07 23:56:53 +01:00
|
|
|
unfilteredNotes
|
2023-06-29 00:04:30 +02:00
|
|
|
.filter(note => ignoreExcludeFromNoteMap || !note.isLabelTruthy('excludeFromNoteMap'))
|
2021-10-21 22:52:52 +02:00
|
|
|
.map(note => note.noteId)
|
|
|
|
);
|
2021-09-20 20:02:23 +02:00
|
|
|
|
2022-11-07 23:19:38 +01:00
|
|
|
if (mapRootNote.type === 'search') {
|
|
|
|
noteIds.delete(mapRootNote.noteId);
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:52:52 +02:00
|
|
|
for (const noteId of getNeighbors(mapRootNote, 3)) {
|
|
|
|
noteIds.add(noteId);
|
|
|
|
}
|
|
|
|
|
2023-04-23 15:36:34 +08:00
|
|
|
const noteIdsArray = Array.from(noteIds)
|
|
|
|
|
|
|
|
const notes = noteIdsArray.map(noteId => {
|
2021-10-21 22:52:52 +02:00
|
|
|
const note = becca.getNote(noteId);
|
|
|
|
|
|
|
|
return [
|
2021-09-20 20:02:23 +02:00
|
|
|
note.noteId,
|
2022-01-08 21:50:16 +01:00
|
|
|
note.getTitleOrProtected(),
|
2022-12-20 20:41:51 +01:00
|
|
|
note.type,
|
|
|
|
note.getLabelValue('color')
|
2021-10-21 22:52:52 +02:00
|
|
|
];
|
|
|
|
});
|
2021-09-20 20:02:23 +02:00
|
|
|
|
|
|
|
const links = Object.values(becca.attributes).filter(rel => {
|
2023-01-06 20:31:55 +01:00
|
|
|
if (rel.type !== 'relation' || rel.name === 'relationMapLink' || rel.name === 'template' || rel.name === 'inherit') {
|
2021-09-17 22:34:23 +02:00
|
|
|
return false;
|
|
|
|
}
|
2021-09-20 20:02:23 +02:00
|
|
|
else if (!noteIds.has(rel.noteId) || !noteIds.has(rel.value)) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-09-17 22:34:23 +02:00
|
|
|
else if (rel.name === 'imageLink') {
|
|
|
|
const parentNote = becca.getNote(rel.noteId);
|
|
|
|
|
|
|
|
return !parentNote.getChildNotes().find(childNote => childNote.noteId === rel.value);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return true;
|
|
|
|
}
|
2021-09-20 20:02:23 +02:00
|
|
|
})
|
2022-11-07 23:56:53 +01:00
|
|
|
.map(rel => ({
|
2022-12-21 15:19:05 +01:00
|
|
|
id: `${rel.noteId}-${rel.name}-${rel.value}`,
|
2021-09-17 22:34:23 +02:00
|
|
|
sourceNoteId: rel.noteId,
|
|
|
|
targetNoteId: rel.value,
|
|
|
|
name: rel.name
|
|
|
|
}));
|
|
|
|
|
2021-09-20 20:02:23 +02:00
|
|
|
return {
|
|
|
|
notes: notes,
|
2023-04-23 15:36:34 +08:00
|
|
|
noteIdToDescendantCountMap: buildDescendantCountMap(noteIdsArray),
|
2021-09-20 20:02:23 +02:00
|
|
|
links: links
|
|
|
|
};
|
|
|
|
}
|
2021-09-17 22:34:23 +02:00
|
|
|
|
2021-09-22 23:02:38 +02:00
|
|
|
function getTreeMap(req) {
|
2021-09-20 23:04:41 +02:00
|
|
|
const mapRootNote = becca.getNote(req.params.noteId);
|
2023-06-30 11:18:34 +02:00
|
|
|
// if the map root itself has "excludeFromNoteMap" (journal typically) then there wouldn't be anything to display,
|
|
|
|
// so we'll just ignore it
|
2023-06-29 00:04:30 +02:00
|
|
|
const ignoreExcludeFromNoteMap = mapRootNote.isLabelTruthy('excludeFromNoteMap');
|
2022-12-23 15:32:11 +01:00
|
|
|
const subtree = mapRootNote.getSubtree({
|
|
|
|
includeArchived: false,
|
|
|
|
resolveSearch: true,
|
|
|
|
includeHidden: mapRootNote.isInHiddenSubtree()
|
|
|
|
});
|
2021-09-17 22:34:23 +02:00
|
|
|
|
2022-11-05 22:32:50 +01:00
|
|
|
const notes = subtree.notes
|
2023-06-29 00:04:30 +02:00
|
|
|
.filter(note => ignoreExcludeFromNoteMap || !note.isLabelTruthy('excludeFromNoteMap'))
|
2021-09-21 22:45:06 +02:00
|
|
|
.filter(note => {
|
|
|
|
if (note.type !== 'image' || note.getChildNotes().length > 0) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const imageLinkRelation = note.getTargetRelations().find(rel => rel.name === 'imageLink');
|
|
|
|
|
|
|
|
if (!imageLinkRelation) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return !note.getParentNotes().find(parentNote => parentNote.noteId === imageLinkRelation.noteId);
|
|
|
|
})
|
2021-09-17 22:34:23 +02:00
|
|
|
.map(note => [
|
|
|
|
note.noteId,
|
2022-01-08 21:50:16 +01:00
|
|
|
note.getTitleOrProtected(),
|
2022-12-20 20:41:51 +01:00
|
|
|
note.type,
|
|
|
|
note.getLabelValue('color')
|
2021-09-17 22:34:23 +02:00
|
|
|
]);
|
|
|
|
|
2022-08-02 17:00:47 +02:00
|
|
|
const noteIds = new Set();
|
2021-09-17 22:34:23 +02:00
|
|
|
notes.forEach(([noteId]) => noteIds.add(noteId));
|
|
|
|
|
2021-09-20 20:02:23 +02:00
|
|
|
const links = [];
|
|
|
|
|
2022-11-05 22:32:50 +01:00
|
|
|
for (const {parentNoteId, childNoteId} of subtree.relationships) {
|
|
|
|
if (!noteIds.has(parentNoteId) || !noteIds.has(childNoteId)) {
|
2021-09-17 22:34:23 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
links.push({
|
2022-11-05 22:32:50 +01:00
|
|
|
sourceNoteId: parentNoteId,
|
|
|
|
targetNoteId: childNoteId
|
2021-09-17 22:34:23 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-23 15:36:34 +08:00
|
|
|
const noteIdToDescendantCountMap = buildDescendantCountMap(Array.from(noteIds));
|
2022-11-05 22:32:50 +01:00
|
|
|
|
|
|
|
updateDescendantCountMapForSearch(noteIdToDescendantCountMap, subtree.relationships);
|
|
|
|
|
2021-09-17 22:34:23 +02:00
|
|
|
return {
|
|
|
|
notes: notes,
|
2022-11-05 22:32:50 +01:00
|
|
|
noteIdToDescendantCountMap: noteIdToDescendantCountMap,
|
2021-09-17 22:34:23 +02:00
|
|
|
links: links
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-11-05 22:32:50 +01:00
|
|
|
function updateDescendantCountMapForSearch(noteIdToDescendantCountMap, relationships) {
|
|
|
|
for (const {parentNoteId, childNoteId} of relationships) {
|
|
|
|
const parentNote = becca.notes[parentNoteId];
|
|
|
|
if (!parentNote || parentNote.type !== 'search') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
noteIdToDescendantCountMap[parentNote.noteId] = noteIdToDescendantCountMap[parentNoteId] || 0;
|
|
|
|
noteIdToDescendantCountMap[parentNote.noteId] += noteIdToDescendantCountMap[childNoteId] || 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-01 23:12:54 +01:00
|
|
|
function removeImages(document) {
|
|
|
|
const images = document.getElementsByTagName('img');
|
|
|
|
while (images.length > 0) {
|
|
|
|
images[0].parentNode.removeChild(images[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
const EXCERPT_CHAR_LIMIT = 200;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
function findExcerpts(sourceNote, referencedNoteId) {
|
|
|
|
const html = sourceNote.getContent();
|
|
|
|
const document = new JSDOM(html).window.document;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
const excerpts = [];
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
removeImages(document);
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
for (const linkEl of document.querySelectorAll("a")) {
|
|
|
|
const href = linkEl.getAttribute("href");
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
if (!href || !href.endsWith(referencedNoteId)) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
linkEl.classList.add("backlink-link");
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
let centerEl = linkEl;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2022-05-27 21:55:58 +02:00
|
|
|
while (centerEl.tagName !== 'BODY' && centerEl.parentElement?.textContent?.length <= EXCERPT_CHAR_LIMIT) {
|
2021-12-02 22:00:42 +01:00
|
|
|
centerEl = centerEl.parentElement;
|
|
|
|
}
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2023-01-15 21:04:17 +01:00
|
|
|
/** @var {HTMLElement[]} */
|
2021-12-02 22:00:42 +01:00
|
|
|
const excerptEls = [centerEl];
|
|
|
|
let excerptLength = centerEl.textContent.length;
|
|
|
|
let left = centerEl;
|
|
|
|
let right = centerEl;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
while (excerptLength < EXCERPT_CHAR_LIMIT) {
|
|
|
|
let added = false;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
const prev = left.previousElementSibling;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
if (prev) {
|
|
|
|
const prevText = prev.textContent;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
if (prevText.length + excerptLength > EXCERPT_CHAR_LIMIT) {
|
|
|
|
const prefix = prevText.substr(prevText.length - (EXCERPT_CHAR_LIMIT - excerptLength));
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2022-12-21 15:19:05 +01:00
|
|
|
const textNode = document.createTextNode(`…${prefix}`);
|
2021-12-02 22:00:42 +01:00
|
|
|
excerptEls.unshift(textNode);
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
break;
|
|
|
|
}
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
left = prev;
|
|
|
|
excerptEls.unshift(left);
|
|
|
|
excerptLength += prevText.length;
|
|
|
|
added = true;
|
|
|
|
}
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
const next = right.nextElementSibling;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
if (next) {
|
|
|
|
const nextText = next.textContent;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
if (nextText.length + excerptLength > EXCERPT_CHAR_LIMIT) {
|
|
|
|
const suffix = nextText.substr(nextText.length - (EXCERPT_CHAR_LIMIT - excerptLength));
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2022-12-21 15:19:05 +01:00
|
|
|
const textNode = document.createTextNode(`${suffix}…`);
|
2021-12-02 22:00:42 +01:00
|
|
|
excerptEls.push(textNode);
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
break;
|
2021-12-01 23:12:54 +01:00
|
|
|
}
|
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
right = next;
|
|
|
|
excerptEls.push(right);
|
|
|
|
excerptLength += nextText.length;
|
|
|
|
added = true;
|
|
|
|
}
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
if (!added) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
const excerptWrapper = document.createElement('div');
|
|
|
|
excerptWrapper.classList.add("ck-content");
|
|
|
|
excerptWrapper.classList.add("backlink-excerpt");
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
for (const childEl of excerptEls) {
|
|
|
|
excerptWrapper.appendChild(childEl);
|
|
|
|
}
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
excerpts.push(excerptWrapper.outerHTML);
|
|
|
|
}
|
|
|
|
return excerpts;
|
|
|
|
}
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2022-10-22 15:01:10 +02:00
|
|
|
function getFilteredBacklinks(note) {
|
|
|
|
return note.getTargetRelations()
|
|
|
|
// search notes have "ancestor" relations which are not interesting
|
2022-11-14 21:03:14 +01:00
|
|
|
.filter(relation => !!relation.getNote() && relation.getNote().type !== 'search');
|
2022-10-22 15:01:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function getBacklinkCount(req) {
|
|
|
|
const {noteId} = req.params;
|
|
|
|
|
2023-05-08 00:02:08 +02:00
|
|
|
const note = becca.getNoteOrThrow(noteId);
|
2022-10-22 15:01:10 +02:00
|
|
|
|
2023-05-08 00:02:08 +02:00
|
|
|
return {
|
|
|
|
count: getFilteredBacklinks(note).length
|
|
|
|
};
|
2022-10-22 15:01:10 +02:00
|
|
|
}
|
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
function getBacklinks(req) {
|
|
|
|
const {noteId} = req.params;
|
2023-05-08 00:02:08 +02:00
|
|
|
const note = becca.getNoteOrThrow(noteId);
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
let backlinksWithExcerptCount = 0;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2022-10-22 15:01:10 +02:00
|
|
|
return getFilteredBacklinks(note).map(backlink => {
|
2021-12-02 22:00:42 +01:00
|
|
|
const sourceNote = backlink.note;
|
2021-12-01 23:12:54 +01:00
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
if (sourceNote.type !== 'text' || backlinksWithExcerptCount > 50) {
|
|
|
|
return {
|
|
|
|
noteId: sourceNote.noteId,
|
|
|
|
relationName: backlink.name
|
|
|
|
};
|
2021-12-01 23:12:54 +01:00
|
|
|
}
|
|
|
|
|
2021-12-02 22:00:42 +01:00
|
|
|
backlinksWithExcerptCount++;
|
|
|
|
|
|
|
|
const excerpts = findExcerpts(sourceNote, noteId);
|
|
|
|
|
2021-12-01 23:12:54 +01:00
|
|
|
return {
|
|
|
|
noteId: sourceNote.noteId,
|
|
|
|
excerpts
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-06-03 22:55:59 +02:00
|
|
|
module.exports = {
|
2021-09-17 22:34:23 +02:00
|
|
|
getLinkMap,
|
2021-12-01 23:12:54 +01:00
|
|
|
getTreeMap,
|
2022-10-22 15:01:10 +02:00
|
|
|
getBacklinkCount,
|
2021-12-01 23:12:54 +01:00
|
|
|
getBacklinks
|
2020-06-20 12:31:38 +02:00
|
|
|
};
|