mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 06:59:31 +01:00
core: add support for processing internal links in note content
This commit is contained in:
@@ -306,7 +306,7 @@ export default class Vault {
|
||||
data = rawContent.data;
|
||||
type = rawContent.type;
|
||||
} else if (data && type) {
|
||||
data = await this.db.content.extractAttachments({
|
||||
data = await this.db.content.postProcess({
|
||||
data,
|
||||
type,
|
||||
noteId: id
|
||||
|
||||
@@ -33,6 +33,7 @@ import Database from "../api";
|
||||
import { getOutputType } from "./attachments";
|
||||
import { SQLCollection } from "../database/sql-collection";
|
||||
import { NoteContent } from "./session-content";
|
||||
import { InternalLink } from "../utils/internal-link";
|
||||
|
||||
export const EMPTY_CONTENT = (noteId: string): UnencryptedContentItem => ({
|
||||
noteId,
|
||||
@@ -96,7 +97,7 @@ export class Content implements ICollection {
|
||||
typeof content.data === "string" ? content.data : undefined;
|
||||
|
||||
if (unencryptedData && content.type && content.noteId)
|
||||
unencryptedData = await this.extractAttachments({
|
||||
unencryptedData = await this.postProcess({
|
||||
type: content.type,
|
||||
data: unencryptedData,
|
||||
noteId: content.noteId
|
||||
@@ -299,19 +300,22 @@ export class Content implements ICollection {
|
||||
await this.add(contentItem);
|
||||
}
|
||||
|
||||
async extractAttachments(
|
||||
contentItem: NoteContent<false> & { noteId: string }
|
||||
) {
|
||||
// if (contentItem.localOnly) return contentItem;
|
||||
|
||||
async postProcess(contentItem: NoteContent<false> & { noteId: string }) {
|
||||
const content = getContentFromData(contentItem.type, contentItem.data);
|
||||
if (!content) return contentItem.data;
|
||||
const { data, hashes } = await content.extractAttachments(
|
||||
const { data, hashes, internalLinks } = await content.postProcess(
|
||||
this.db.attachments.save
|
||||
);
|
||||
|
||||
await this.processInternalLinks(contentItem.noteId, internalLinks);
|
||||
await this.processLinkedAttachments(contentItem.noteId, hashes);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private async processLinkedAttachments(noteId: string, hashes: string[]) {
|
||||
const noteAttachments = await this.db.relations
|
||||
.from({ type: "note", id: contentItem.noteId }, "attachment")
|
||||
.from({ type: "note", id: noteId }, "attachment")
|
||||
.selector.filter.select(["id", "hash"])
|
||||
.execute();
|
||||
|
||||
@@ -322,7 +326,7 @@ export class Content implements ICollection {
|
||||
for (const attachment of toDelete) {
|
||||
await this.db.relations.unlink(
|
||||
{
|
||||
id: contentItem.noteId,
|
||||
id: noteId,
|
||||
type: "note"
|
||||
},
|
||||
{ id: attachment.id, type: "attachment" }
|
||||
@@ -340,14 +344,51 @@ export class Content implements ICollection {
|
||||
for (const attachment of attachments) {
|
||||
await this.db.relations.add(
|
||||
{
|
||||
id: contentItem.noteId,
|
||||
id: noteId,
|
||||
type: "note"
|
||||
},
|
||||
{ id: attachment.id, type: "attachment" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
private async processInternalLinks(
|
||||
noteId: string,
|
||||
internalLinks: InternalLink[]
|
||||
) {
|
||||
const links = await this.db.relations
|
||||
.from({ type: "note", id: noteId }, "note")
|
||||
.get();
|
||||
|
||||
const toDelete = links.filter((link) => {
|
||||
return internalLinks.every((l) => l.id !== link.toId);
|
||||
});
|
||||
|
||||
const toAdd = internalLinks.filter((link) => {
|
||||
return links.every((l) => link.id !== l.toId);
|
||||
});
|
||||
|
||||
for (const link of toDelete) {
|
||||
await this.db.relations.unlink(
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
},
|
||||
{ id: link.toId, type: link.toType }
|
||||
);
|
||||
}
|
||||
|
||||
for (const link of toAdd) {
|
||||
const note = await this.db.notes.exists(link.id);
|
||||
if (!note) continue;
|
||||
await this.db.relations.add(
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
},
|
||||
link
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "../utils/html-parser";
|
||||
import { HTMLRewriter } from "../utils/html-rewriter";
|
||||
import { ContentBlock } from "../types";
|
||||
import { InternalLink, parseInternalLink } from "../utils/internal-link";
|
||||
|
||||
export type ResolveHashes = (
|
||||
hashes: string[]
|
||||
@@ -46,6 +47,7 @@ const ATTRIBUTES = {
|
||||
mime: "data-mime",
|
||||
filename: "data-filename",
|
||||
src: "src",
|
||||
href: "href",
|
||||
blockId: "data-block-id"
|
||||
};
|
||||
|
||||
@@ -142,8 +144,8 @@ export class Tiptap {
|
||||
}).transform(this.data);
|
||||
}
|
||||
|
||||
async extractAttachments(
|
||||
store: (
|
||||
async postProcess(
|
||||
saveAttachment: (
|
||||
data: string,
|
||||
mime: string,
|
||||
filename?: string
|
||||
@@ -151,13 +153,17 @@ export class Tiptap {
|
||||
) {
|
||||
if (
|
||||
!this.data.includes(ATTRIBUTES.src) &&
|
||||
!this.data.includes(ATTRIBUTES.hash)
|
||||
!this.data.includes(ATTRIBUTES.hash) &&
|
||||
// check for internal links
|
||||
!this.data.includes("nn://")
|
||||
)
|
||||
return {
|
||||
data: this.data,
|
||||
hashes: []
|
||||
hashes: [],
|
||||
internalLinks: []
|
||||
};
|
||||
|
||||
const internalLinks: InternalLink[] = [];
|
||||
const sources: {
|
||||
src: string;
|
||||
filename?: string;
|
||||
@@ -168,6 +174,7 @@ export class Tiptap {
|
||||
ontag: (name, attr, pos) => {
|
||||
const hash = attr[ATTRIBUTES.hash];
|
||||
const src = attr[ATTRIBUTES.src];
|
||||
const href = attr[ATTRIBUTES.href];
|
||||
if (name === "img" && !hash && src) {
|
||||
sources.push({
|
||||
src,
|
||||
@@ -175,6 +182,10 @@ export class Tiptap {
|
||||
mime: attr[ATTRIBUTES.mime],
|
||||
id: `${pos.start}${pos.end}`
|
||||
});
|
||||
} else if (name === "a" && href && href.startsWith("nn://")) {
|
||||
const internalLink = parseInternalLink(href);
|
||||
if (!internalLink) return;
|
||||
internalLinks.push(internalLink);
|
||||
}
|
||||
}
|
||||
}).parse(this.data);
|
||||
@@ -184,7 +195,7 @@ export class Tiptap {
|
||||
try {
|
||||
const { data, mimeType } = dataurl.toObject(image.src);
|
||||
if (!data || !mimeType) continue;
|
||||
const hash = await store(data, mimeType, image.filename);
|
||||
const hash = await saveAttachment(data, mimeType, image.filename);
|
||||
if (!hash) continue;
|
||||
|
||||
images[image.id] = hash;
|
||||
@@ -228,7 +239,8 @@ export class Tiptap {
|
||||
|
||||
return {
|
||||
data: html,
|
||||
hashes
|
||||
hashes,
|
||||
internalLinks
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,8 @@ export * from "./types";
|
||||
export { VirtualizedGrouping } from "./utils/virtualized-grouping";
|
||||
export { DefaultColors } from "./collections/colors";
|
||||
export { type BackupFile, type LegacyBackupFile } from "./database/backup";
|
||||
export {
|
||||
createInternalLink,
|
||||
parseInternalLink,
|
||||
type InternalLink
|
||||
} from "./utils/internal-link";
|
||||
|
||||
61
packages/core/src/utils/internal-link.ts
Normal file
61
packages/core/src/utils/internal-link.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const InternalLinkTypes = ["note"] as const;
|
||||
type InternalLinkType = (typeof InternalLinkTypes)[number];
|
||||
export type InternalLink<T extends InternalLinkType = InternalLinkType> = {
|
||||
type: T;
|
||||
id: string;
|
||||
params?: Partial<InternalLinkParams[T]>;
|
||||
};
|
||||
type InternalLinkParams = {
|
||||
note: { blockId: string };
|
||||
};
|
||||
export function createInternalLink<T extends InternalLinkType>(
|
||||
type: T,
|
||||
id: string,
|
||||
params?: InternalLinkParams[T]
|
||||
) {
|
||||
let link = `nn://${type}/${id}`;
|
||||
if (params) {
|
||||
link +=
|
||||
"?" +
|
||||
Object.entries(params)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("&");
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
export function parseInternalLink(link: string): InternalLink | undefined {
|
||||
const url = new URL(link);
|
||||
if (url.protocol !== "nn:") return;
|
||||
const [type, id] = url.pathname.split("/").slice(2);
|
||||
if (!type || !id || !isValidInternalType(type)) return;
|
||||
|
||||
return {
|
||||
type,
|
||||
id,
|
||||
params: Object.fromEntries(url.searchParams.entries())
|
||||
};
|
||||
}
|
||||
|
||||
function isValidInternalType(type: string): type is InternalLinkType {
|
||||
return InternalLinkTypes.includes(type as any);
|
||||
}
|
||||
Reference in New Issue
Block a user