-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Expand file tree
/
Copy pathclipper.ts
More file actions
223 lines (178 loc) · 6.88 KB
/
clipper.ts
File metadata and controls
223 lines (178 loc) · 6.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import type { Request } from "express";
import { parse } from "node-html-parser";
import path from "path";
import type BNote from "../../becca/entities/bnote.js";
import ValidationError from "../../errors/validation_error.js";
import appInfo from "../../services/app_info.js";
import attributeFormatter from "../../services/attribute_formatter.js";
import attributeService from "../../services/attributes.js";
import dateNoteService from "../../services/date_notes.js";
import dateUtils from "../../services/date_utils.js";
import htmlSanitizer from "../../services/html_sanitizer.js";
import imageService from "../../services/image.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import searchService from "../../services/search/services/search.js";
import utils from "../../services/utils.js";
import ws from "../../services/ws.js";
interface Image {
src: string;
dataUrl: string;
imageId: string;
}
async function addClipping(req: Request) {
// if a #clipType=clippings note exists with the same 'pageUrl' attribute,
// append the content to that note
// otherwise create a new note under clipperInbox (or today's note)
const { title, content, images } = req.body;
const clipType = "clippings";
const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl);
let clippingNote = findClippingNote(pageUrl, clipType);
if (!clippingNote) {
const clipperInbox = await getClipperInboxNote();
clippingNote = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
title,
content: "",
type: "text"
}).note;
clippingNote.setLabel("clipType", "clippings");
clippingNote.setLabel("pageUrl", pageUrl);
clippingNote.setLabel("iconClass", "bx bx-globe");
}
const rewrittenContent = processContent(images, clippingNote, content);
const existingContent = clippingNote.getContent();
if (typeof existingContent !== "string") {
throw new ValidationError("Invalid note content type.");
}
clippingNote.setContent(`${existingContent}${existingContent.trim() ? "<br>" : ""}${rewrittenContent}`);
return {
noteId: clippingNote.noteId
};
}
function findClippingNote(pageUrl: string, clipType: string | null) {
if (!pageUrl) {
return null;
}
const notes = searchService.searchNotes(
attributeFormatter.formatAttrForSearch(
{
type: "label",
name: "pageUrl",
value: pageUrl
},
true
)
);
return clipType ? notes.find((note) => note.getOwnedLabelValue("clipType") === clipType) : notes[0];
}
async function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel("clipperInbox");
if (!clipperInbox) {
clipperInbox = await dateNoteService.getDayNote(dateUtils.localNowDate());
}
return clipperInbox;
}
async function createNote(req: Request) {
const { content, images, labels } = req.body;
const clipType = htmlSanitizer.sanitize(req.body.clipType);
const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl);
const trimmedTitle = (typeof req.body.title === "string") ? req.body.title.trim() : "";
const title = trimmedTitle || `Clipped note from ${pageUrl}`;
const clipperInbox = await getClipperInboxNote();
let note = findClippingNote(pageUrl, clipType);
if (!note) {
note = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
title,
content: "",
type: "text"
}).note;
note.setLabel("clipType", clipType);
if (pageUrl) {
note.setLabel("pageUrl", pageUrl);
note.setLabel("iconClass", "bx bx-globe");
}
}
if (labels) {
for (const labelName in labels) {
const labelValue = htmlSanitizer.sanitize(labels[labelName]);
note.setLabel(labelName, labelValue);
}
}
const existingContent = note.getContent();
if (typeof existingContent !== "string") {
throw new ValidationError("Invalid note content type.");
}
const rewrittenContent = processContent(images, note, content);
const newContent = `${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`;
note.setContent(newContent);
noteService.asyncPostProcessContent(note, newContent); // to mark attachments as used
return {
noteId: note.noteId
};
}
export function processContent(images: Image[], note: BNote, content: string) {
let rewrittenContent = htmlSanitizer.sanitize(content);
if (images) {
for (const { src, dataUrl, imageId } of images) {
const filename = path.basename(src);
if (!dataUrl || !dataUrl.startsWith("data:image")) {
const excerpt = dataUrl ? dataUrl.substr(0, Math.min(100, dataUrl.length)) : "null";
log.info(`Image could not be recognized as data URL: ${excerpt}`);
continue;
}
const buffer = Buffer.from(dataUrl.split(",")[1], "base64");
const attachment = imageService.saveImageToAttachment(note.noteId, buffer, filename, true);
const encodedTitle = encodeURIComponent(attachment.title);
const url = `api/attachments/${attachment.attachmentId}/image/${encodedTitle}`;
log.info(`Replacing '${imageId}' with '${url}' in note '${note.noteId}'`);
rewrittenContent = utils.replaceAll(rewrittenContent, imageId, url);
}
}
// fallback if parsing/downloading images fails for some reason on the extension side (
rewrittenContent = noteService.downloadImages(note.noteId, rewrittenContent);
// Check if rewrittenContent contains at least one HTML tag
if (!/<.+?>/.test(rewrittenContent)) {
rewrittenContent = `<p>${rewrittenContent}</p>`;
}
// Create a JSDOM object from the existing HTML content
const dom = parse(rewrittenContent);
// Get the content inside the body tag and serialize it
rewrittenContent = dom.innerHTML ?? "";
return rewrittenContent;
}
function openNote(req: Request<{ noteId: string }>) {
if (utils.isElectron) {
ws.sendMessageToAllClients({
type: "openNote",
noteId: req.params.noteId
});
return {
result: "ok"
};
}
return {
result: "open-in-browser"
};
}
function handshake() {
return {
appName: "trilium",
protocolVersion: appInfo.clipperProtocolVersion
};
}
async function findNotesByUrl(req: Request<{ noteUrl: string }>) {
const pageUrl = req.params.noteUrl;
const foundPage = findClippingNote(pageUrl, null);
return {
noteId: foundPage ? foundPage.noteId : null
};
}
export default {
createNote,
addClipping,
openNote,
handshake,
findNotesByUrl
};