mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 15:09:33 +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;
|
data = rawContent.data;
|
||||||
type = rawContent.type;
|
type = rawContent.type;
|
||||||
} else if (data && type) {
|
} else if (data && type) {
|
||||||
data = await this.db.content.extractAttachments({
|
data = await this.db.content.postProcess({
|
||||||
data,
|
data,
|
||||||
type,
|
type,
|
||||||
noteId: id
|
noteId: id
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import Database from "../api";
|
|||||||
import { getOutputType } from "./attachments";
|
import { getOutputType } from "./attachments";
|
||||||
import { SQLCollection } from "../database/sql-collection";
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
import { NoteContent } from "./session-content";
|
import { NoteContent } from "./session-content";
|
||||||
|
import { InternalLink } from "../utils/internal-link";
|
||||||
|
|
||||||
export const EMPTY_CONTENT = (noteId: string): UnencryptedContentItem => ({
|
export const EMPTY_CONTENT = (noteId: string): UnencryptedContentItem => ({
|
||||||
noteId,
|
noteId,
|
||||||
@@ -96,7 +97,7 @@ export class Content implements ICollection {
|
|||||||
typeof content.data === "string" ? content.data : undefined;
|
typeof content.data === "string" ? content.data : undefined;
|
||||||
|
|
||||||
if (unencryptedData && content.type && content.noteId)
|
if (unencryptedData && content.type && content.noteId)
|
||||||
unencryptedData = await this.extractAttachments({
|
unencryptedData = await this.postProcess({
|
||||||
type: content.type,
|
type: content.type,
|
||||||
data: unencryptedData,
|
data: unencryptedData,
|
||||||
noteId: content.noteId
|
noteId: content.noteId
|
||||||
@@ -299,19 +300,22 @@ export class Content implements ICollection {
|
|||||||
await this.add(contentItem);
|
await this.add(contentItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
async extractAttachments(
|
async postProcess(contentItem: NoteContent<false> & { noteId: string }) {
|
||||||
contentItem: NoteContent<false> & { noteId: string }
|
|
||||||
) {
|
|
||||||
// if (contentItem.localOnly) return contentItem;
|
|
||||||
|
|
||||||
const content = getContentFromData(contentItem.type, contentItem.data);
|
const content = getContentFromData(contentItem.type, contentItem.data);
|
||||||
if (!content) return contentItem.data;
|
if (!content) return contentItem.data;
|
||||||
const { data, hashes } = await content.extractAttachments(
|
const { data, hashes, internalLinks } = await content.postProcess(
|
||||||
this.db.attachments.save
|
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
|
const noteAttachments = await this.db.relations
|
||||||
.from({ type: "note", id: contentItem.noteId }, "attachment")
|
.from({ type: "note", id: noteId }, "attachment")
|
||||||
.selector.filter.select(["id", "hash"])
|
.selector.filter.select(["id", "hash"])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
@@ -322,7 +326,7 @@ export class Content implements ICollection {
|
|||||||
for (const attachment of toDelete) {
|
for (const attachment of toDelete) {
|
||||||
await this.db.relations.unlink(
|
await this.db.relations.unlink(
|
||||||
{
|
{
|
||||||
id: contentItem.noteId,
|
id: noteId,
|
||||||
type: "note"
|
type: "note"
|
||||||
},
|
},
|
||||||
{ id: attachment.id, type: "attachment" }
|
{ id: attachment.id, type: "attachment" }
|
||||||
@@ -340,14 +344,51 @@ export class Content implements ICollection {
|
|||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
await this.db.relations.add(
|
await this.db.relations.add(
|
||||||
{
|
{
|
||||||
id: contentItem.noteId,
|
id: noteId,
|
||||||
type: "note"
|
type: "note"
|
||||||
},
|
},
|
||||||
{ id: attachment.id, type: "attachment" }
|
{ 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";
|
} from "../utils/html-parser";
|
||||||
import { HTMLRewriter } from "../utils/html-rewriter";
|
import { HTMLRewriter } from "../utils/html-rewriter";
|
||||||
import { ContentBlock } from "../types";
|
import { ContentBlock } from "../types";
|
||||||
|
import { InternalLink, parseInternalLink } from "../utils/internal-link";
|
||||||
|
|
||||||
export type ResolveHashes = (
|
export type ResolveHashes = (
|
||||||
hashes: string[]
|
hashes: string[]
|
||||||
@@ -46,6 +47,7 @@ const ATTRIBUTES = {
|
|||||||
mime: "data-mime",
|
mime: "data-mime",
|
||||||
filename: "data-filename",
|
filename: "data-filename",
|
||||||
src: "src",
|
src: "src",
|
||||||
|
href: "href",
|
||||||
blockId: "data-block-id"
|
blockId: "data-block-id"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,8 +144,8 @@ export class Tiptap {
|
|||||||
}).transform(this.data);
|
}).transform(this.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async extractAttachments(
|
async postProcess(
|
||||||
store: (
|
saveAttachment: (
|
||||||
data: string,
|
data: string,
|
||||||
mime: string,
|
mime: string,
|
||||||
filename?: string
|
filename?: string
|
||||||
@@ -151,13 +153,17 @@ export class Tiptap {
|
|||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!this.data.includes(ATTRIBUTES.src) &&
|
!this.data.includes(ATTRIBUTES.src) &&
|
||||||
!this.data.includes(ATTRIBUTES.hash)
|
!this.data.includes(ATTRIBUTES.hash) &&
|
||||||
|
// check for internal links
|
||||||
|
!this.data.includes("nn://")
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
data: this.data,
|
data: this.data,
|
||||||
hashes: []
|
hashes: [],
|
||||||
|
internalLinks: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const internalLinks: InternalLink[] = [];
|
||||||
const sources: {
|
const sources: {
|
||||||
src: string;
|
src: string;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
@@ -168,6 +174,7 @@ export class Tiptap {
|
|||||||
ontag: (name, attr, pos) => {
|
ontag: (name, attr, pos) => {
|
||||||
const hash = attr[ATTRIBUTES.hash];
|
const hash = attr[ATTRIBUTES.hash];
|
||||||
const src = attr[ATTRIBUTES.src];
|
const src = attr[ATTRIBUTES.src];
|
||||||
|
const href = attr[ATTRIBUTES.href];
|
||||||
if (name === "img" && !hash && src) {
|
if (name === "img" && !hash && src) {
|
||||||
sources.push({
|
sources.push({
|
||||||
src,
|
src,
|
||||||
@@ -175,6 +182,10 @@ export class Tiptap {
|
|||||||
mime: attr[ATTRIBUTES.mime],
|
mime: attr[ATTRIBUTES.mime],
|
||||||
id: `${pos.start}${pos.end}`
|
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);
|
}).parse(this.data);
|
||||||
@@ -184,7 +195,7 @@ export class Tiptap {
|
|||||||
try {
|
try {
|
||||||
const { data, mimeType } = dataurl.toObject(image.src);
|
const { data, mimeType } = dataurl.toObject(image.src);
|
||||||
if (!data || !mimeType) continue;
|
if (!data || !mimeType) continue;
|
||||||
const hash = await store(data, mimeType, image.filename);
|
const hash = await saveAttachment(data, mimeType, image.filename);
|
||||||
if (!hash) continue;
|
if (!hash) continue;
|
||||||
|
|
||||||
images[image.id] = hash;
|
images[image.id] = hash;
|
||||||
@@ -228,7 +239,8 @@ export class Tiptap {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: html,
|
data: html,
|
||||||
hashes
|
hashes,
|
||||||
|
internalLinks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,3 +21,8 @@ export * from "./types";
|
|||||||
export { VirtualizedGrouping } from "./utils/virtualized-grouping";
|
export { VirtualizedGrouping } from "./utils/virtualized-grouping";
|
||||||
export { DefaultColors } from "./collections/colors";
|
export { DefaultColors } from "./collections/colors";
|
||||||
export { type BackupFile, type LegacyBackupFile } from "./database/backup";
|
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