Merge remote-tracking branch 'origin/lightweight/decouple_server_api' into lightweight/browser_api

This commit is contained in:
Elian Doran 2026-01-06 19:33:35 +02:00
commit 9bce12a85b
No known key found for this signature in database
181 changed files with 8687 additions and 8424 deletions

View File

@ -1,14 +1,4 @@
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
type Multiplicity = "single" | "multi";
export interface DefinitionObject {
isPromoted?: boolean;
labelType?: LabelType;
multiplicity?: Multiplicity;
numberPrecision?: number;
promotedAlias?: string;
inverseRelation?: string;
}
import { DefinitionObject, LabelType, Multiplicity } from "@triliumnext/commons";
function parse(value: string) {
const tokens = value.split(",").map((t) => t.trim());

View File

@ -1,14 +1,16 @@
import { useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import "./UserAttributesList.css";
import { useTriliumEvent } from "../react/hooks";
import attributes from "../../services/attributes";
import { DefinitionObject } from "../../services/promoted_attribute_definition_parser";
import { formatDateTime } from "../../utils/formatters";
import type { DefinitionObject } from "@triliumnext/commons";
import { ComponentChildren, CSSProperties } from "preact";
import { useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { getReadableTextColor } from "../../services/css_class_manager";
import { formatDateTime } from "../../utils/formatters";
import { useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import NoteLink from "../react/NoteLink";
import { getReadableTextColor } from "../../services/css_class_manager";
interface UserAttributesListProps {
note: FNote;
@ -29,7 +31,7 @@ export default function UserAttributesDisplay({ note, ignoredAttributes }: UserA
<div className="user-attributes">
{userAttributes?.map(attr => buildUserAttribute(attr))}
</div>
)
);
}
@ -46,13 +48,13 @@ function useNoteAttributesWithDefinitions(note: FNote, attributesToIgnore: stri
}
function UserAttribute({ attr, children, style }: { attr: AttributeWithDefinitions, children: ComponentChildren, style?: CSSProperties }) {
const className = `${attr.type === "label" ? "label" + " " + attr.def.labelType : "relation"}`;
const className = `${attr.type === "label" ? `label` + ` ${ attr.def.labelType}` : "relation"}`;
return (
<span key={attr.friendlyName} className={`user-attribute type-${className}`} style={style}>
{children}
</span>
)
);
}
function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
@ -61,7 +63,7 @@ function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
let style: CSSProperties | undefined;
if (attr.type === "label") {
let value = attr.value;
const value = attr.value;
switch (attr.def.labelType) {
case "number":
let formattedValue = value;
@ -102,7 +104,7 @@ function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
content = <>{defaultLabel}<NoteLink notePath={attr.value} showNoteIcon /></>;
}
return <UserAttribute attr={attr} style={style}>{content}</UserAttribute>
return <UserAttribute attr={attr} style={style}>{content}</UserAttribute>;
}
function getAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {

View File

@ -1,11 +1,12 @@
import type { CellComponent, ColumnDefinition, EmptyCallback, FormatterParams, ValueBooleanCallback, ValueVoidCallback } from "tabulator-tables";
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
import { LabelType } from "@triliumnext/commons";
import { JSX } from "preact";
import { renderReactWidget } from "../../react/react_utils.jsx";
import Icon from "../../react/Icon.jsx";
import { useEffect, useRef, useState } from "preact/hooks";
import type { CellComponent, ColumnDefinition, EmptyCallback, FormatterParams, ValueBooleanCallback, ValueVoidCallback } from "tabulator-tables";
import froca from "../../../services/froca.js";
import Icon from "../../react/Icon.jsx";
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
import { renderReactWidget } from "../../react/react_utils.jsx";
type ColumnType = LabelType | "relation";
@ -78,7 +79,7 @@ export function buildColumnDefinitions({ info, movableRows, existingColumnData,
rowHandle: movableRows,
width: calculateIndexColumnWidth(rowNumberHint, movableRows),
formatter: wrapFormatter(({ cell, formatterParams }) => <div>
{(formatterParams as RowNumberFormatterParams).movableRows && <><span class="bx bx-dots-vertical-rounded"></span>{" "}</>}
{(formatterParams as RowNumberFormatterParams).movableRows && <><span class="bx bx-dots-vertical-rounded" />{" "}</>}
{cell.getRow().getPosition(true)}
</div>),
formatterParams: { movableRows } satisfies RowNumberFormatterParams
@ -200,14 +201,14 @@ function wrapEditor(Component: (opts: EditorOpts) => JSX.Element): ((
editorParams: {},
) => HTMLElement | false) {
return (cell, _, success, cancel, editorParams) => {
const elWithParams = <Component cell={cell} success={success} cancel={cancel} editorParams={editorParams} />
const elWithParams = <Component cell={cell} success={success} cancel={cancel} editorParams={editorParams} />;
return renderReactWidget(null, elWithParams)[0];
};
}
function NoteFormatter({ cell }: FormatterOpts) {
const noteId = cell.getValue();
const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : null)
const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : null);
useEffect(() => {
if (!noteId || note?.noteId === noteId) return;
@ -231,5 +232,5 @@ function RelationEditor({ cell, success }: EditorOpts) {
hideAllButtons: true
}}
noteIdChanged={success}
/>
/>;
}

View File

@ -35,8 +35,7 @@
"sucrase": "3.35.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.71.2",
"@braintree/sanitize-url": "7.1.1",
"@anthropic-ai/sdk": "0.71.2",
"@electron/remote": "2.1.3",
"@preact/preset-vite": "2.10.2",
"@triliumnext/commons": "workspace:*",
@ -50,17 +49,14 @@
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
"@types/debounce": "1.2.4",
"@types/ejs": "3.1.5",
"@types/escape-html": "1.0.4",
"@types/ejs": "3.1.5",
"@types/express-http-proxy": "1.6.7",
"@types/express-session": "1.18.2",
"@types/fs-extra": "11.0.4",
"@types/html": "1.0.4",
"@types/ini": "4.1.1",
"@types/mime-types": "3.0.1",
"@types/ini": "4.1.1",
"@types/multer": "2.0.0",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.16.0",
"@types/safe-compare": "1.1.2",
"@types/sax": "1.2.7",
"@types/serve-favicon": "2.5.7",
"@types/serve-static": "2.2.0",
@ -87,8 +83,7 @@
"ejs": "3.1.10",
"electron": "39.2.7",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"electron-window-state": "5.0.3",
"express": "5.2.1",
"express-http-proxy": "2.1.2",
"express-openid-connect": "2.19.3",
@ -110,15 +105,12 @@
"jimp": "1.6.0",
"lorem-ipsum": "2.0.8",
"marked": "17.0.1",
"mime-types": "3.0.2",
"multer": "2.0.2",
"normalize-strings": "1.1.1",
"ollama": "0.6.3",
"openai": "6.15.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.17.0",
"safe-compare": "1.1.4",
"sax": "1.4.3",
"serve-favicon": "2.5.1",
"stream-throttle": "0.1.3",
@ -129,7 +121,6 @@
"time2fa": "1.4.2",
"tmp": "0.2.5",
"turndown": "7.2.2",
"unescape": "1.0.1",
"vite": "7.3.0",
"ws": "8.18.3",
"xml2js": "0.6.2",

View File

@ -1,6 +1,6 @@
import anonymizationService from "./services/anonymization.js";
import sqlInit from "./services/sql_init.js";
await import("./becca/entity_constructor.js");
await import("@triliumnext/core");
sqlInit.dbReady.then(async () => {
try {

View File

@ -1,25 +1,25 @@
import("@triliumnext/core");
import { erase } from "@triliumnext/core";
import compression from "compression";
import cookieParser from "cookie-parser";
import express from "express";
import { auth } from "express-openid-connect";
import helmet from "helmet";
import { t } from "i18next";
import path from "path";
import favicon from "serve-favicon";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compression from "compression";
import config from "./services/config.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
import assets from "./routes/assets.js";
import routes from "./routes/routes.js";
import custom from "./routes/custom.js";
import error_handlers from "./routes/error_handlers.js";
import { startScheduledCleanup } from "./services/erase.js";
import sql_init from "./services/sql_init.js";
import { auth } from "express-openid-connect";
import openID from "./services/open_id.js";
import { t } from "i18next";
import eventService from "./services/events.js";
import routes from "./routes/routes.js";
import config from "./services/config.js";
import log from "./services/log.js";
import "./services/handlers.js";
import "./becca/becca_loader.js";
import openID from "./services/open_id.js";
import { RESOURCE_DIR } from "./services/resource_dir.js";
import sql_init from "./services/sql_init.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
export default async function buildApp() {
const app = express();
@ -107,7 +107,7 @@ export default async function buildApp() {
await import("./services/scheduler.js");
startScheduledCleanup();
erase.startScheduledCleanup();
if (utils.isElectron) {
(await import("@electron/remote/main/index.js")).initialize();

View File

@ -1,7 +1,2 @@
"use strict";
import Becca from "./becca-interface.js";
const becca = new Becca();
import { becca } from "@triliumnext/core";
export default becca;

View File

@ -1,260 +1,2 @@
import type { AttachmentRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import protectedSessionService from "../../services/protected_session.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import type BBranch from "./bbranch.js";
import type BNote from "./bnote.js";
const attachmentRoleToNoteTypeMapping = {
image: "image",
file: "file"
};
interface ContentOpts {
// TODO: Found in bnote.ts, to check if it's actually used and not a typo.
forceSave?: boolean;
/** will also save this BAttachment entity */
forceFullSave?: boolean;
/** override frontend heuristics on when to reload, instruct to reload */
forceFrontendReload?: boolean;
}
/**
* Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for
* larger amounts of data and generally not accessible to the user.
*/
class BAttachment extends AbstractBeccaEntity<BAttachment> {
static get entityName() {
return "attachments";
}
static get primaryKeyName() {
return "attachmentId";
}
static get hashedProperties() {
return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"];
}
noteId?: number;
attachmentId?: string;
/** either noteId or revisionId to which this attachment belongs */
ownerId!: string;
role!: string;
mime!: string;
title!: string;
type?: keyof typeof attachmentRoleToNoteTypeMapping;
position?: number;
utcDateScheduledForErasureSince?: string | null;
/** optionally added to the entity */
contentLength?: number;
isDecrypted?: boolean;
constructor(row: AttachmentRow) {
super();
this.updateFromRow(row);
this.decrypt();
}
updateFromRow(row: AttachmentRow): void {
if (!row.ownerId?.trim()) {
throw new Error("'ownerId' must be given to initialize a Attachment entity");
} else if (!row.role?.trim()) {
throw new Error("'role' must be given to initialize a Attachment entity");
} else if (!row.mime?.trim()) {
throw new Error("'mime' must be given to initialize a Attachment entity");
} else if (!row.title?.trim()) {
throw new Error("'title' must be given to initialize a Attachment entity");
}
this.attachmentId = row.attachmentId;
this.ownerId = row.ownerId;
this.role = row.role;
this.mime = row.mime;
this.title = row.title;
this.position = row.position;
this.blobId = row.blobId;
this.isProtected = !!row.isProtected;
this.dateModified = row.dateModified;
this.utcDateModified = row.utcDateModified;
this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince;
this.contentLength = row.contentLength;
}
copy(): BAttachment {
return new BAttachment({
ownerId: this.ownerId,
role: this.role,
mime: this.mime,
title: this.title,
blobId: this.blobId,
isProtected: this.isProtected
});
}
getNote(): BNote {
return this.becca.notes[this.ownerId];
}
/** @returns true if the note has string content (not binary) */
override hasStringContent(): boolean {
return utils.isStringNote(this.type, this.mime); // here was !== undefined && utils.isStringNote(this.type, this.mime); I dont know why we need !=undefined. But it filters out canvas libary items
}
isContentAvailable() {
return (
!this.attachmentId || // new attachment which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
}
getTitleOrProtected() {
return this.isContentAvailable() ? this.title : "[protected]";
}
decrypt() {
if (!this.isProtected || !this.attachmentId) {
this.isDecrypted = true;
return;
}
if (!this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
this.title = protectedSessionService.decryptString(this.title) || "";
this.isDecrypted = true;
} catch (e: any) {
log.error(`Could not decrypt attachment ${this.attachmentId}: ${e.message} ${e.stack}`);
}
}
}
getContent(): Buffer {
return this._getContent() as Buffer;
}
setContent(content: string | Buffer, opts?: ContentOpts) {
this._setContent(content, opts);
}
convertToNote(): { note: BNote; branch: BBranch } {
// TODO: can this ever be "search"?
if ((this.type as string) === "search") {
throw new Error(`Note of type search cannot have child notes`);
}
if (!this.getNote()) {
throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + "Converting note revision's attachments to note is not (yet) supported.");
}
if (!(this.role in attachmentRoleToNoteTypeMapping)) {
throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`);
}
if (!this.isContentAvailable()) {
// isProtected is the same for attachment
throw new Error(`Cannot convert protected attachment outside of protected session`);
}
const { note, branch } = noteService.createNewNote({
parentNoteId: this.ownerId,
title: this.title,
type: (attachmentRoleToNoteTypeMapping as any)[this.role],
mime: this.mime,
content: this.getContent(),
isProtected: this.isProtected
});
this.markAsDeleted();
const parentNote = this.getNote();
if (this.role === "image" && parentNote.type === "text") {
const origContent = parentNote.getContent();
if (typeof origContent !== "string") {
throw new Error(`Note with ID '${note.noteId} has a text type but non-string content.`);
}
const oldAttachmentUrl = `api/attachments/${this.attachmentId}/image/`;
const newNoteUrl = `api/images/${note.noteId}/`;
const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl);
if (fixedContent !== origContent) {
parentNote.setContent(fixedContent);
}
noteService.asyncPostProcessContent(note, fixedContent);
}
return { note, branch };
}
getFileName() {
const type = this.role === "image" ? "image" : "file";
return utils.formatDownloadTitle(this.title, type, this.mime);
}
override beforeSaving() {
super.beforeSaving();
if (this.position === undefined || this.position === null) {
this.position =
10 +
sql.getValue<number>(
/*sql*/`SELECT COALESCE(MAX(position), 0)
FROM attachments
WHERE ownerId = ?`,
[this.noteId]
);
}
this.dateModified = dateUtils.localNowDateTime();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
attachmentId: this.attachmentId,
ownerId: this.ownerId,
role: this.role,
mime: this.mime,
title: this.title || undefined,
position: this.position,
blobId: this.blobId,
isProtected: !!this.isProtected,
isDeleted: false,
dateModified: this.dateModified,
utcDateModified: this.utcDateModified,
utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince,
contentLength: this.contentLength
};
}
override getPojoToSave() {
const pojo = this.getPojo();
delete pojo.contentLength;
if (pojo.isProtected) {
if (this.isDecrypted) {
pojo.title = protectedSessionService.encrypt(pojo.title || "") || undefined;
} else {
// updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title;
}
}
return pojo;
}
}
import { BAttachment } from "@triliumnext/core";
export default BAttachment;

View File

@ -1,227 +1,2 @@
"use strict";
import BNote from "./bnote.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/date_utils.js";
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
import sanitizeAttributeName from "../../services/sanitize_attribute_name.js";
import type { AttributeRow, AttributeType } from "@triliumnext/commons";
interface SavingOpts {
skipValidation?: boolean;
}
/**
* Attribute is an abstract concept which has two real uses - label (key - value pair)
* and relation (representing named relationship between source and target note)
*/
class BAttribute extends AbstractBeccaEntity<BAttribute> {
static get entityName() {
return "attributes";
}
static get primaryKeyName() {
return "attributeId";
}
static get hashedProperties() {
return ["attributeId", "noteId", "type", "name", "value", "isInheritable"];
}
attributeId!: string;
noteId!: string;
type!: AttributeType;
name!: string;
position!: number;
value!: string;
isInheritable!: boolean;
constructor(row?: AttributeRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
updateFromRow(row: AttributeRow) {
this.update([row.attributeId, row.noteId, row.type, row.name, row.value, row.isInheritable, row.position, row.utcDateModified]);
}
update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]: any) {
this.attributeId = attributeId;
this.noteId = noteId;
this.type = type;
this.name = name;
this.position = position;
this.value = value || "";
this.isInheritable = !!isInheritable;
this.utcDateModified = utcDateModified;
return this;
}
override init() {
if (this.attributeId) {
this.becca.attributes[this.attributeId] = this;
}
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
}
this.becca.notes[this.noteId].ownedAttributes.push(this);
const key = `${this.type}-${this.name.toLowerCase()}`;
this.becca.attributeIndex[key] = this.becca.attributeIndex[key] || [];
this.becca.attributeIndex[key].push(this);
const targetNote = this.targetNote;
if (targetNote) {
targetNote.targetRelations.push(this);
}
}
validate() {
if (!["label", "relation"].includes(this.type)) {
throw new Error(`Invalid attribute type '${this.type}' in attribute '${this.attributeId}' of note '${this.noteId}'`);
}
if (!this.name?.trim()) {
throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`);
}
if (this.type === "relation" && !(this.value in this.becca.notes)) {
throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`);
}
}
get isAffectingSubtree() {
return this.isInheritable || (this.type === "relation" && ["template", "inherit"].includes(this.name));
}
get targetNoteId() {
// alias
return this.type === "relation" ? this.value : undefined;
}
isAutoLink() {
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
get note() {
return this.becca.notes[this.noteId];
}
get targetNote() {
if (this.type === "relation") {
return this.becca.notes[this.value];
}
}
getNote() {
const note = this.becca.getNote(this.noteId);
if (!note) {
throw new Error(`Note '${this.noteId}' of attribute '${this.attributeId}', type '${this.type}', name '${this.name}' does not exist.`);
}
return note;
}
getTargetNote() {
if (this.type !== "relation") {
throw new Error(`Attribute '${this.attributeId}' is not a relation.`);
}
if (!this.value) {
return null;
}
return this.becca.getNote(this.value);
}
isDefinition() {
return this.type === "label" && (this.name.startsWith("label:") || this.name.startsWith("relation:"));
}
getDefinition() {
return promotedAttributeDefinitionParser.parse(this.value);
}
getDefinedName() {
if (this.type === "label" && this.name.startsWith("label:")) {
return this.name.substr(6);
} else if (this.type === "label" && this.name.startsWith("relation:")) {
return this.name.substr(9);
} else {
return this.name;
}
}
override get isDeleted() {
return !(this.attributeId in this.becca.attributes);
}
override beforeSaving(opts: SavingOpts = {}) {
if (!opts.skipValidation) {
this.validate();
}
this.name = sanitizeAttributeName(this.name);
if (!this.value) {
// null value isn't allowed
this.value = "";
}
if (this.position === undefined || this.position === null) {
const maxExistingPosition = this.getNote()
.getAttributes()
.reduce((maxPosition, attr) => Math.max(maxPosition, attr.position || 0), 0);
this.position = maxExistingPosition + 10;
}
if (!this.isInheritable) {
this.isInheritable = false;
}
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
this.becca.attributes[this.attributeId] = this;
}
getPojo() {
return {
attributeId: this.attributeId,
noteId: this.noteId,
type: this.type,
name: this.name,
position: this.position,
value: this.value,
isInheritable: this.isInheritable,
utcDateModified: this.utcDateModified,
isDeleted: false
};
}
createClone(type: AttributeType, name: string, value: string, isInheritable?: boolean) {
return new BAttribute({
noteId: this.noteId,
type: type,
name: name,
value: value,
position: this.position,
isInheritable: isInheritable,
utcDateModified: this.utcDateModified
});
}
}
import { BAttribute } from "@triliumnext/core";
export default BAttribute;

View File

@ -1,288 +1,2 @@
"use strict";
import BNote from "./bnote.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/date_utils.js";
import utils from "../../services/utils.js";
import TaskContext from "../../services/task_context.js";
import cls from "../../services/cls.js";
import log from "../../services/log.js";
import type { BranchRow } from "@triliumnext/commons";
import handlers from "../../services/handlers.js";
/**
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
* parents.
*
* Note that you should not rely on the branch's identity, since it can change easily with a note's move.
* Always check noteId instead.
*/
class BBranch extends AbstractBeccaEntity<BBranch> {
static get entityName() {
return "branches";
}
static get primaryKeyName() {
return "branchId";
}
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() {
return ["branchId", "noteId", "parentNoteId", "prefix"];
}
branchId?: string;
noteId!: string;
parentNoteId!: string;
prefix!: string | null;
notePosition!: number;
isExpanded!: boolean;
constructor(row?: BranchRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
updateFromRow(row: BranchRow) {
this.update([row.branchId, row.noteId, row.parentNoteId, row.prefix, row.notePosition, row.isExpanded, row.utcDateModified]);
}
update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]: any) {
this.branchId = branchId;
this.noteId = noteId;
this.parentNoteId = parentNoteId;
this.prefix = prefix;
this.notePosition = notePosition;
this.isExpanded = !!isExpanded;
this.utcDateModified = utcDateModified;
return this;
}
override init() {
if (this.branchId) {
this.becca.branches[this.branchId] = this;
}
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
const childNote = this.childNote;
if (!childNote.parentBranches.includes(this)) {
childNote.parentBranches.push(this);
}
if (this.noteId === "root") {
return;
}
const parentNote = this.parentNote;
if (parentNote) {
if (!childNote.parents.includes(parentNote)) {
childNote.parents.push(parentNote);
}
if (!parentNote.children.includes(childNote)) {
parentNote.children.push(childNote);
}
}
}
get childNote(): BNote {
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
}
return this.becca.notes[this.noteId];
}
getNote(): BNote {
return this.childNote;
}
/** @returns root branch will have undefined parent, all other branches have to have a parent note */
get parentNote(): BNote | undefined {
if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== "none") {
// entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.parentNoteId, new BNote({ noteId: this.parentNoteId }));
}
return this.becca.notes[this.parentNoteId];
}
override get isDeleted() {
return this.branchId == undefined || !(this.branchId in this.becca.branches);
}
/**
* Branch is weak when its existence should not hinder deletion of its note.
* As a result, note with only weak branches should be immediately deleted.
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
* of deletion should not act as a clone.
*/
get isWeak() {
return ["_share", "_lbBookmarks"].includes(this.parentNoteId);
}
/**
* Delete a branch. If this is a last note's branch, delete the note as well.
*
* @param deleteId - optional delete identified
*
* @returns true if note has been deleted, false otherwise
*/
deleteBranch(deleteId?: string, taskContext?: TaskContext<"deleteNotes">): boolean {
if (!deleteId) {
deleteId = utils.randomString(10);
}
if (!taskContext) {
taskContext = new TaskContext("no-progress-reporting", "deleteNotes", null);
}
taskContext.increaseProgressCount();
const note = this.getNote();
if (!taskContext.noteDeletionHandlerTriggered) {
const parentBranches = note.getParentBranches();
if (parentBranches.length === 1 && parentBranches[0] === this) {
// needs to be run before branches and attributes are deleted and thus attached relations disappear
handlers.runAttachedRelations(note, "runOnNoteDeletion", note);
}
}
if ((this.noteId === "root" || this.noteId === cls.getHoistedNoteId()) && !this.isWeak) {
throw new Error("Can't delete root or hoisted branch/note");
}
this.markAsDeleted(deleteId);
const notDeletedBranches = note.getStrongParentBranches();
if (notDeletedBranches.length === 0) {
for (const weakBranch of note.getParentBranches()) {
weakBranch.markAsDeleted(deleteId);
}
for (const childBranch of note.getChildBranches()) {
if (childBranch) {
childBranch.deleteBranch(deleteId, taskContext);
}
}
// first delete children and then parent - this will show up better in recent changes
log.info(`Deleting note '${note.noteId}'`);
this.becca.notes[note.noteId].isBeingDeleted = true;
for (const attribute of note.getOwnedAttributes().slice()) {
attribute.markAsDeleted(deleteId);
}
for (const relation of note.getTargetRelations()) {
relation.markAsDeleted(deleteId);
}
for (const attachment of note.getAttachments()) {
attachment.markAsDeleted(deleteId);
}
note.markAsDeleted(deleteId);
return true;
} else {
return false;
}
}
override beforeSaving() {
if (!this.noteId || !this.parentNoteId) {
throw new Error(`noteId and parentNoteId are mandatory properties for Branch`);
}
this.branchId = `${this.parentNoteId}_${this.noteId}`;
if (this.notePosition === undefined || this.notePosition === null) {
let maxNotePos = 0;
if (this.parentNote) {
for (const childBranch of this.parentNote.getChildBranches()) {
if (!childBranch) {
continue;
}
if (
maxNotePos < childBranch.notePosition &&
childBranch.noteId !== "_hidden" // hidden has a very large notePosition to always stay last
) {
maxNotePos = childBranch.notePosition;
}
}
}
this.notePosition = maxNotePos + 10;
}
if (!this.isExpanded) {
this.isExpanded = false;
}
if (!this.prefix?.trim()) {
this.prefix = null;
}
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
this.becca.branches[this.branchId] = this;
}
getPojo() {
return {
branchId: this.branchId,
noteId: this.noteId,
parentNoteId: this.parentNoteId,
prefix: this.prefix,
notePosition: this.notePosition,
isExpanded: this.isExpanded,
isDeleted: false,
utcDateModified: this.utcDateModified
};
}
createClone(parentNoteId: string, notePosition?: number) {
const existingBranch = this.becca.getBranchFromChildAndParent(this.noteId, parentNoteId);
if (existingBranch) {
if (notePosition) {
existingBranch.notePosition = notePosition;
}
return existingBranch;
} else {
return new BBranch({
noteId: this.noteId,
parentNoteId: parentNoteId,
notePosition: notePosition || null,
prefix: this.prefix,
isExpanded: this.isExpanded
});
}
}
getParentNote() {
return this.parentNote;
}
}
import { BBranch } from "@triliumnext/core";
export default BBranch;

View File

@ -1,89 +1,2 @@
"use strict";
import type { EtapiTokenRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
/**
* EtapiToken is an entity representing token used to authenticate against Trilium REST API from client applications.
* Used by:
* - Trilium Sender
* - ETAPI clients
*
* The format user is presented with is "<etapiTokenId>_<tokenHash>". This is also called "authToken" to distinguish it
* from tokenHash and token.
*/
class BEtapiToken extends AbstractBeccaEntity<BEtapiToken> {
static get entityName() {
return "etapi_tokens";
}
static get primaryKeyName() {
return "etapiTokenId";
}
static get hashedProperties() {
return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"];
}
etapiTokenId?: string;
name!: string;
tokenHash!: string;
private _isDeleted?: boolean;
constructor(row?: EtapiTokenRow) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
override get isDeleted() {
return !!this._isDeleted;
}
updateFromRow(row: EtapiTokenRow) {
this.etapiTokenId = row.etapiTokenId;
this.name = row.name;
this.tokenHash = row.tokenHash;
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
this.utcDateModified = row.utcDateModified || this.utcDateCreated;
this._isDeleted = !!row.isDeleted;
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
override init() {
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
getPojo() {
return {
etapiTokenId: this.etapiTokenId,
name: this.name,
tokenHash: this.tokenHash,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
isDeleted: this.isDeleted
};
}
override beforeSaving() {
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
}
import { BEtapiToken } from "@triliumnext/core";
export default BEtapiToken;

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +1,2 @@
"use strict";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import type { OptionRow } from "@triliumnext/commons";
/**
* Option represents a name-value pair, either directly configurable by the user or some system property.
*/
class BOption extends AbstractBeccaEntity<BOption> {
static get entityName() {
return "options";
}
static get primaryKeyName() {
return "name";
}
static get hashedProperties() {
return ["name", "value"];
}
name!: string;
value!: string;
constructor(row?: OptionRow) {
super();
if (row) {
this.updateFromRow(row);
}
this.becca.options[this.name] = this;
}
updateFromRow(row: OptionRow) {
this.name = row.name;
this.value = row.value;
this.isSynced = !!row.isSynced;
this.utcDateModified = row.utcDateModified;
}
override beforeSaving() {
super.beforeSaving();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
name: this.name,
value: this.value,
isSynced: this.isSynced,
utcDateModified: this.utcDateModified
};
}
}
import { BOption } from "@triliumnext/core";
export default BOption;

View File

@ -1,46 +1,2 @@
"use strict";
import type { RecentNoteRow } from "@triliumnext/commons";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
/**
* RecentNote represents recently visited note.
*/
class BRecentNote extends AbstractBeccaEntity<BRecentNote> {
static get entityName() {
return "recent_notes";
}
static get primaryKeyName() {
return "noteId";
}
static get hashedProperties() {
return ["noteId", "notePath"];
}
noteId!: string;
notePath!: string;
constructor(row: RecentNoteRow) {
super();
this.updateFromRow(row);
}
updateFromRow(row: RecentNoteRow): void {
this.noteId = row.noteId;
this.notePath = row.notePath;
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
}
getPojo() {
return {
noteId: this.noteId,
notePath: this.notePath,
utcDateCreated: this.utcDateCreated
};
}
}
import { BRecentNote } from "@triliumnext/core";
export default BRecentNote;

View File

@ -1,225 +1,2 @@
"use strict";
import protectedSessionService from "../../services/protected_session.js";
import utils from "../../services/utils.js";
import dateUtils from "../../services/date_utils.js";
import becca from "../becca.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js";
import BAttachment from "./battachment.js";
import type { AttachmentRow, NoteType, RevisionPojo, RevisionRow } from "@triliumnext/commons";
import eraseService from "../../services/erase.js";
interface ContentOpts {
/** will also save this BRevision entity */
forceSave?: boolean;
}
interface GetByIdOpts {
includeContentLength?: boolean;
}
/**
* Revision represents a snapshot of note's title and content at some point in the past.
* It's used for seamless note versioning.
*/
class BRevision extends AbstractBeccaEntity<BRevision> {
static get entityName() {
return "revisions";
}
static get primaryKeyName() {
return "revisionId";
}
static get hashedProperties() {
return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"];
}
revisionId?: string;
noteId!: string;
type!: NoteType;
mime!: string;
title!: string;
dateLastEdited?: string;
utcDateLastEdited?: string;
contentLength?: number;
content?: string | Buffer;
constructor(row: RevisionRow, titleDecrypted = false) {
super();
this.updateFromRow(row);
if (this.isProtected && !titleDecrypted) {
const decryptedTitle = protectedSessionService.isProtectedSessionAvailable() ? protectedSessionService.decryptString(this.title) : null;
this.title = decryptedTitle || "[protected]";
}
}
updateFromRow(row: RevisionRow) {
this.revisionId = row.revisionId;
this.noteId = row.noteId;
this.type = row.type;
this.mime = row.mime;
this.isProtected = !!row.isProtected;
this.title = row.title;
this.blobId = row.blobId;
this.dateLastEdited = row.dateLastEdited;
this.dateCreated = row.dateCreated;
this.utcDateLastEdited = row.utcDateLastEdited;
this.utcDateCreated = row.utcDateCreated;
this.utcDateModified = row.utcDateModified;
this.contentLength = row.contentLength;
}
getNote() {
return becca.notes[this.noteId];
}
/** @returns true if the note has string content (not binary) */
override hasStringContent(): boolean {
return utils.isStringNote(this.type, this.mime);
}
isContentAvailable() {
return (
!this.revisionId || // new note which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
}
/*
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
* part of Revision entity with its own sync. The reason behind this hybrid design is that
* content can be quite large, and it's not necessary to load it / fill memory for any note access even
* if we don't need a content, especially for bulk operations like search.
*
* This is the same approach as is used for Note's content.
*/
getContent(): string | Buffer {
return this._getContent();
}
/**
* @throws Error in case of invalid JSON */
getJsonContent(): {} | null {
const content = this.getContent();
if (!content || typeof content !== "string" || !content.trim()) {
return null;
}
return JSON.parse(content);
}
/** @returns valid object or null if the content cannot be parsed as JSON */
getJsonContentSafely(): {} | null {
try {
return this.getJsonContent();
} catch (e) {
return null;
}
}
setContent(content: string | Buffer, opts: ContentOpts = {}) {
this._setContent(content, opts);
}
getAttachments(): BAttachment[] {
return sql
.getRows<AttachmentRow>(
`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND isDeleted = 0`,
[this.revisionId]
)
.map((row) => new BAttachment(row));
}
getAttachmentById(attachmentId: String, opts: GetByIdOpts = {}): BAttachment | null {
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId]).map((row) => new BAttachment(row))[0];
}
getAttachmentsByRole(role: string): BAttachment[] {
return sql
.getRows<AttachmentRow>(
`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND role = ?
AND isDeleted = 0
ORDER BY position`,
[this.revisionId, role]
)
.map((row) => new BAttachment(row));
}
getAttachmentByTitle(title: string): BAttachment {
// cannot use SQL to filter by title since it can be encrypted
return this.getAttachments().filter((attachment) => attachment.title === title)[0];
}
/**
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/
eraseRevision() {
if (this.revisionId) {
eraseService.eraseRevisions([this.revisionId]);
}
}
override beforeSaving() {
super.beforeSaving();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
revisionId: this.revisionId,
noteId: this.noteId,
type: this.type,
mime: this.mime,
isProtected: this.isProtected,
title: this.title,
blobId: this.blobId,
dateLastEdited: this.dateLastEdited,
dateCreated: this.dateCreated,
utcDateLastEdited: this.utcDateLastEdited,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
content: this.content, // used when retrieving full note revision to frontend
contentLength: this.contentLength
} satisfies RevisionPojo;
}
override getPojoToSave() {
const pojo = this.getPojo();
delete pojo.content; // not getting persisted
delete pojo.contentLength; // not getting persisted
if (pojo.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
pojo.title = protectedSessionService.encrypt(this.title) ?? "";
} else {
// updating protected note outside of protected session means we will keep original ciphertexts
pojo.title = "";
}
}
return pojo;
}
}
import { BRevision } from "@triliumnext/core";
export default BRevision;

View File

@ -1,5 +1,8 @@
import { CryptoProvider } from "@triliumnext/core";
import crypto from "crypto";
import { generator } from "rand-token";
const randtoken = generator({ source: "crypto" });
export default class NodejsCryptoProvider implements CryptoProvider {
@ -19,4 +22,8 @@ export default class NodejsCryptoProvider implements CryptoProvider {
return crypto.randomBytes(size);
}
randomString(length: number): string {
return randtoken.generate(length);
}
}

View File

@ -1,12 +0,0 @@
import HttpError from "./http_error.js";
class ForbiddenError extends HttpError {
constructor(message: string) {
super(message, 403);
this.name = "ForbiddenError";
}
}
export default ForbiddenError;

View File

@ -1,13 +0,0 @@
class HttpError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.name = "HttpError";
this.statusCode = statusCode;
}
}
export default HttpError;

View File

@ -1,12 +0,0 @@
import HttpError from "./http_error.js";
class NotFoundError extends HttpError {
constructor(message: string) {
super(message, 404);
this.name = "NotFoundError";
}
}
export default NotFoundError;

View File

@ -1,9 +0,0 @@
class OpenIdError {
message: string;
constructor(message: string) {
this.message = message;
}
}
export default OpenIdError;

View File

@ -1,12 +0,0 @@
import HttpError from "./http_error.js";
class ValidationError extends HttpError {
constructor(message: string) {
super(message, 400)
this.name = "ValidationError";
}
}
export default ValidationError;

View File

@ -1,20 +1,21 @@
import becca from "../becca/becca.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import noteService from "../services/notes.js";
import TaskContext from "../services/task_context.js";
import v from "./validators.js";
import searchService from "../services/search/services/search.js";
import SearchContext from "../services/search/search_context.js";
import zipExportService from "../services/export/zip.js";
import zipImportService from "../services/import/zip.js";
import { NoteParams } from "@triliumnext/core";
import type { Request, Router } from "express";
import type { ParsedQs } from "qs";
import type { NoteParams } from "../services/note-interface.js";
import type { SearchParams } from "../services/search/services/types.js";
import type { ValidatorMap } from "./etapi-interface.js";
import becca from "../becca/becca.js";
import zipExportService from "../services/export/zip.js";
import type { ExportFormat } from "../services/export/zip/abstract_provider.js";
import zipImportService from "../services/import/zip.js";
import noteService from "../services/notes.js";
import SearchContext from "../services/search/search_context.js";
import searchService from "../services/search/services/search.js";
import type { SearchParams } from "../services/search/services/types.js";
import TaskContext from "../services/task_context.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import type { ValidatorMap } from "./etapi-interface.js";
import mappers from "./mappers.js";
import v from "./validators.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/notes", (req, res, next) => {

View File

@ -26,7 +26,7 @@ async function startApplication() {
},
async onTransactionRollback() {
const cls = (await import("./services/cls.js")).default;
const becca_loader = (await import("./becca/becca_loader.js")).default;
const becca_loader = (await import("@triliumnext/core")).becca_loader;
const entity_changes = (await import("./services/entity_changes.js")).default;
const log = (await import("./services/log")).default;

View File

@ -1,5 +1,6 @@
import { becca_loader } from "@triliumnext/core";
import becca from "../becca/becca.js";
import becca_loader from "../becca/becca_loader.js";
import cls from "../services/cls.js";
import log from "../services/log.js";
import sql from "../services/sql.js";

View File

@ -1,5 +1,6 @@
import { becca_loader } from "@triliumnext/core";
import becca from "../becca/becca";
import becca_loader from "../becca/becca_loader";
import cls from "../services/cls.js";
import hidden_subtree from "../services/hidden_subtree";

View File

@ -1,9 +1,9 @@
import becca from "../../becca/becca.js";
import blobService from "../../services/blob.js";
import ValidationError from "../../errors/validation_error.js";
import imageService from "../../services/image.js";
import type { Request } from "express";
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
import { blob as blobService, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import imageService from "../../services/image.js";
function getAttachmentBlob(req: Request) {
const preview = req.query.preview === "true";
@ -34,7 +34,7 @@ function getAllAttachments(req: Request) {
function saveAttachment(req: Request) {
const { noteId } = req.params;
const { attachmentId, role, mime, title, content } = req.body;
const matchByQuery = req.query.matchBy
const matchByQuery = req.query.matchBy;
const isValidMatchBy = (typeof matchByQuery === "string") && (matchByQuery === "attachmentId" || matchByQuery === "title");
const matchBy = isValidMatchBy ? matchByQuery : undefined;

View File

@ -1,13 +1,12 @@
"use strict";
import sql from "../../services/sql.js";
import log from "../../services/log.js";
import attributeService from "../../services/attributes.js";
import BAttribute from "../../becca/entities/battribute.js";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { UpdateAttributeResponse } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import BAttribute from "../../becca/entities/battribute.js";
import attributeService from "../../services/attributes.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
function getEffectiveNoteAttributes(req: Request) {
const note = becca.getNote(req.params.noteId);
@ -47,7 +46,7 @@ function updateNoteAttribute(req: Request) {
}
attribute = new BAttribute({
noteId: noteId,
noteId,
name: body.name,
type: body.type,
isInheritable: body.isInheritable
@ -208,7 +207,7 @@ function createRelation(req: Request) {
if (!attribute) {
attribute = new BAttribute({
noteId: sourceNoteId,
name: name,
name,
type: "relation",
value: targetNoteId
}).save();

View File

@ -1,8 +1,7 @@
import { becca_service, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import beccaService from "../../becca/becca_service.js";
import ValidationError from "../../errors/validation_error.js";
import cls from "../../services/cls.js";
import log from "../../services/log.js";
import searchService from "../../services/search/services/search.js";
@ -67,8 +66,8 @@ function getRecentNotes(activeNoteId: string) {
return recentNotes.map((rn) => {
const notePathArray = rn.notePath.split("/");
const { title, icon } = beccaService.getNoteTitleAndIcon(notePathArray[notePathArray.length - 1]);
const notePathTitle = beccaService.getNoteTitleForPath(notePathArray);
const { title, icon } = becca_service.getNoteTitleAndIcon(notePathArray[notePathArray.length - 1]);
const notePathTitle = becca_service.getNoteTitleForPath(notePathArray);
return {
notePath: rn.notePath,

View File

@ -1,18 +1,15 @@
"use strict";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import entityChangesService from "../../services/entity_changes.js";
import treeService from "../../services/tree.js";
import eraseService from "../../services/erase.js";
import becca from "../../becca/becca.js";
import TaskContext from "../../services/task_context.js";
import branchService from "../../services/branches.js";
import log from "../../services/log.js";
import ValidationError from "../../errors/validation_error.js";
import eventService from "../../services/events.js";
import { erase as eraseService, events as eventService, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import branchService from "../../services/branches.js";
import entityChangesService from "../../services/entity_changes.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import treeService from "../../services/tree.js";
import utils from "../../services/utils.js";
/**
* Code in this file deals with moving and cloning branches. The relationship between note and parent note is unique
* for not deleted branches. There may be multiple deleted note-parent note relationships.
@ -256,7 +253,7 @@ function deleteBranch(req: Request) {
}
return {
noteDeleted: noteDeleted
noteDeleted
};
}
@ -272,7 +269,7 @@ function setPrefix(req: Request) {
function setPrefixBatch(req: Request) {
const { branchIds, prefix } = req.body;
if (!Array.isArray(branchIds)) {
throw new ValidationError("branchIds must be an array");
}

View File

@ -1,16 +1,15 @@
import { sanitize, ValidationError } from "@triliumnext/core";
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 cloneService from "../../services/cloning.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";
@ -32,13 +31,13 @@ async function addClipping(req: Request) {
const clipperInbox = await getClipperInboxNote();
const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl);
const pageUrl = sanitize.sanitizeUrl(req.body.pageUrl);
let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType);
if (!clippingNote) {
clippingNote = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
title: title,
title,
content: "",
type: "text"
}).note;
@ -99,8 +98,8 @@ async function getClipperInboxNote() {
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 clipType = sanitize.sanitizeHtml(req.body.clipType);
const pageUrl = sanitize.sanitizeUrl(req.body.pageUrl);
const trimmedTitle = (typeof req.body.title === "string") ? req.body.title.trim() : "";
const title = trimmedTitle || `Clipped note from ${pageUrl}`;
@ -126,7 +125,7 @@ async function createNote(req: Request) {
if (labels) {
for (const labelName in labels) {
const labelValue = htmlSanitizer.sanitize(labels[labelName]);
const labelValue = sanitize.sanitizeHtml(labels[labelName]);
note.setLabel(labelName, labelValue);
}
}
@ -147,7 +146,7 @@ async function createNote(req: Request) {
}
export function processContent(images: Image[], note: BNote, content: string) {
let rewrittenContent = htmlSanitizer.sanitize(content);
let rewrittenContent = sanitize.sanitizeHtml(content);
if (images) {
for (const { src, dataUrl, imageId } of images) {
@ -198,11 +197,11 @@ function openNote(req: Request) {
return {
result: "ok"
};
} else {
return {
result: "open-in-browser"
};
}
return {
result: "open-in-browser"
};
}
function handshake() {

View File

@ -1,15 +1,13 @@
"use strict";
import sql from "../../services/sql.js";
import log from "../../services/log.js";
import backupService from "../../services/backup.js";
import anonymizationService from "../../services/anonymization.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import sql_init from "../../services/sql_init.js";
import becca_loader from "../../becca/becca_loader.js";
import { BackupDatabaseNowResponse, DatabaseCheckIntegrityResponse } from "@triliumnext/commons";
import { becca_loader, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import anonymizationService from "../../services/anonymization.js";
import backupService from "../../services/backup.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import sql_init from "../../services/sql_init.js";
function getExistingBackups() {
return backupService.getExistingBackups();

View File

@ -1,14 +1,12 @@
"use strict";
import zipExportService from "../../services/export/zip.js";
import singleExportService from "../../services/export/single.js";
import opmlExportService from "../../services/export/opml.js";
import becca from "../../becca/becca.js";
import TaskContext from "../../services/task_context.js";
import log from "../../services/log.js";
import NotFoundError from "../../errors/not_found_error.js";
import { NotFoundError, ValidationError } from "@triliumnext/core";
import type { Request, Response } from "express";
import ValidationError from "../../errors/validation_error.js";
import becca from "../../becca/becca.js";
import opmlExportService from "../../services/export/opml.js";
import singleExportService from "../../services/export/single.js";
import zipExportService from "../../services/export/zip.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
function exportBranch(req: Request, res: Response) {

View File

@ -1,5 +1,4 @@
import { ValidationError } from "@triliumnext/core";
import chokidar from "chokidar";
import type { Request, Response } from "express";
import fs from "fs";
@ -9,7 +8,6 @@ import tmp from "tmp";
import becca from "../../becca/becca.js";
import type BAttachment from "../../becca/entities/battachment.js";
import type BNote from "../../becca/entities/bnote.js";
import ValidationError from "../../errors/validation_error.js";
import dataDirs from "../../services/data_dir.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
@ -123,7 +121,7 @@ function attachmentContentProvider(req: Request) {
return streamContent(attachment.getContent(), attachment.getFileName(), attachment.mime);
}
async function streamContent(content: string | Buffer, fileName: string, mimeType: string) {
async function streamContent(content: string | Uint8Array, fileName: string, mimeType: string) {
if (typeof content === "string") {
content = Buffer.from(content, "utf8");
}
@ -170,7 +168,7 @@ function saveAttachmentToTmpDir(req: Request) {
const createdTemporaryFiles = new Set<string>();
function saveToTmpDir(fileName: string, content: string | Buffer, entityType: string, entityId: string) {
function saveToTmpDir(fileName: string, content: string | Uint8Array, entityType: string, entityId: string) {
const tmpObj = tmp.fileSync({
postfix: fileName,
tmpdir: dataDirs.TMP_DIR

View File

@ -1,11 +1,12 @@
"use strict";
import imageService from "../../services/image.js";
import becca from "../../becca/becca.js";
import fs from "fs";
import type { Request, Response } from "express";
import fs from "fs";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import imageService from "../../services/image.js";
import { RESOURCE_DIR } from "../../services/resource_dir.js";
function returnImageFromNote(req: Request, res: Response) {
@ -42,7 +43,7 @@ function returnImageInt(image: BNote | BRevision | null, res: Response) {
}
export function renderSvgAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
let svg: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
let svg: string | Uint8Array = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
const attachment = image.getAttachmentByTitle(attachmentName);
if (attachment) {

View File

@ -1,18 +1,16 @@
"use strict";
import { becca_loader,ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import path from "path";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import cls from "../../services/cls.js";
import enexImportService from "../../services/import/enex.js";
import opmlImportService from "../../services/import/opml.js";
import zipImportService from "../../services/import/zip.js";
import singleImportService from "../../services/import/single.js";
import cls from "../../services/cls.js";
import path from "path";
import becca from "../../becca/becca.js";
import beccaLoader from "../../becca/becca_loader.js";
import zipImportService from "../../services/import/zip.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import type BNote from "../../becca/entities/bnote.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
async function importNotesToBranch(req: Request) {
@ -88,7 +86,7 @@ async function importNotesToBranch(req: Request) {
setTimeout(
() =>
taskContext.taskSucceeded({
parentNoteId: parentNoteId,
parentNoteId,
importedNoteId: note?.noteId
}),
1000
@ -96,7 +94,7 @@ async function importNotesToBranch(req: Request) {
}
// import has deactivated note events so becca is not updated, instead we force it to reload
beccaLoader.load();
becca_loader.load();
return note.getPojo();
}
@ -138,7 +136,7 @@ function importAttachmentsToNote(req: Request) {
setTimeout(
() =>
taskContext.taskSucceeded({
parentNoteId: parentNoteId
parentNoteId
}),
1000
);

View File

@ -1,20 +1,18 @@
"use strict";
import options from "../../services/options.js";
import utils from "../../services/utils.js";
import dateUtils from "../../services/date_utils.js";
import instanceId from "../../services/instance_id.js";
import passwordEncryptionService from "../../services/encryption/password_encryption.js";
import protectedSessionService from "../../services/protected_session.js";
import appInfo from "../../services/app_info.js";
import eventService from "../../services/events.js";
import sqlInit from "../../services/sql_init.js";
import sql from "../../services/sql.js";
import ws from "../../services/ws.js";
import etapiTokenService from "../../services/etapi_tokens.js";
import { events as eventService, getInstanceId } from "@triliumnext/core";
import type { Request } from "express";
import totp from "../../services/totp";
import appInfo from "../../services/app_info.js";
import dateUtils from "../../services/date_utils.js";
import passwordEncryptionService from "../../services/encryption/password_encryption.js";
import recoveryCodeService from "../../services/encryption/recovery_codes";
import etapiTokenService from "../../services/etapi_tokens.js";
import options from "../../services/options.js";
import protectedSessionService from "../../services/protected_session.js";
import sql from "../../services/sql.js";
import sqlInit from "../../services/sql_init.js";
import totp from "../../services/totp";
import utils from "../../services/utils.js";
import ws from "../../services/ws.js";
/**
* @swagger
@ -115,7 +113,7 @@ function loginSync(req: Request) {
req.session.loggedIn = true;
return {
instanceId: instanceId,
instanceId: getInstanceId(),
maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1")
};
}

View File

@ -1,18 +1,15 @@
"use strict";
import noteService from "../../services/notes.js";
import eraseService from "../../services/erase.js";
import treeService from "../../services/tree.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import blobService from "../../services/blob.js";
import type { Request } from "express";
import type BBranch from "../../becca/entities/bbranch.js";
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
import { blob as blobService, erase as eraseService, ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import type BBranch from "../../becca/entities/bbranch.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import treeService from "../../services/tree.js";
import utils from "../../services/utils.js";
/**
* @swagger

View File

@ -1,9 +1,9 @@
import type { OptionNames } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import config from "../../services/config.js";
import { changeLanguage, getLocales } from "../../services/i18n.js";
import log from "../../services/log.js";

View File

@ -1,16 +1,15 @@
"use strict";
import { ChangePasswordResponse } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import passwordService from "../../services/encryption/password.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { ChangePasswordResponse } from "@triliumnext/commons";
function changePassword(req: Request): ChangePasswordResponse {
if (passwordService.isPasswordSet()) {
return passwordService.changePassword(req.body.current_password, req.body.new_password);
} else {
return passwordService.setPassword(req.body.new_password);
}
return passwordService.setPassword(req.body.new_password);
}
function resetPassword(req: Request) {

View File

@ -1,18 +1,14 @@
"use strict";
import beccaService from "../../becca/becca_service.js";
import utils from "../../services/utils.js";
import sql from "../../services/sql.js";
import cls from "../../services/cls.js";
import path from "path";
import becca from "../../becca/becca.js";
import blobService from "../../services/blob.js";
import eraseService from "../../services/erase.js";
import { EditedNotesResponse, RevisionItem, RevisionPojo } from "@triliumnext/commons";
import { becca_service, binary_utils, blob as blobService, erase as eraseService, NotePojo } from "@triliumnext/core";
import type { Request, Response } from "express";
import type BRevision from "../../becca/entities/brevision.js";
import path from "path";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type { NotePojo } from "../../becca/becca-interface.js";
import { EditedNotesResponse, RevisionItem, RevisionPojo, RevisionRow } from "@triliumnext/commons";
import type BRevision from "../../becca/entities/brevision.js";
import cls from "../../services/cls.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
interface NotePath {
noteId: string;
@ -56,7 +52,7 @@ function getRevision(req: Request) {
revision.content = revision.getContent();
if (revision.content && revision.type === "image") {
revision.content = revision.content.toString("base64");
revision.content = binary_utils.encodeBase64(revision.content);
}
}
@ -166,7 +162,7 @@ function getEditedNotesOnDate(req: Request) {
)
ORDER BY isDeleted
LIMIT 50`,
{ date: `${req.params.date}%` }
{ date: `${req.params.date}%` }
);
let notes = becca.getNotes(noteIds, true);
@ -191,7 +187,7 @@ function getNotePathData(note: BNote): NotePath | undefined {
const retPath = note.getBestNotePath();
if (retPath) {
const noteTitle = beccaService.getNoteTitleForPath(retPath);
const noteTitle = becca_service.getNoteTitleForPath(retPath);
let branchId;
@ -204,7 +200,7 @@ function getNotePathData(note: BNote): NotePath | undefined {
return {
noteId: note.noteId,
branchId: branchId,
branchId,
title: noteTitle,
notePath: retPath,
path: retPath.join("/")

View File

@ -1,17 +1,14 @@
"use strict";
import { becca_service,ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import SearchContext from "../../services/search/search_context.js";
import searchService, { EMPTY_RESULT, type SearchNoteResult } from "../../services/search/services/search.js";
import attributeFormatter from "../../services/attribute_formatter.js";
import bulkActionService from "../../services/bulk_actions.js";
import cls from "../../services/cls.js";
import attributeFormatter from "../../services/attribute_formatter.js";
import ValidationError from "../../errors/validation_error.js";
import type SearchResult from "../../services/search/search_result.js";
import hoistedNoteService from "../../services/hoisted_note.js";
import beccaService from "../../becca/becca_service.js";
import SearchContext from "../../services/search/search_context.js";
import type SearchResult from "../../services/search/search_result.js";
import searchService, { EMPTY_RESULT, type SearchNoteResult } from "../../services/search/services/search.js";
function searchFromNote(req: Request): SearchNoteResult {
const note = becca.getNoteOrThrow(req.params.noteId);
@ -72,7 +69,7 @@ function quickSearch(req: Request) {
// Map to API format
const searchResults = trimmed.map((result) => {
const { title, icon } = beccaService.getNoteTitleAndIcon(result.noteId);
const { title, icon } = becca_service.getNoteTitleAndIcon(result.noteId);
return {
notePath: result.notePath,
noteTitle: title,
@ -82,7 +79,7 @@ function quickSearch(req: Request) {
highlightedContentSnippet: result.highlightedContentSnippet,
attributeSnippet: result.attributeSnippet,
highlightedAttributeSnippet: result.highlightedAttributeSnippet,
icon: icon
icon
};
});
@ -90,7 +87,7 @@ function quickSearch(req: Request) {
return {
searchResultNoteIds: resultNoteIds,
searchResults: searchResults,
searchResults,
error: searchContext.getError()
};
}

View File

@ -1,9 +1,9 @@
import { utils } from "@triliumnext/core";
import type { Request } from "express";
import imageType from "image-type";
import imageService from "../../services/image.js";
import noteService from "../../services/notes.js";
import sanitizeAttributeName from "../../services/sanitize_attribute_name.js";
import specialNotesService from "../../services/special_notes.js";
async function uploadImage(req: Request) {
@ -43,14 +43,14 @@ async function uploadImage(req: Request) {
const labels = JSON.parse(labelsStr);
for (const { name, value } of labels) {
note.setLabel(sanitizeAttributeName(name), value);
note.setLabel(utils.sanitizeAttributeName(name), value);
}
}
note.setLabel("sentFromSender");
return {
noteId: noteId
noteId
};
}
@ -72,7 +72,7 @@ async function saveNote(req: Request) {
if (req.body.labels) {
for (const { name, value } of req.body.labels) {
note.setLabel(sanitizeAttributeName(name), value);
note.setLabel(utils.sanitizeAttributeName(name), value);
}
}

View File

@ -1,17 +1,15 @@
"use strict";
import { SimilarNoteResponse } from "@triliumnext/commons";
import { similarity } from "@triliumnext/core";
import type { Request } from "express";
import similarityService from "../../becca/similarity.js";
import becca from "../../becca/becca.js";
import { SimilarNoteResponse } from "@triliumnext/commons";
async function getSimilarNotes(req: Request) {
const noteId = req.params.noteId;
const _note = becca.getNoteOrThrow(noteId);
return (await similarityService.findSimilarNotes(noteId) satisfies SimilarNoteResponse);
return (await similarity.findSimilarNotes(noteId) satisfies SimilarNoteResponse);
}
export default {

View File

@ -1,9 +1,8 @@
"use strict";
import sql from "../../services/sql.js";
import becca from "../../becca/becca.js";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import becca from "../../becca/becca.js";
import sql from "../../services/sql.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
interface Table {

View File

@ -1,21 +1,20 @@
"use strict";
import { type EntityChange,SyncTestResponse } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import type { Request } from "express";
import { t } from "i18next";
import syncService from "../../services/sync.js";
import syncUpdateService from "../../services/sync_update.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import contentHashService from "../../services/content_hash.js";
import entityChangesService from "../../services/entity_changes.js";
import log from "../../services/log.js";
import optionService from "../../services/options.js";
import sql from "../../services/sql.js";
import sqlInit from "../../services/sql_init.js";
import optionService from "../../services/options.js";
import contentHashService from "../../services/content_hash.js";
import log from "../../services/log.js";
import syncService from "../../services/sync.js";
import syncOptions from "../../services/sync_options.js";
import syncUpdateService from "../../services/sync_update.js";
import utils, { safeExtractMessageAndStackFromError } from "../../services/utils.js";
import ws from "../../services/ws.js";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import { t } from "i18next";
import { SyncTestResponse, type EntityChange } from "@triliumnext/commons";
async function testSync(): Promise<SyncTestResponse> {
try {
@ -287,10 +286,10 @@ function update(req: Request) {
if (pageIndex !== pageCount - 1) {
return;
} else {
body = JSON.parse(partialRequests[requestId].payload);
delete partialRequests[requestId];
}
}
body = JSON.parse(partialRequests[requestId].payload);
delete partialRequests[requestId];
}
const { entities, instanceId } = body;

View File

@ -1,11 +1,10 @@
"use strict";
import type { AttributeRow, BranchRow, NoteRow } from "@triliumnext/commons";
import { NotFoundError } from "@triliumnext/core";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import log from "../../services/log.js";
import NotFoundError from "../../errors/not_found_error.js";
import type { Request } from "express";
import type BNote from "../../becca/entities/bnote.js";
import type { AttributeRow, BranchRow, NoteRow } from "@triliumnext/commons";
import log from "../../services/log.js";
function getNotesAndBranchesAndAttributes(_noteIds: string[] | Set<string>) {
const noteIds = new Set(_noteIds);

View File

@ -1,8 +1,7 @@
import { ForbiddenError, HttpError, NotFoundError } from "@triliumnext/core";
import type { Application, NextFunction, Request, Response } from "express";
import log from "../services/log.js";
import NotFoundError from "../errors/not_found_error.js";
import ForbiddenError from "../errors/forbidden_error.js";
import HttpError from "../errors/http_error.js";
function register(app: Application) {

View File

@ -1,18 +1,19 @@
import { ValidationError } from "@triliumnext/core";
import crypto from "crypto";
import utils from "../services/utils.js";
import optionService from "../services/options.js";
import myScryptService from "../services/encryption/my_scrypt.js";
import log from "../services/log.js";
import passwordService from "../services/encryption/password.js";
import assetPath, { assetUrlFragment } from "../services/asset_path.js";
import appPath from "../services/app_path.js";
import ValidationError from "../errors/validation_error.js";
import type { Request, Response } from 'express';
import totp from '../services/totp.js';
import recoveryCodeService from '../services/encryption/recovery_codes.js';
import openID from '../services/open_id.js';
import appPath from "../services/app_path.js";
import assetPath, { assetUrlFragment } from "../services/asset_path.js";
import myScryptService from "../services/encryption/my_scrypt.js";
import openIDEncryption from '../services/encryption/open_id_encryption.js';
import passwordService from "../services/encryption/password.js";
import recoveryCodeService from '../services/encryption/recovery_codes.js';
import { getCurrentLocale } from "../services/i18n.js";
import log from "../services/log.js";
import openID from '../services/open_id.js';
import optionService from "../services/options.js";
import totp from '../services/totp.js';
import utils from "../services/utils.js";
function loginPage(req: Request, res: Response) {
// Login page is triggered twice. Once here, and another time (see sendLoginError) if the password is failed.
@ -23,9 +24,9 @@ function loginPage(req: Request, res: Response) {
ssoEnabled: openID.isOpenIDEnabled(),
ssoIssuerName: openID.getSSOIssuerName(),
ssoIssuerIcon: openID.getSSOIssuerIcon(),
assetPath: assetPath,
assetPath,
assetPathFragment: assetUrlFragment,
appPath: appPath,
appPath,
currentLocale: getCurrentLocale()
});
}
@ -181,9 +182,9 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to
wrongTotp: errorType === 'totp',
totpEnabled: totp.isTotpEnabled(),
ssoEnabled: openID.isOpenIDEnabled(),
assetPath: assetPath,
assetPath,
assetPathFragment: assetUrlFragment,
appPath: appPath,
appPath,
currentLocale: getCurrentLocale()
});
}

View File

@ -1,10 +1,8 @@
import { AbstractBeccaEntity,NotFoundError, ValidationError } from "@triliumnext/core";
import express, { type RequestHandler } from "express";
import multer from "multer";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import { namespace } from "../cls_provider.js";
import NotFoundError from "../errors/not_found_error.js";
import ValidationError from "../errors/validation_error.js";
import auth from "../services/auth.js";
import cls from "../services/cls.js";
import entityChangesService from "../services/entity_changes.js";

View File

@ -1,21 +1,11 @@
import path from "path";
import build from "./build.js";
import packageJson from "../../package.json" with { type: "json" };
import dataDir from "./data_dir.js";
import { AppInfo } from "@triliumnext/commons";
import { app_info as coreAppInfo } from "@triliumnext/core";
import path from "path";
const APP_DB_VERSION = 233;
const SYNC_VERSION = 36;
const CLIPPER_PROTOCOL_VERSION = "1.0";
import dataDir from "./data_dir.js";
export default {
appVersion: packageJson.version,
dbVersion: APP_DB_VERSION,
...coreAppInfo,
nodeVersion: process.version,
syncVersion: SYNC_VERSION,
buildDate: build.buildDate,
buildRevision: build.buildRevision,
dataDirectory: path.resolve(dataDir.TRILIUM_DATA_DIR),
clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION,
utcDateTime: new Date().toISOString()
} satisfies AppInfo;

View File

@ -1,41 +1,38 @@
import { type AttributeRow, dayjs, formatLogMessage } from "@triliumnext/commons";
import { type AbstractBeccaEntity, Becca, NoteParams } from "@triliumnext/core";
import axios from "axios";
import * as cheerio from "cheerio";
import xml2js from "xml2js";
import becca from "../becca/becca.js";
import type BAttachment from "../becca/entities/battachment.js";
import type BAttribute from "../becca/entities/battribute.js";
import type BBranch from "../becca/entities/bbranch.js";
import type BEtapiToken from "../becca/entities/betapi_token.js";
import type BNote from "../becca/entities/bnote.js";
import type BOption from "../becca/entities/boption.js";
import type BRevision from "../becca/entities/brevision.js";
import appInfo from "./app_info.js";
import attributeService from "./attributes.js";
import type { ApiParams } from "./backend_script_api_interface.js";
import backupService from "./backup.js";
import branchService from "./branches.js";
import cloningService from "./cloning.js";
import config from "./config.js";
import dateNoteService from "./date_notes.js";
import exportService from "./export/zip.js";
import log from "./log.js";
import noteService from "./notes.js";
import sql from "./sql.js";
import { randomString, escapeHtml, unescapeHtml } from "./utils.js";
import attributeService from "./attributes.js";
import dateNoteService from "./date_notes.js";
import treeService from "./tree.js";
import config from "./config.js";
import axios from "axios";
import { dayjs } from "@triliumnext/commons";
import xml2js from "xml2js";
import * as cheerio from "cheerio";
import cloningService from "./cloning.js";
import appInfo from "./app_info.js";
import searchService from "./search/services/search.js";
import optionsService from "./options.js";
import SearchContext from "./search/search_context.js";
import becca from "../becca/becca.js";
import ws from "./ws.js";
import searchService from "./search/services/search.js";
import SpacedUpdate from "./spaced_update.js";
import specialNotesService from "./special_notes.js";
import branchService from "./branches.js";
import exportService from "./export/zip.js";
import sql from "./sql.js";
import syncMutex from "./sync_mutex.js";
import backupService from "./backup.js";
import optionsService from "./options.js";
import { formatLogMessage } from "@triliumnext/commons";
import type BNote from "../becca/entities/bnote.js";
import type AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type BBranch from "../becca/entities/bbranch.js";
import type BAttribute from "../becca/entities/battribute.js";
import type BAttachment from "../becca/entities/battachment.js";
import type BRevision from "../becca/entities/brevision.js";
import type BEtapiToken from "../becca/entities/betapi_token.js";
import type BOption from "../becca/entities/boption.js";
import type { AttributeRow } from "@triliumnext/commons";
import type Becca from "../becca/becca-interface.js";
import type { NoteParams } from "./note-interface.js";
import type { ApiParams } from "./backend_script_api_interface.js";
import treeService from "./tree.js";
import { escapeHtml, randomString, unescapeHtml } from "./utils.js";
import ws from "./ws.js";
/**
* A whole number
@ -506,7 +503,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
throw new Error(`Unable to find parent note with ID ${parentNote}.`);
}
let extraOptions: NoteParams = {
const extraOptions: NoteParams = {
..._extraOptions,
content: "",
type: "text",
@ -620,13 +617,13 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
}
const parentNoteId = opts.isVisible ? "_lbVisibleLaunchers" : "_lbAvailableLaunchers";
const noteId = "al_" + opts.id;
const noteId = `al_${ opts.id}`;
const launcherNote =
becca.getNote(noteId) ||
specialNotesService.createLauncher({
noteId: noteId,
parentNoteId: parentNoteId,
noteId,
parentNoteId,
launcherType: opts.type
}).note;
@ -680,7 +677,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
ws.sendMessageToAllClients({
type: "execute-script",
script: script,
script,
params: prepareParams(params),
startNoteId: this.startNote?.noteId,
currentNoteId: this.currentNote.noteId,
@ -696,9 +693,9 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
return params.map((p) => {
if (typeof p === "function") {
return `!@#Function: ${p.toString()}`;
} else {
return p;
}
return p;
});
}
};

View File

@ -1,5 +1,6 @@
import type { AbstractBeccaEntity } from "@triliumnext/core";
import type { Request, Response } from "express";
import type AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type BNote from "../becca/entities/bnote.js";
export interface ApiParams {

View File

@ -1,5 +0,0 @@
export interface Blob {
blobId: string;
content: string | Buffer;
utcDateModified: string;
}

View File

@ -1,11 +1,12 @@
import log from "./log.js";
import becca from "../becca/becca.js";
import cloningService from "./cloning.js";
import branchService from "./branches.js";
import { randomString } from "./utils.js";
import eraseService from "./erase.js";
import type BNote from "../becca/entities/bnote.js";
import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons";
import { erase as eraseService } from "@triliumnext/core";
import becca from "../becca/becca.js";
import type BNote from "../becca/entities/bnote.js";
import branchService from "./branches.js";
import cloningService from "./cloning.js";
import log from "./log.js";
import { randomString } from "./utils.js";
type ActionHandler<T> = (action: T, note: BNote) => void;

View File

@ -1,190 +1,2 @@
"use strict";
import sql from "./sql.js";
import eventChangesService from "./entity_changes.js";
import treeService from "./tree.js";
import BBranch from "../becca/entities/bbranch.js";
import becca from "../becca/becca.js";
import log from "./log.js";
import { CloneResponse } from "@triliumnext/commons";
function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: string | null = null): CloneResponse {
if (!(noteId in becca.notes) || !(parentNoteId in becca.notes)) {
return { success: false, message: "Note cannot be cloned because either the cloned note or the intended parent is deleted." };
}
const parentNote = becca.getNote(parentNoteId);
if (!parentNote) {
return { success: false, message: "Note cannot be cloned because the parent note could not be found." };
}
if (parentNote.type === "search") {
return {
success: false,
message: "Can't clone into a search note"
};
}
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
if (!validationResult.success) {
return validationResult;
}
const branch = new BBranch({
noteId: noteId,
parentNoteId: parentNoteId,
prefix: prefix,
isExpanded: false
}).save();
log.info(`Cloned note '${noteId}' to a new parent note '${parentNoteId}' with prefix '${prefix}'`);
return {
success: true,
branchId: branch.branchId,
notePath: `${parentNote.getBestNotePathString()}/${noteId}`
};
}
function cloneNoteToBranch(noteId: string, parentBranchId: string, prefix?: string) {
const parentBranch = becca.getBranch(parentBranchId);
if (!parentBranch) {
return { success: false, message: `Parent branch '${parentBranchId}' does not exist.` };
}
const ret = cloneNoteToParentNote(noteId, parentBranch.noteId, prefix);
parentBranch.isExpanded = true; // the new target should be expanded, so it immediately shows up to the user
parentBranch.save();
return ret;
}
function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefix?: string) {
if (!(noteId in becca.notes)) {
return { branch: null, success: false, message: `Note '${noteId}' is deleted.` };
} else if (!(parentNoteId in becca.notes)) {
return { branch: null, success: false, message: `Note '${parentNoteId}' is deleted.` };
}
const parentNote = becca.getNote(parentNoteId);
if (!parentNote) {
return { branch: null, success: false, message: "Can't find parent note." };
}
if (parentNote.type === "search") {
return { branch: null, success: false, message: "Can't clone into a search note" };
}
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
if (!validationResult.success) {
return validationResult;
}
const branch = new BBranch({
noteId: noteId,
parentNoteId: parentNoteId,
prefix: prefix,
isExpanded: false
}).save();
log.info(`Ensured note '${noteId}' is in parent note '${parentNoteId}' with prefix '${branch.prefix}'`);
return { branch: branch, success: true };
}
function ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string) {
const branchId = sql.getValue<string>(/*sql*/`SELECT branchId FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]);
const branch = becca.getBranch(branchId);
if (branch) {
if (!branch.isWeak && branch.getNote().getStrongParentBranches().length <= 1) {
return {
success: false,
message: `Cannot remove branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' because this would delete the note as well.`
};
}
branch.deleteBranch();
log.info(`Ensured note '${noteId}' is NOT in parent note '${parentNoteId}'`);
return { success: true };
}
}
function toggleNoteInParent(present: boolean, noteId: string, parentNoteId: string, prefix?: string) {
if (present) {
return ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
} else {
return ensureNoteIsAbsentFromParent(noteId, parentNoteId);
}
}
function cloneNoteAfter(noteId: string, afterBranchId: string) {
if (["_hidden", "root"].includes(noteId)) {
return { success: false, message: `Cloning the note '${noteId}' is forbidden.` };
}
const afterBranch = becca.getBranch(afterBranchId);
if (!afterBranch) {
return { success: false, message: `Branch '${afterBranchId}' does not exist.` };
}
if (afterBranch.noteId === "_hidden") {
return { success: false, message: "Cannot clone after the hidden branch." };
}
const afterNote = becca.getBranch(afterBranchId);
if (!(noteId in becca.notes)) {
return { success: false, message: `Note to be cloned '${noteId}' is deleted or does not exist.` };
} else if (!afterNote || !(afterNote.parentNoteId in becca.notes)) {
return { success: false, message: `After note '${afterNote?.parentNoteId}' is deleted or does not exist.` };
}
const parentNote = becca.getNote(afterNote.parentNoteId);
if (!parentNote || parentNote.type === "search") {
return {
success: false,
message: "Can't clone into a search note"
};
}
const validationResult = treeService.validateParentChild(afterNote.parentNoteId, noteId);
if (!validationResult.success) {
return validationResult;
}
// we don't change utcDateModified, so other changes are prioritized in case of conflict
// also we would have to sync all those modified branches otherwise hash checks would fail
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, afterNote.notePosition]);
eventChangesService.putNoteReorderingEntityChange(afterNote.parentNoteId);
const branch = new BBranch({
noteId: noteId,
parentNoteId: afterNote.parentNoteId,
notePosition: afterNote.notePosition + 10,
isExpanded: false
}).save();
log.info(`Cloned note '${noteId}' into parent note '${afterNote.parentNoteId}' after note '${afterNote.noteId}', branch '${afterBranchId}'`);
return { success: true, branchId: branch.branchId };
}
export default {
cloneNoteToBranch,
cloneNoteToParentNote,
ensureNoteIsPresentInParent,
ensureNoteIsAbsentFromParent,
toggleNoteInParent,
cloneNoteAfter
};
import { cloning } from "@triliumnext/core";
export default cloning;

View File

@ -1,91 +1,72 @@
import type { EntityChange } from "@triliumnext/commons";
import { getContext } from "@triliumnext/core/src/services/context";
type Callback = (...args: any[]) => any;
import { cls } from "@triliumnext/core";
function init<T>(callback: () => T) {
return getContext().init(callback);
}
function wrap(callback: Callback) {
return () => {
try {
init(callback);
} catch (e: any) {
console.log(`Error occurred: ${e.message}: ${e.stack}`);
}
};
return cls.getContext().init(callback);
}
function getHoistedNoteId() {
return getContext().get("hoistedNoteId") || "root";
return cls.getHoistedNoteId();
}
function getComponentId() {
return getContext().get("componentId");
}
function getLocalNowDateTime() {
return getContext().get("localNowDateTime");
return cls.getComponentId();
}
/** @deprecated */
function disableEntityEvents() {
getContext().set("disableEntityEvents", true);
cls.disableEntityEvents();
}
/** @deprecated */
function enableEntityEvents() {
getContext().set("disableEntityEvents", false);
cls.enableEntityEvents();
}
function isEntityEventsDisabled() {
return !!getContext().get("disableEntityEvents");
return cls.isEntityEventsDisabled();
}
/** @deprecated */
function setMigrationRunning(running: boolean) {
getContext().set("migrationRunning", !!running);
cls.setMigrationRunning(running);
}
/** @deprecated */
function isMigrationRunning() {
return !!getContext().get("migrationRunning");
return cls.isMigrationRunning();
}
function getAndClearEntityChangeIds() {
const entityChangeIds = getContext().get("entityChangeIds") || [];
const entityChangeIds = cls.getContext().get("entityChangeIds") || [];
getContext().set("entityChangeIds", []);
cls.getContext().set("entityChangeIds", []);
return entityChangeIds;
}
function putEntityChange(entityChange: EntityChange) {
if (getContext().get("ignoreEntityChangeIds")) {
return;
}
const entityChangeIds = getContext().get("entityChangeIds") || [];
// store only ID since the record can be modified (e.g., in erase)
entityChangeIds.push(entityChange.id);
getContext().set("entityChangeIds", entityChangeIds);
cls.putEntityChange(entityChange);
}
function ignoreEntityChangeIds() {
getContext().set("ignoreEntityChangeIds", true);
cls.getContext().set("ignoreEntityChangeIds", true);
}
function get(key: string) {
return getContext().get(key);
return cls.getContext().get(key);
}
function set(key: string, value: unknown) {
getContext().set(key, value);
cls.getContext().set(key, value);
}
function reset() {
getContext().reset();
cls.getContext().reset();
}
export const wrap = cls.wrap;
export default {
init,
wrap,
@ -93,7 +74,6 @@ export default {
set,
getHoistedNoteId,
getComponentId,
getLocalNowDateTime,
disableEntityEvents,
enableEntityEvents,
isEntityEventsDisabled,

View File

@ -1,22 +1,19 @@
"use strict";
import sql from "./sql.js";
import sqlInit from "./sql_init.js";
import log from "./log.js";
import ws from "./ws.js";
import syncMutexService from "./sync_mutex.js";
import cls from "./cls.js";
import entityChangesService from "./entity_changes.js";
import optionsService from "./options.js";
import BBranch from "../becca/entities/bbranch.js";
import becca from "../becca/becca.js";
import { hash as getHash, hashedBlobId, randomString } from "../services/utils.js";
import eraseService from "../services/erase.js";
import sanitizeAttributeName from "./sanitize_attribute_name.js";
import noteTypesService from "../services/note_types.js";
import type { BranchRow } from "@triliumnext/commons";
import type { EntityChange } from "@triliumnext/commons";
import becca_loader from "../becca/becca_loader.js";
import { becca_loader, erase as eraseService, utils } from "@triliumnext/core";
import becca from "../becca/becca.js";
import BBranch from "../becca/entities/bbranch.js";
import noteTypesService from "../services/note_types.js";
import { hashedBlobId, randomString } from "../services/utils.js";
import cls from "./cls.js";
import entityChangesService from "./entity_changes.js";
import log from "./log.js";
import optionsService from "./options.js";
import sql from "./sql.js";
import sqlInit from "./sql_init.js";
import syncMutexService from "./sync_mutex.js";
import ws from "./ws.js";
const noteTypes = noteTypesService.getNoteTypeNames();
class ConsistencyChecks {
@ -84,11 +81,11 @@ class ConsistencyChecks {
}
return true;
} else {
logError(`Tree cycle detected at parent-child relationship: '${parentNoteId}' - '${noteId}', whole path: '${path}'`);
this.unrecoveredConsistencyErrors = true;
}
logError(`Tree cycle detected at parent-child relationship: '${parentNoteId}' - '${noteId}', whole path: '${path}'`);
this.unrecoveredConsistencyErrors = true;
} else {
const newPath = path.slice();
newPath.push(noteId);
@ -186,7 +183,7 @@ class ConsistencyChecks {
if (note.getParentBranches().length === 0) {
const newBranch = new BBranch({
parentNoteId: "root",
noteId: noteId,
noteId,
prefix: "recovered"
}).save();
@ -349,7 +346,7 @@ class ConsistencyChecks {
if (this.autoFix) {
const branch = new BBranch({
parentNoteId: "root",
noteId: noteId,
noteId,
prefix: "recovered"
}).save();
@ -485,18 +482,18 @@ class ConsistencyChecks {
if (!blobAlreadyExists) {
// manually creating row since this can also affect deleted notes
sql.upsert("blobs", "blobId", {
noteId: noteId,
noteId,
content: blankContent,
utcDateModified: fakeDate,
dateModified: fakeDate
});
const hash = getHash(randomString(10));
const hash = utils.hash(randomString(10));
entityChangesService.putEntityChange({
entityName: "blobs",
entityId: blobId,
hash: hash,
hash,
isErased: false,
utcDateChanged: fakeDate,
isSynced: true
@ -805,7 +802,7 @@ class ConsistencyChecks {
const attrNames = sql.getColumn<string>(/*sql*/`SELECT DISTINCT name FROM attributes`);
for (const origName of attrNames) {
const fixedName = sanitizeAttributeName(origName);
const fixedName = utils.sanitizeAttributeName(origName);
if (fixedName !== origName) {
if (this.autoFix) {
@ -911,7 +908,7 @@ class ConsistencyChecks {
ws.sendMessageToAllClients({ type: "consistency-checks-failed" });
} else {
log.info(`All consistency checks passed ` + (this.fixedIssues ? "after some fixes" : "with no errors detected") + ` (took ${elapsedTimeMs}ms)`);
log.info(`All consistency checks passed ${ this.fixedIssues ? "after some fixes" : "with no errors detected" } (took ${elapsedTimeMs}ms)`);
}
}
}

View File

@ -1,9 +1,7 @@
"use strict";
import { erase as eraseService,utils } from "@triliumnext/core";
import sql from "./sql.js";
import { hash } from "./utils.js";
import log from "./log.js";
import eraseService from "./erase.js";
import sql from "./sql.js";
type SectorHash = Record<string, string>;
@ -48,7 +46,7 @@ function getEntityHashes() {
for (const entityHashMap of Object.values(hashMap)) {
for (const key in entityHashMap) {
entityHashMap[key] = hash(entityHashMap[key]);
entityHashMap[key] = utils.hash(entityHashMap[key]);
}
}

View File

@ -1,107 +1,2 @@
import { dayjs } from "@triliumnext/commons";
import cls from "./cls.js";
const LOCAL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss.SSSZZ";
const UTC_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ssZ";
function utcNowDateTime() {
return utcDateTimeStr(new Date());
}
// CLS date time is important in web deployments - server often runs in different time zone than user is located in,
// so we'd prefer client timezone to be used to record local dates. For this reason, requests from clients contain
// "trilium-local-now-datetime" header which is then stored in CLS
function localNowDateTime() {
return cls.getLocalNowDateTime() || dayjs().format(LOCAL_DATETIME_FORMAT);
}
function localNowDate() {
const clsDateTime = cls.getLocalNowDateTime();
if (clsDateTime) {
return clsDateTime.substr(0, 10);
} else {
const date = new Date();
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
}
function pad(num: number) {
return num <= 9 ? `0${num}` : `${num}`;
}
function utcDateStr(date: Date) {
return date.toISOString().split("T")[0];
}
function utcDateTimeStr(date: Date) {
return date.toISOString().replace("T", " ");
}
/**
* @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr().
* also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time
*/
function parseDateTime(str: string) {
try {
return new Date(Date.parse(str));
} catch (e: any) {
throw new Error(`Can't parse date from '${str}': ${e.stack}`);
}
}
function parseLocalDate(str: string) {
const datePart = str.substr(0, 10);
// not specifying the timezone and specifying the time means Date.parse() will use the local timezone
return parseDateTime(`${datePart} 12:00:00.000`);
}
function getDateTimeForFile() {
return new Date().toISOString().substr(0, 19).replace(/:/g, "");
}
function validateLocalDateTime(str: string | null | undefined) {
if (!str) {
return;
}
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}[+-][0-9]{4}/.test(str)) {
return `Invalid local date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110+0200'`;
}
if (!dayjs(str, LOCAL_DATETIME_FORMAT)) {
return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`;
}
}
function validateUtcDateTime(str: string | undefined) {
if (!str) {
return;
}
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z/.test(str)) {
return `Invalid UTC date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110Z'`;
}
if (!dayjs(str, UTC_DATETIME_FORMAT)) {
return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`;
}
}
export default {
LOCAL_DATETIME_FORMAT,
UTC_DATETIME_FORMAT,
utcNowDateTime,
localNowDateTime,
localNowDate,
utcDateStr,
utcDateTimeStr,
parseDateTime,
parseLocalDate,
getDateTimeForFile,
validateLocalDateTime,
validateUtcDateTime
};
import { date_utils } from "@triliumnext/core";
export default date_utils;

View File

@ -1,6 +1,5 @@
import { data_encryption } from "@triliumnext/core";
import { data_encryption, OpenIdError } from "@triliumnext/core";
import OpenIdError from "../../errors/open_id_error.js";
import sql from "../sql.js";
import sqlInit from "../sql_init.js";
import utils, { constantTimeCompare } from "../utils.js";

View File

@ -1,208 +1,2 @@
import sql from "./sql.js";
import dateUtils from "./date_utils.js";
import log from "./log.js";
import cls from "./cls.js";
import { randomString } from "./utils.js";
import instanceId from "./instance_id.js";
import becca from "../becca/becca.js";
import blobService from "../services/blob.js";
import type { EntityChange } from "@triliumnext/commons";
import type { Blob } from "./blob-interface.js";
import eventService from "./events.js";
let maxEntityChangeId = 0;
function putEntityChangeWithInstanceId(origEntityChange: EntityChange, instanceId: string) {
const ec = { ...origEntityChange, instanceId };
putEntityChange(ec);
}
function putEntityChangeWithForcedChange(origEntityChange: EntityChange) {
const ec = { ...origEntityChange, changeId: null };
putEntityChange(ec);
}
function putEntityChange(origEntityChange: EntityChange) {
const ec = { ...origEntityChange };
delete ec.id;
if (!ec.changeId) {
ec.changeId = randomString(12);
}
ec.componentId = ec.componentId || cls.getComponentId() || "NA"; // NA = not available
ec.instanceId = ec.instanceId || instanceId;
ec.isSynced = ec.isSynced ? 1 : 0;
ec.isErased = ec.isErased ? 1 : 0;
ec.id = sql.replace("entity_changes", ec);
if (ec.id) {
maxEntityChangeId = Math.max(maxEntityChangeId, ec.id);
}
cls.putEntityChange(ec);
}
function putNoteReorderingEntityChange(parentNoteId: string, componentId?: string) {
putEntityChange({
entityName: "note_reordering",
entityId: parentNoteId,
hash: "N/A",
isErased: false,
utcDateChanged: dateUtils.utcNowDateTime(),
isSynced: true,
componentId,
instanceId
});
eventService.emit(eventService.ENTITY_CHANGED, {
entityName: "note_reordering",
entity: sql.getMap(/*sql*/`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [parentNoteId])
});
}
function putEntityChangeForOtherInstances(ec: EntityChange) {
putEntityChange({
...ec,
changeId: null,
instanceId: null
});
}
function addEntityChangesForSector(entityName: string, sector: string) {
const entityChanges = sql.getRows<EntityChange>(/*sql*/`SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ?`, [entityName, sector]);
let entitiesInserted = entityChanges.length;
sql.transactional(() => {
if (entityName === "blobs") {
entitiesInserted += addEntityChangesForDependingEntity(sector, "notes", "noteId");
entitiesInserted += addEntityChangesForDependingEntity(sector, "attachments", "attachmentId");
entitiesInserted += addEntityChangesForDependingEntity(sector, "revisions", "revisionId");
}
for (const ec of entityChanges) {
putEntityChangeWithForcedChange(ec);
}
});
log.info(`Added sector ${sector} of '${entityName}' (${entitiesInserted} entities) to the sync queue.`);
}
function addEntityChangesForDependingEntity(sector: string, tableName: string, primaryKeyColumn: string) {
// problem in blobs might be caused by problem in entity referencing the blob
const dependingEntityChanges = sql.getRows<EntityChange>(
`
SELECT dep_change.*
FROM entity_changes orig_sector
JOIN ${tableName} ON ${tableName}.blobId = orig_sector.entityId
JOIN entity_changes dep_change ON dep_change.entityName = '${tableName}' AND dep_change.entityId = ${tableName}.${primaryKeyColumn}
WHERE orig_sector.entityName = 'blobs' AND SUBSTR(orig_sector.entityId, 1, 1) = ?`,
[sector]
);
for (const ec of dependingEntityChanges) {
putEntityChangeWithForcedChange(ec);
}
return dependingEntityChanges.length;
}
function cleanupEntityChangesForMissingEntities(entityName: string, entityPrimaryKey: string) {
sql.execute(`
DELETE
FROM entity_changes
WHERE
isErased = 0
AND entityName = '${entityName}'
AND entityId NOT IN (SELECT ${entityPrimaryKey} FROM ${entityName})`);
}
function fillEntityChanges(entityName: string, entityPrimaryKey: string, condition = "") {
cleanupEntityChangesForMissingEntities(entityName, entityPrimaryKey);
sql.transactional(() => {
const entityIds = sql.getColumn<string>(/*sql*/`SELECT ${entityPrimaryKey} FROM ${entityName} ${condition}`);
let createdCount = 0;
for (const entityId of entityIds) {
const existingRows = sql.getValue("SELECT COUNT(1) FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
if (existingRows !== 0) {
// we don't want to replace existing entities (which would effectively cause full resync)
continue;
}
createdCount++;
const ec: Partial<EntityChange> = {
entityName,
entityId,
isErased: false
};
if (entityName === "blobs") {
const blob = sql.getRow<Blob>("SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?", [entityId]);
ec.hash = blobService.calculateContentHash(blob);
ec.utcDateChanged = blob.utcDateModified;
ec.isSynced = true; // blobs are always synced
} else {
const entity = becca.getEntity(entityName, entityId);
if (entity) {
ec.hash = entity.generateHash();
ec.utcDateChanged = entity.getUtcDateChanged() || dateUtils.utcNowDateTime();
ec.isSynced = entityName !== "options" || !!entity.isSynced;
} else {
// entity might be null (not present in becca) when it's deleted
// this will produce different hash value than when entity is being deleted since then
// all normal hashed attributes are being used. Sync should recover from that, though.
ec.hash = "deleted";
ec.utcDateChanged = dateUtils.utcNowDateTime();
ec.isSynced = true; // deletable (the ones with isDeleted) entities are synced
}
}
putEntityChange(ec as EntityChange);
}
if (createdCount > 0) {
log.info(`Created ${createdCount} missing entity changes for entity '${entityName}'.`);
}
});
}
function fillAllEntityChanges() {
sql.transactional(() => {
sql.execute("DELETE FROM entity_changes WHERE isErased = 0");
fillEntityChanges("notes", "noteId");
fillEntityChanges("branches", "branchId");
fillEntityChanges("revisions", "revisionId");
fillEntityChanges("attachments", "attachmentId");
fillEntityChanges("blobs", "blobId");
fillEntityChanges("attributes", "attributeId");
fillEntityChanges("etapi_tokens", "etapiTokenId");
fillEntityChanges("options", "name", "WHERE isSynced = 1");
});
}
function recalculateMaxEntityChangeId() {
maxEntityChangeId = sql.getValue<number>("SELECT COALESCE(MAX(id), 0) FROM entity_changes");
}
export default {
putNoteReorderingEntityChange,
putEntityChangeForOtherInstances,
putEntityChangeWithForcedChange,
putEntityChange,
putEntityChangeWithInstanceId,
fillAllEntityChanges,
addEntityChangesForSector,
getMaxEntityChangeId: () => maxEntityChangeId,
recalculateMaxEntityChangeId
};
import { entity_changes } from "@triliumnext/core";
export default entity_changes;

View File

@ -1,14 +1,15 @@
"use strict";
import mimeTypes from "mime-types";
import html from "html";
import { getContentDisposition, escapeHtml } from "../utils.js";
import mdService from "./markdown.js";
import becca from "../../becca/becca.js";
import type TaskContext from "../task_context.js";
import type BBranch from "../../becca/entities/bbranch.js";
import type { Response } from "express";
import html from "html";
import mimeTypes from "mime-types";
import becca from "../../becca/becca.js";
import type BBranch from "../../becca/entities/bbranch.js";
import type BNote from "../../becca/entities/bnote.js";
import type TaskContext from "../task_context.js";
import { escapeHtml,getContentDisposition } from "../utils.js";
import mdService from "./markdown.js";
import type { ExportFormat } from "./zip/abstract_provider.js";
function exportSingleNote(taskContext: TaskContext<"export">, branch: BBranch, format: ExportFormat, res: Response) {
@ -34,7 +35,7 @@ function exportSingleNote(taskContext: TaskContext<"export">, branch: BBranch, f
taskContext.taskSucceeded(null);
}
export function mapByNoteType(note: BNote, content: string | Buffer<ArrayBufferLike>, format: ExportFormat) {
export function mapByNoteType(note: BNote, content: string | Uint8Array, format: ExportFormat) {
let payload, extension, mime;
if (typeof content !== "string") {

View File

@ -1,29 +1,28 @@
"use strict";
import dateUtils from "../date_utils.js";
import path from "path";
import packageInfo from "../../../package.json" with { type: "json" };
import { getContentDisposition } from "../utils.js";
import protectedSessionService from "../protected_session.js";
import sanitize from "sanitize-filename";
import fs from "fs";
import becca from "../../becca/becca.js";
import { NoteType } from "@triliumnext/commons";
import { ValidationError } from "@triliumnext/core";
import archiver from "archiver";
import type { Response } from "express";
import fs from "fs";
import path from "path";
import sanitize from "sanitize-filename";
import packageInfo from "../../../package.json" with { type: "json" };
import becca from "../../becca/becca.js";
import BBranch from "../../becca/entities/bbranch.js";
import type BNote from "../../becca/entities/bnote.js";
import dateUtils from "../date_utils.js";
import log from "../log.js";
import TaskContext from "../task_context.js";
import ValidationError from "../../errors/validation_error.js";
import type NoteMeta from "../meta/note_meta.js";
import type AttachmentMeta from "../meta/attachment_meta.js";
import type AttributeMeta from "../meta/attribute_meta.js";
import BBranch from "../../becca/entities/bbranch.js";
import type { Response } from "express";
import type NoteMeta from "../meta/note_meta.js";
import type { NoteMetaFile } from "../meta/note_meta.js";
import HtmlExportProvider from "./zip/html.js";
import protectedSessionService from "../protected_session.js";
import TaskContext from "../task_context.js";
import { getContentDisposition } from "../utils.js";
import { AdvancedExportOptions, type ExportFormat, ZipExportProviderData } from "./zip/abstract_provider.js";
import HtmlExportProvider from "./zip/html.js";
import MarkdownExportProvider from "./zip/markdown.js";
import ShareThemeExportProvider from "./zip/share_theme.js";
import type BNote from "../../becca/entities/bnote.js";
import { NoteType } from "@triliumnext/commons";
async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, format: ExportFormat, res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) {
if (!["html", "markdown", "share"].includes(format)) {
@ -73,11 +72,11 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
} while (newName in existingFileNames);
return `${index}_${fileName}`;
} else {
existingFileNames[lcFileName] = 1;
return fileName;
}
existingFileNames[lcFileName] = 1;
return fileName;
}
function getDataFileName(type: NoteType | null, mime: string, baseFileName: string, existingFileNames: Record<string, number>): string {
@ -89,15 +88,15 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
// Crop fileName to avoid its length exceeding 30 and prevent cutting into the extension.
if (fileName.length > 30) {
// We use regex to match the extension to preserve multiple dots in extensions (e.g. .tar.gz).
let match = fileName.match(/(\.[a-zA-Z0-9_.!#-]+)$/);
let ext = match ? match[0] : "";
const match = fileName.match(/(\.[a-zA-Z0-9_.!#-]+)$/);
const ext = match ? match[0] : "";
// Crop the extension if extension length exceeds 30
const croppedExt = ext.slice(-30);
// Crop the file name section and append the cropped extension
fileName = fileName.slice(0, 30 - croppedExt.length) + croppedExt;
}
let existingExtension = path.extname(fileName).toLowerCase();
const existingExtension = path.extname(fileName).toLowerCase();
const newExtension = provider.mapExtension(type, mime, existingExtension, format);
// if the note is already named with the extension (e.g. "image.jpg"), then it's silly to append the exact same extension again
@ -140,7 +139,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
const meta: NoteMeta = {
isClone: true,
noteId: note.noteId,
notePath: notePath,
notePath,
title: note.getTitleOrProtected(),
prefix: branch.prefix,
dataFileName: fileName,
@ -198,16 +197,16 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
meta.attachments = attachments
.toSorted((a, b) => ((a.attachmentId ?? "").localeCompare(b.attachmentId ?? "", "en") ?? 1))
.map((attachment) => {
const attMeta: AttachmentMeta = {
attachmentId: attachment.attachmentId,
title: attachment.title,
role: attachment.role,
mime: attachment.mime,
position: attachment.position,
dataFileName: getDataFileName(null, attachment.mime, baseFileName + "_" + attachment.title, existingFileNames)
};
return attMeta;
});
const attMeta: AttachmentMeta = {
attachmentId: attachment.attachmentId,
title: attachment.title,
role: attachment.role,
mime: attachment.mime,
position: attachment.position,
dataFileName: getDataFileName(null, attachment.mime, `${baseFileName }_${ attachment.title}`, existingFileNames)
};
return attMeta;
});
if (childBranches.length > 0) {
meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName);
@ -319,7 +318,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
}
}
function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note?: BNote): string | Buffer {
function prepareContent(title: string, content: string | Uint8Array, noteMeta: NoteMeta, note?: BNote): string | Uint8Array {
const isText = ["html", "markdown"].includes(noteMeta?.format || "");
if (isText) {
content = content.toString();
@ -340,11 +339,11 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
if (noteMeta.isClone) {
const targetUrl = getNoteTargetUrl(noteMeta.noteId, noteMeta);
let content: string | Buffer = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
let content: string | Uint8Array = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
content = prepareContent(noteMeta.title, content, noteMeta, undefined);
archive.append(content, { name: filePathPrefix + noteMeta.dataFileName });
archive.append(content as Buffer, { name: filePathPrefix + noteMeta.dataFileName });
return;
}
@ -360,7 +359,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
if (noteMeta.dataFileName) {
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta, note);
archive.append(content, {
archive.append(content as string | Buffer, {
name: filePathPrefix + noteMeta.dataFileName,
date: dateUtils.parseDateTime(note.utcDateModified)
});
@ -376,7 +375,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
const attachment = note.getAttachmentById(attachmentMeta.attachmentId);
const content = attachment.getContent();
archive.append(content, {
archive.append(content as Buffer, {
name: filePathPrefix + attachmentMeta.dataFileName,
date: dateUtils.parseDateTime(note.utcDateModified)
});
@ -421,9 +420,9 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
} else if (attr.value === "root" || attr.value?.startsWith("_")) {
// relations to "named" noteIds can be preserved
return true;
} else {
return false;
}
return false;
});
}

View File

@ -52,7 +52,7 @@ export abstract class ZipExportProvider {
}
abstract prepareMeta(metaFile: NoteMetaFile): void;
abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer;
abstract prepareContent(title: string, content: string | Uint8Array, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Uint8Array;
abstract afterDone(rootMeta: NoteMeta): void;
/**

View File

@ -34,7 +34,7 @@ export default class HtmlExportProvider extends ZipExportProvider {
metaFile.files.push(this.cssMeta);
}
prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer {
prepareContent(title: string, content: string | Uint8Array, noteMeta: NoteMeta): string | Uint8Array {
if (noteMeta.format === "html" && typeof content === "string") {
if (!content.substr(0, 100).toLowerCase().includes("<html") && !this.zipExportOptions?.skipHtmlTemplate) {
if (!noteMeta?.notePath?.length) {

View File

@ -1,12 +1,12 @@
import NoteMeta from "../../meta/note_meta"
import { ZipExportProvider } from "./abstract_provider.js"
import NoteMeta from "../../meta/note_meta";
import mdService from "../markdown.js";
import { ZipExportProvider } from "./abstract_provider.js";
export default class MarkdownExportProvider extends ZipExportProvider {
prepareMeta() { }
prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer {
prepareContent(title: string, content: string | Uint8Array, noteMeta: NoteMeta): string | Uint8Array {
if (noteMeta.format === "markdown" && typeof content === "string") {
content = this.rewriteFn(content, noteMeta);
content = mdService.toMarkdown(content);

View File

@ -61,7 +61,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider {
metaFile.files.push(this.indexMeta);
}
prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer {
prepareContent(title: string, content: string | Uint8Array, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Uint8Array {
if (!noteMeta?.notePath?.length) {
throw new Error("Missing note path.");
}
@ -150,7 +150,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider {
const note = this.branch.getNote();
const fullHtml = this.prepareContent(rootMeta.title ?? "", note.getContent(), rootMeta, note, this.branch);
this.archive.append(fullHtml, { name: this.indexMeta.dataFileName });
this.archive.append(fullHtml as Buffer, { name: this.indexMeta.dataFileName });
}
#saveAssets(rootMeta: NoteMeta, assetsMeta: NoteMeta[]) {
@ -166,7 +166,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider {
// Inject the custom fonts.
for (const iconPack of this.iconPacks) {
const extension = MIME_TO_EXTENSION_MAPPINGS[iconPack.fontMime];
let fontData: Buffer | undefined;
let fontData: Uint8Array | undefined;
if (iconPack.builtin) {
fontData = readFileSync(join(getClientDir(), "fonts", `${iconPack.fontAttachmentId}.${extension}`));
} else {
@ -178,7 +178,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider {
continue;
};
const fontFileName = `assets/icon-pack-${iconPack.prefix.toLowerCase()}.${extension}`;
this.archive.append(fontData, { name: fontFileName });
this.archive.append(fontData as Buffer, { name: fontFileName });
}
}

View File

@ -1,251 +1,2 @@
import eventService from "./events.js";
import scriptService from "./script.js";
import treeService from "./tree.js";
import noteService from "./notes.js";
import becca from "../becca/becca.js";
import BAttribute from "../becca/entities/battribute.js";
import hiddenSubtreeService from "./hidden_subtree.js";
import oneTimeTimer from "./one_time_timer.js";
import type BNote from "../becca/entities/bnote.js";
import type AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type { DefinitionObject } from "./promoted_attribute_definition_interface.js";
type Handler = (definition: DefinitionObject, note: BNote, targetNote: BNote) => void;
function runAttachedRelations(note: BNote, relationName: string, originEntity: AbstractBeccaEntity<any>) {
if (!note) {
return;
}
// the same script note can get here with multiple ways, but execute only once
const notesToRun = new Set(
note
.getRelations(relationName)
.map((relation) => relation.getTargetNote())
.filter((note) => !!note) as BNote[]
);
for (const noteToRun of notesToRun) {
scriptService.executeNoteNoException(noteToRun, { originEntity });
}
}
eventService.subscribe(eventService.NOTE_TITLE_CHANGED, (note) => {
runAttachedRelations(note, "runOnNoteTitleChange", note);
if (!note.isRoot()) {
const noteFromCache = becca.notes[note.noteId];
if (!noteFromCache) {
return;
}
for (const parentNote of noteFromCache.parents) {
if (parentNote.hasLabel("sorted")) {
treeService.sortNotesIfNeeded(parentNote.noteId);
}
}
}
});
eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED], ({ entityName, entity }) => {
if (entityName === "attributes") {
runAttachedRelations(entity.getNote(), "runOnAttributeChange", entity);
if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) {
handleSortedAttribute(entity);
} else if (entity.type === "label") {
handleMaybeSortingLabel(entity);
}
} else if (entityName === "notes") {
// ENTITY_DELETED won't trigger anything since all branches/attributes are already deleted at this point
runAttachedRelations(entity, "runOnNoteChange", entity);
}
});
eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => {
if (entityName === "branches") {
const parentNote = becca.getNote(entity.parentNoteId);
if (parentNote?.hasLabel("sorted")) {
treeService.sortNotesIfNeeded(parentNote.noteId);
}
const childNote = becca.getNote(entity.noteId);
if (childNote) {
runAttachedRelations(childNote, "runOnBranchChange", entity);
}
}
});
eventService.subscribe(eventService.NOTE_CONTENT_CHANGE, ({ entity }) => {
runAttachedRelations(entity, "runOnNoteContentChange", entity);
});
eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => {
if (entityName === "attributes") {
runAttachedRelations(entity.getNote(), "runOnAttributeCreation", entity);
if (entity.type === "relation" && entity.name === "template") {
const note = becca.getNote(entity.noteId);
if (!note) {
return;
}
const templateNote = becca.getNote(entity.value);
if (!templateNote) {
return;
}
const content = note.getContent();
if (
["text", "code", "mermaid", "canvas", "relationMap", "mindMap", "webView"].includes(note.type) &&
typeof content === "string" &&
// if the note has already content we're not going to overwrite it with template's one
(!content || content.trim().length === 0) &&
templateNote.hasStringContent()
) {
const templateNoteContent = templateNote.getContent();
if (templateNoteContent) {
note.setContent(templateNoteContent);
}
note.type = templateNote.type;
note.mime = templateNote.mime;
note.save();
}
// we'll copy the children notes only if there's none so far
// this protects against e.g. multiple assignment of template relation resulting in having multiple copies of the subtree
if (note.getChildNotes().length === 0 && !note.isDescendantOfNote(templateNote.noteId)) {
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
}
} else if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) {
handleSortedAttribute(entity);
} else if (entity.type === "label") {
handleMaybeSortingLabel(entity);
}
} else if (entityName === "branches") {
runAttachedRelations(entity.getNote(), "runOnBranchCreation", entity);
if (entity.parentNote?.hasLabel("sorted")) {
treeService.sortNotesIfNeeded(entity.parentNoteId);
}
} else if (entityName === "notes") {
runAttachedRelations(entity, "runOnNoteCreation", entity);
}
});
eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote }) => {
runAttachedRelations(parentNote, "runOnChildNoteCreation", childNote);
});
function processInverseRelations(entityName: string, entity: BAttribute, handler: Handler) {
if (entityName === "attributes" && entity.type === "relation") {
const note = entity.getNote();
const relDefinitions = note.getLabels(`relation:${entity.name}`);
for (const relDefinition of relDefinitions) {
const definition = relDefinition.getDefinition();
if (definition.inverseRelation && definition.inverseRelation.trim()) {
const targetNote = entity.getTargetNote();
if (targetNote) {
handler(definition, note, targetNote);
}
}
}
}
}
function handleSortedAttribute(entity: BAttribute) {
treeService.sortNotesIfNeeded(entity.noteId);
if (entity.isInheritable) {
const note = becca.notes[entity.noteId];
if (note) {
for (const noteId of note.getSubtreeNoteIds()) {
treeService.sortNotesIfNeeded(noteId);
}
}
}
}
function handleMaybeSortingLabel(entity: BAttribute) {
// check if this label is used for sorting, if yes force re-sort
const note = becca.notes[entity.noteId];
// this will not work on deleted notes, but in that case we don't really need to re-sort
if (note) {
for (const parentNote of note.getParentNotes()) {
const sorted = parentNote.getLabelValue("sorted");
if (sorted === null) {
// checking specifically for null since that means the label doesn't exist
// empty valued "sorted" is still valid
continue;
}
if (
sorted.includes(entity.name) || // hacky check if this label is used in the sort
entity.name === "top" ||
entity.name === "bottom"
) {
treeService.sortNotesIfNeeded(parentNote.noteId);
}
}
}
}
eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => {
processInverseRelations(entityName, entity, (definition, note, targetNote) => {
// we need to make sure that also target's inverse attribute exists and if not, then create it
// inverse attribute has to target our note as well
const hasInverseAttribute = targetNote.getRelations(definition.inverseRelation).some((attr) => attr.value === note.noteId);
if (!hasInverseAttribute) {
new BAttribute({
noteId: targetNote.noteId,
type: "relation",
name: definition.inverseRelation || "",
value: note.noteId,
isInheritable: entity.isInheritable
}).save();
// becca will not be updated before we'll check from the other side which would create infinite relation creation (#2269)
targetNote.invalidateThisCache();
}
});
});
eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) => {
processInverseRelations(entityName, entity, (definition: DefinitionObject, note: BNote, targetNote: BNote) => {
// if one inverse attribute is deleted, then the other should be deleted as well
const relations = targetNote.getOwnedRelations(definition.inverseRelation);
for (const relation of relations) {
if (relation.value === note.noteId) {
relation.markAsDeleted();
}
}
});
if (entityName === "branches") {
runAttachedRelations(entity.getNote(), "runOnBranchDeletion", entity);
}
if (entityName === "notes" && entity.noteId.startsWith("_")) {
// "named" note has been deleted, we will probably need to rebuild the hidden subtree
// scheduling so that bulk deletes won't trigger so many checks
oneTimeTimer.scheduleExecution("hidden-subtree-check", 1000, () => hiddenSubtreeService.checkHiddenSubtree());
}
});
export default {
runAttachedRelations
};
import { handlers } from "@triliumnext/core";
export default handlers;

View File

@ -1,498 +1,2 @@
import BAttribute from "../becca/entities/battribute.js";
import BBranch from "../becca/entities/bbranch.js";
import type { HiddenSubtreeItem } from "@triliumnext/commons";
import becca from "../becca/becca.js";
import noteService from "./notes.js";
import log from "./log.js";
import migrationService from "./migration.js";
import { t } from "i18next";
import { cleanUpHelp, getHelpHiddenSubtreeData } from "./in_app_help.js";
import buildLaunchBarConfig from "./hidden_subtree_launcherbar.js";
import buildHiddenSubtreeTemplates from "./hidden_subtree_templates.js";
export const LBTPL_ROOT = "_lbTplRoot";
export const LBTPL_BASE = "_lbTplBase";
export const LBTPL_HEADER = "_lbTplHeader";
export const LBTPL_NOTE_LAUNCHER = "_lbTplLauncherNote";
export const LBTPL_WIDGET = "_lbTplLauncherWidget";
export const LBTPL_COMMAND = "_lbTplLauncherCommand";
export const LBTPL_SCRIPT = "_lbTplLauncherScript";
export const LBTPL_SPACER = "_lbTplSpacer";
export const LBTPL_CUSTOM_WIDGET = "_lbTplCustomWidget";
/*
* Hidden subtree is generated as a "predictable structure" which means that it avoids generating random IDs to always
* produce the same structure. This is needed because it is run on multiple instances in the sync cluster which might produce
* duplicate subtrees. This way, all instances will generate the same structure with the same IDs.
*/
let hiddenSubtreeDefinition: HiddenSubtreeItem;
function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenSubtreeItem {
const launchbarConfig = buildLaunchBarConfig();
return {
id: "_hidden",
title: t("hidden-subtree.root-title"),
type: "doc",
icon: "bx bx-hide",
// we want to keep the hidden subtree always last, otherwise there will be problems with e.g., keyboard navigation
// over tree when it's in the middle
notePosition: 999_999_999,
enforceAttributes: true,
attributes: [
{ type: "label", name: "docName", value: "hidden" }
],
children: [
{
id: "_search",
title: t("hidden-subtree.search-history-title"),
type: "doc"
},
{
id: "_globalNoteMap",
title: t("hidden-subtree.note-map-title"),
type: "noteMap",
attributes: [
{ type: "label", name: "mapRootNoteId", value: "hoisted" },
{ type: "label", name: "keepCurrentHoisting" }
]
},
{
id: "_sqlConsole",
title: t("hidden-subtree.sql-console-history-title"),
type: "doc",
icon: "bx-data"
},
{
id: "_share",
title: t("hidden-subtree.shared-notes-title"),
type: "doc",
attributes: [{ type: "label", name: "docName", value: "share" }]
},
{
id: "_bulkAction",
title: t("hidden-subtree.bulk-action-title"),
type: "doc"
},
{
id: "_backendLog",
title: t("hidden-subtree.backend-log-title"),
type: "contentWidget",
icon: "bx-terminal",
attributes: [
{ type: "label", name: "keepCurrentHoisting" },
{ type: "label", name: "fullContentWidth" }
]
},
{
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
id: "_userHidden",
title: t("hidden-subtree.user-hidden-title"),
type: "doc",
attributes: [{ type: "label", name: "docName", value: "user_hidden" }]
},
{
id: LBTPL_ROOT,
title: t("hidden-subtree.launch-bar-templates-title"),
type: "doc",
children: [
{
id: LBTPL_BASE,
title: t("hidden-subtree.base-abstract-launcher-title"),
type: "doc"
},
{
id: LBTPL_COMMAND,
title: t("hidden-subtree.command-launcher-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "command" },
{ type: "label", name: "docName", value: "launchbar_command_launcher" }
]
},
{
id: LBTPL_NOTE_LAUNCHER,
title: t("hidden-subtree.note-launcher-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "note" },
{ type: "label", name: "relation:target", value: "promoted" },
{ type: "label", name: "relation:hoistedNote", value: "promoted" },
{ type: "label", name: "label:keyboardShortcut", value: "promoted,text" },
{ type: "label", name: "docName", value: "launchbar_note_launcher" }
]
},
{
id: LBTPL_SCRIPT,
title: t("hidden-subtree.script-launcher-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "script" },
{ type: "label", name: "relation:script", value: "promoted" },
{ type: "label", name: "label:keyboardShortcut", value: "promoted,text" },
{ type: "label", name: "docName", value: "launchbar_script_launcher" }
]
},
{
id: LBTPL_WIDGET,
title: t("hidden-subtree.built-in-widget-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "builtinWidget" }
]
},
{
id: LBTPL_SPACER,
title: t("hidden-subtree.spacer-title"),
type: "doc",
icon: "bx-move-vertical",
attributes: [
{ type: "relation", name: "template", value: LBTPL_WIDGET },
{ type: "label", name: "builtinWidget", value: "spacer" },
{ type: "label", name: "label:baseSize", value: "promoted,number" },
{ type: "label", name: "label:growthFactor", value: "promoted,number" },
{ type: "label", name: "docName", value: "launchbar_spacer" }
]
},
{
id: LBTPL_CUSTOM_WIDGET,
title: t("hidden-subtree.custom-widget-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "customWidget" },
{ type: "label", name: "relation:widget", value: "promoted" },
{ type: "label", name: "docName", value: "launchbar_widget_launcher" }
]
}
]
},
{
id: "_lbRoot",
title: t("hidden-subtree.launch-bar-title"),
type: "doc",
icon: "bx-sidebar",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbAvailableLaunchers",
title: t("hidden-subtree.available-launchers-title"),
type: "doc",
icon: "bx-hide",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: launchbarConfig.desktopAvailableLaunchers
},
{
id: "_lbVisibleLaunchers",
title: t("hidden-subtree.visible-launchers-title"),
type: "doc",
icon: "bx-show",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: launchbarConfig.desktopVisibleLaunchers
}
]
},
{
id: "_lbMobileRoot",
title: "Mobile Launch Bar",
type: "doc",
icon: "bx-mobile",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbMobileAvailableLaunchers",
title: t("hidden-subtree.available-launchers-title"),
type: "doc",
icon: "bx-hide",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: launchbarConfig.mobileAvailableLaunchers
},
{
id: "_lbMobileVisibleLaunchers",
title: t("hidden-subtree.visible-launchers-title"),
type: "doc",
icon: "bx-show",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: launchbarConfig.mobileVisibleLaunchers
}
]
},
{
id: "_options",
title: t("hidden-subtree.options-title"),
type: "book",
icon: "bx-cog",
children: [
{ id: "_optionsAppearance", title: t("hidden-subtree.appearance-title"), type: "contentWidget", icon: "bx-layout" },
{ id: "_optionsShortcuts", title: t("hidden-subtree.shortcuts-title"), type: "contentWidget", icon: "bxs-keyboard" },
{ id: "_optionsTextNotes", title: t("hidden-subtree.text-notes"), type: "contentWidget", icon: "bx-text" },
{ id: "_optionsCodeNotes", title: t("hidden-subtree.code-notes-title"), type: "contentWidget", icon: "bx-code" },
{ id: "_optionsImages", title: t("hidden-subtree.images-title"), type: "contentWidget", icon: "bx-image" },
{ id: "_optionsSpellcheck", title: t("hidden-subtree.spellcheck-title"), type: "contentWidget", icon: "bx-check-double" },
{ id: "_optionsPassword", title: t("hidden-subtree.password-title"), type: "contentWidget", icon: "bx-lock" },
{ id: '_optionsMFA', title: t('hidden-subtree.multi-factor-authentication-title'), type: 'contentWidget', icon: 'bx-lock ' },
{ id: "_optionsEtapi", title: t("hidden-subtree.etapi-title"), type: "contentWidget", icon: "bx-extension" },
{ id: "_optionsBackup", title: t("hidden-subtree.backup-title"), type: "contentWidget", icon: "bx-data" },
{ id: "_optionsSync", title: t("hidden-subtree.sync-title"), type: "contentWidget", icon: "bx-wifi" },
{ id: "_optionsAi", title: t("hidden-subtree.ai-llm-title"), type: "contentWidget", icon: "bx-bot" },
{ id: "_optionsOther", title: t("hidden-subtree.other"), type: "contentWidget", icon: "bx-dots-horizontal" },
{ id: "_optionsLocalization", title: t("hidden-subtree.localization"), type: "contentWidget", icon: "bx-world" },
{ id: "_optionsAdvanced", title: t("hidden-subtree.advanced-title"), type: "contentWidget" }
]
},
{
id: "_help",
title: t("hidden-subtree.user-guide"),
type: "book",
icon: "bx-help-circle",
children: helpSubtree,
isExpanded: true
},
buildHiddenSubtreeTemplates()
]
};
}
interface CheckHiddenExtraOpts {
restoreNames?: boolean;
}
function checkHiddenSubtree(force = false, extraOpts: CheckHiddenExtraOpts = {}) {
if (!force && !migrationService.isDbUpToDate()) {
// on-delete hook might get triggered during some future migration and cause havoc
log.info("Will not check hidden subtree until migration is finished.");
return;
}
const helpSubtree = getHelpHiddenSubtreeData();
if (!hiddenSubtreeDefinition || force) {
hiddenSubtreeDefinition = buildHiddenSubtreeDefinition(helpSubtree);
}
checkHiddenSubtreeRecursively("root", hiddenSubtreeDefinition, extraOpts);
try {
cleanUpHelp(helpSubtree);
} catch (e) {
// Non-critical operation should something go wrong.
console.error(e);
}
}
/**
* Get all expected parent IDs for a given note ID from the hidden subtree definition
*/
function getExpectedParentIds(noteId: string, subtree: HiddenSubtreeItem): string[] {
const expectedParents: string[] = [];
function traverse(item: HiddenSubtreeItem, parentId: string) {
if (item.id === noteId) {
expectedParents.push(parentId);
}
if (item.children) {
for (const child of item.children) {
traverse(child, item.id);
}
}
}
// Start traversal from root
if (subtree.id === noteId) {
expectedParents.push("root");
}
if (subtree.children) {
for (const child of subtree.children) {
traverse(child, subtree.id);
}
}
return expectedParents;
}
/**
* Check if a note ID is within the hidden subtree structure
*/
function isWithinHiddenSubtree(noteId: string): boolean {
// Consider a note to be within hidden subtree if it starts with underscore
// This is the convention used for hidden subtree notes
return noteId.startsWith("_") || noteId === "root";
}
function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtreeItem, extraOpts: CheckHiddenExtraOpts = {}) {
if (!item.id || !item.type || !item.title) {
throw new Error(`Item does not contain mandatory properties: ${JSON.stringify(item)}`);
}
if (item.id.charAt(0) !== "_") {
throw new Error(`ID has to start with underscore, given '${item.id}'`);
}
let note = becca.notes[item.id];
let branch;
if (!note) {
// Missing item, add it.
({ note, branch } = noteService.createNewNote({
noteId: item.id,
title: item.title,
type: item.type,
parentNoteId: parentNoteId,
content: item.content ?? "",
ignoreForbiddenParents: true
}));
} else {
// Existing item, check if it's in the right state.
branch = note.getParentBranches().find((branch) => branch.parentNoteId === parentNoteId);
if (item.content && note.getContent() !== item.content) {
log.info(`Updating content of ${item.id}.`);
note.setContent(item.content);
}
// Clean up any branches that shouldn't exist according to the meta definition
// For hidden subtree notes, we want to ensure they only exist in their designated locations
if (item.enforceBranches || item.id.startsWith("_help")) {
// If the note exists but doesn't have a branch in the expected parent,
// create the missing branch to ensure it's in the correct location
if (!branch) {
log.info(`Creating missing branch for note ${item.id} under parent ${parentNoteId}.`);
branch = new BBranch({
noteId: item.id,
parentNoteId: parentNoteId,
notePosition: item.notePosition !== undefined ? item.notePosition : undefined,
isExpanded: item.isExpanded !== undefined ? item.isExpanded : false
}).save();
}
// Remove any branches that are not in the expected parent.
const expectedParents = getExpectedParentIds(item.id, hiddenSubtreeDefinition);
const currentBranches = note.getParentBranches();
for (const currentBranch of currentBranches) {
// Only delete branches that are not in the expected locations
// and are within the hidden subtree structure (avoid touching user-created clones)
if (!expectedParents.includes(currentBranch.parentNoteId) &&
isWithinHiddenSubtree(currentBranch.parentNoteId)) {
log.info(`Removing unexpected branch for note '${item.id}' from parent '${currentBranch.parentNoteId}'`);
currentBranch.markAsDeleted();
}
}
}
}
const attrs = [...(item.attributes || [])];
if (item.icon) {
attrs.push({ type: "label", name: "iconClass", value: `bx ${item.icon}` });
}
if (item.type === "launcher") {
if (item.command) {
attrs.push({ type: "relation", name: "template", value: LBTPL_COMMAND });
attrs.push({ type: "label", name: "command", value: item.command });
} else if (item.builtinWidget) {
if (item.builtinWidget === "spacer") {
attrs.push({ type: "relation", name: "template", value: LBTPL_SPACER });
attrs.push({ type: "label", name: "baseSize", value: item.baseSize });
attrs.push({ type: "label", name: "growthFactor", value: item.growthFactor });
} else {
attrs.push({ type: "relation", name: "template", value: LBTPL_WIDGET });
}
attrs.push({ type: "label", name: "builtinWidget", value: item.builtinWidget });
} else if (item.targetNoteId) {
attrs.push({ type: "relation", name: "template", value: LBTPL_NOTE_LAUNCHER });
attrs.push({ type: "relation", name: "target", value: item.targetNoteId });
} else {
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
}
}
const shouldRestoreNames = extraOpts.restoreNames || note.noteId.startsWith("_help") || item.id.startsWith("_lb") || item.id.startsWith("_template");
if (shouldRestoreNames && note.title !== item.title) {
note.title = item.title;
note.save();
}
if (note.type !== item.type) {
// enforce a correct note type
note.type = item.type;
note.save();
}
if (branch) {
// in case of launchers the branch ID is not preserved and should not be relied upon - launchers which move between
// visible and available will change branch since the branch's parent-child relationship is immutable
if (item.notePosition !== undefined && branch.notePosition !== item.notePosition) {
branch.notePosition = item.notePosition;
branch.save();
}
if (item.isExpanded !== undefined && branch.isExpanded !== item.isExpanded) {
branch.isExpanded = item.isExpanded;
branch.save();
}
}
// Enforce attribute structure if needed.
if (item.enforceAttributes) {
for (const attribute of note.getAttributes()) {
// Remove unwanted attributes.
const attrDef = attrs.find(a => a.name === attribute.name);
if (!attrDef) {
attribute.markAsDeleted();
continue;
}
// Ensure value is consistent.
if (attribute.value !== attrDef.value) {
note.setAttributeValueById(attribute.attributeId, attrDef.value);
}
}
}
for (const attr of attrs) {
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
const existingAttribute = note.getAttributes().find((attr) => attr.attributeId === attrId);
if (!existingAttribute) {
new BAttribute({
attributeId: attrId,
noteId: note.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
isInheritable: attr.isInheritable
}).save();
} else if (attr.name === "docName" || (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
if (existingAttribute.value !== attr.value) {
log.info(`Updating attribute ${attrId} from "${existingAttribute.value}" to "${attr.value}"`);
existingAttribute.value = attr.value ?? "";
existingAttribute.save();
}
}
}
for (const child of item.children || []) {
checkHiddenSubtreeRecursively(item.id, child, extraOpts);
}
}
export default {
checkHiddenSubtree
};
import { hidden_subtree } from "@triliumnext/core";
export default hidden_subtree;

View File

@ -1,17 +1,18 @@
"use strict";
import { sanitize } from "@triliumnext/core";
import imageType from "image-type";
import isAnimated from "is-animated";
import isSvg from "is-svg";
import { Jimp } from "jimp";
import sanitizeFilename from "sanitize-filename";
import becca from "../becca/becca.js";
import log from "./log.js";
import protectedSessionService from "./protected_session.js";
import noteService from "./notes.js";
import optionService from "./options.js";
import protectedSessionService from "./protected_session.js";
import sql from "./sql.js";
import { Jimp } from "jimp";
import imageType from "image-type";
import sanitizeFilename from "sanitize-filename";
import isSvg from "is-svg";
import isAnimated from "is-animated";
import htmlSanitizer from "./html_sanitizer.js";
async function processImage(uploadBuffer: Buffer, originalName: string, shrinkImageSwitch: boolean) {
const compressImages = optionService.getOptionBool("compressImages");
@ -46,9 +47,9 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
async function getImageType(buffer: Buffer) {
if (isSvg(buffer.toString())) {
return { ext: "svg" };
} else {
return (await imageType(buffer)) || { ext: "jpg" }; // optimistic JPG default
}
}
return (await imageType(buffer)) || { ext: "jpg" }; // optimistic JPG default
}
function getImageMimeFromExtension(ext: string) {
@ -60,7 +61,7 @@ function getImageMimeFromExtension(ext: string) {
function updateImage(noteId: string, uploadBuffer: Buffer, originalName: string) {
log.info(`Updating image ${noteId}: ${originalName}`);
originalName = htmlSanitizer.sanitize(originalName);
originalName = sanitize.sanitizeHtml(originalName);
const note = becca.getNote(noteId);
if (!note) {

View File

@ -1,20 +1,20 @@
import type { AttributeType } from "@triliumnext/commons";
import { dayjs } from "@triliumnext/commons";
import { sanitize, utils } from "@triliumnext/core";
import sax from "sax";
import stream from "stream";
import { Throttle } from "stream-throttle";
import log from "../log.js";
import { md5, escapeHtml, fromBase64 } from "../utils.js";
import date_utils from "../date_utils.js";
import sql from "../sql.js";
import noteService from "../notes.js";
import imageService from "../image.js";
import protectedSessionService from "../protected_session.js";
import htmlSanitizer from "../html_sanitizer.js";
import sanitizeAttributeName from "../sanitize_attribute_name.js";
import type TaskContext from "../task_context.js";
import type BNote from "../../becca/entities/bnote.js";
import date_utils from "../date_utils.js";
import imageService from "../image.js";
import log from "../log.js";
import noteService from "../notes.js";
import protectedSessionService from "../protected_session.js";
import sql from "../sql.js";
import type TaskContext from "../task_context.js";
import { escapeHtml, fromBase64,md5 } from "../utils.js";
import type { File } from "./common.js";
import type { AttributeType } from "@triliumnext/commons";
/**
* date format is e.g. 20181121T193703Z or 2013-04-14T16:19:00.000Z (Mac evernote, see #3496)
@ -25,7 +25,7 @@ function parseDate(text: string) {
text = text.replace(/[-:]/g, "");
// insert - and : to convert it to trilium format
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2) + " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
text = `${text.substr(0, 4) }-${ text.substr(4, 2) }-${ text.substr(6, 2) } ${ text.substr(9, 2) }:${ text.substr(11, 2) }:${ text.substr(13, 2) }.000Z`;
return text;
}
@ -117,7 +117,7 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
"\u2611 "
);
content = htmlSanitizer.sanitize(content);
content = sanitize.sanitizeHtml(content);
return content;
}
@ -155,7 +155,7 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
labelName = "pageUrl";
}
labelName = sanitizeAttributeName(labelName || "");
labelName = utils.sanitizeAttributeName(labelName || "");
if (note.attributes) {
note.attributes.push({
@ -201,7 +201,7 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
} else if (currentTag === "tag" && note.attributes) {
note.attributes.push({
type: "label",
name: sanitizeAttributeName(text),
name: utils.sanitizeAttributeName(text),
value: ""
});
}
@ -367,7 +367,7 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
}
}
content = htmlSanitizer.sanitize(content);
content = sanitize.sanitizeHtml(content);
// save updated content with links to files/images
noteEntity.setContent(content);

View File

@ -2,10 +2,10 @@
import { getMimeTypeFromMarkdownName, MIME_TYPE_AUTO } from "@triliumnext/commons";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { sanitize } from "@triliumnext/core";
import { parse, Renderer, type Tokens,use } from "marked";
import { ADMONITION_TYPE_MAPPINGS } from "../export/markdown.js";
import htmlSanitizer from "../html_sanitizer.js";
import utils from "../utils.js";
import wikiLinkInternalLink from "./markdown/wikilink_internal_link.js";
import wikiLinkTransclusion from "./markdown/wikilink_transclusion.js";
@ -151,7 +151,7 @@ function renderToHtml(content: string, title: string) {
// h1 handling needs to come before sanitization
html = importUtils.handleH1(html, title);
html = htmlSanitizer.sanitize(html);
html = sanitize.sanitizeHtml(html);
// Add a trailing semicolon to CSS styles.
html = html.replaceAll(/(<(img|figure|col).*?style=".*?)"/g, "$1;\"");

View File

@ -1,11 +1,12 @@
"use strict";
import noteService from "../../services/notes.js";
import { sanitize } from "@triliumnext/core";
import xml2js from "xml2js";
import protectedSessionService from "../protected_session.js";
import htmlSanitizer from "../html_sanitizer.js";
import type TaskContext from "../task_context.js";
import type BNote from "../../becca/entities/bnote.js";
import noteService from "../../services/notes.js";
import protectedSessionService from "../protected_session.js";
import type TaskContext from "../task_context.js";
const parseString = xml2js.parseString;
interface OpmlXml {
@ -29,8 +30,8 @@ interface OpmlOutline {
}
async function importOpml(taskContext: TaskContext<"importNotes">, fileBuffer: string | Buffer, parentNote: BNote) {
const xml = await new Promise<OpmlXml>(function (resolve, reject) {
parseString(fileBuffer, function (err: any, result: OpmlXml) {
const xml = await new Promise<OpmlXml>((resolve, reject) => {
parseString(fileBuffer, (err: any, result: OpmlXml) => {
if (err) {
reject(err);
} else {
@ -64,7 +65,7 @@ async function importOpml(taskContext: TaskContext<"importNotes">, fileBuffer: s
throw new Error(`Unrecognized OPML version ${opmlVersion}`);
}
content = htmlSanitizer.sanitize(content || "");
content = sanitize.sanitizeHtml(content || "");
const { note } = noteService.createNewNote({
parentNoteId,

View File

@ -1,18 +1,16 @@
"use strict";
import type { NoteType } from "@triliumnext/commons";
import { sanitize } from "@triliumnext/core";
import type BNote from "../../becca/entities/bnote.js";
import type TaskContext from "../task_context.js";
import noteService from "../../services/notes.js";
import imageService from "../../services/image.js";
import noteService from "../../services/notes.js";
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
import protectedSessionService from "../protected_session.js";
import type TaskContext from "../task_context.js";
import type { File } from "./common.js";
import markdownService from "./markdown.js";
import mimeService from "./mime.js";
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
import importUtils from "./utils.js";
import htmlSanitizer from "../html_sanitizer.js";
import type { File } from "./common.js";
import type { NoteType } from "@triliumnext/commons";
function importSingleFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const mime = mimeService.getMime(file.originalname) || file.mimetype;
@ -88,7 +86,7 @@ function importCodeNote(taskContext: TaskContext<"importNotes">, file: File, par
title,
content,
type,
mime: mime,
mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
@ -106,7 +104,7 @@ function importCustomType(taskContext: TaskContext<"importNotes">, file: File, p
title,
content,
type,
mime: mime,
mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
@ -157,7 +155,7 @@ function importMarkdown(taskContext: TaskContext<"importNotes">, file: File, par
let htmlContent = markdownService.renderToHtml(markdownContent, title);
if (taskContext.data?.safeImport) {
htmlContent = htmlSanitizer.sanitize(htmlContent);
htmlContent = sanitize.sanitizeHtml(htmlContent);
}
const { note } = noteService.createNewNote({
@ -185,7 +183,7 @@ function importHtml(taskContext: TaskContext<"importNotes">, file: File, parentN
content = importUtils.handleH1(content, title);
if (taskContext?.data?.safeImport) {
content = htmlSanitizer.sanitize(content);
content = sanitize.sanitizeHtml(content);
}
const { note } = noteService.createNewNote({
@ -214,7 +212,7 @@ function importAttachment(taskContext: TaskContext<"importNotes">, file: File, p
title: file.originalname,
content: file.buffer,
role: "file",
mime: mime
mime
});
taskContext.increaseProgressCount();

View File

@ -1,26 +1,27 @@
"use strict";
import BAttribute from "../../becca/entities/battribute.js";
import { removeTextFileExtension, newEntityId, getNoteTitle, processStringOrBuffer, unescapeHtml } from "../../services/utils.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import attributeService from "../../services/attributes.js";
import BBranch from "../../becca/entities/bbranch.js";
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
import { sanitize } from "@triliumnext/core";
import path from "path";
import protectedSessionService from "../protected_session.js";
import mimeService from "./mime.js";
import treeService from "../tree.js";
import type { Stream } from "stream";
import yauzl from "yauzl";
import htmlSanitizer from "../html_sanitizer.js";
import becca from "../../becca/becca.js";
import BAttachment from "../../becca/entities/battachment.js";
import markdownService from "./markdown.js";
import type TaskContext from "../task_context.js";
import BAttribute from "../../becca/entities/battribute.js";
import BBranch from "../../becca/entities/bbranch.js";
import type BNote from "../../becca/entities/bnote.js";
import type NoteMeta from "../meta/note_meta.js";
import attributeService from "../../services/attributes.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import { getNoteTitle, newEntityId, processStringOrBuffer, removeTextFileExtension, unescapeHtml } from "../../services/utils.js";
import type AttributeMeta from "../meta/attribute_meta.js";
import type { Stream } from "stream";
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
import type NoteMeta from "../meta/note_meta.js";
import protectedSessionService from "../protected_session.js";
import type TaskContext from "../task_context.js";
import treeService from "../tree.js";
import markdownService from "./markdown.js";
import mimeService from "./mime.js";
interface MetaFile {
files: NoteMeta[];
@ -108,7 +109,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
dataFileName: ""
};
let parent: NoteMeta | undefined = undefined;
let parent: NoteMeta | undefined;
for (let segment of pathSegments) {
if (!cursor?.children?.length) {
@ -216,8 +217,8 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
}
if (taskContext.data?.safeImport) {
attr.name = htmlSanitizer.sanitize(attr.name);
attr.value = htmlSanitizer.sanitize(attr.value);
attr.name = sanitize.sanitizeHtml(attr.name);
attr.value = sanitize.sanitizeHtml(attr.value);
}
attributes.push(attr);
@ -241,10 +242,10 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
}
const { note } = noteService.createNewNote({
parentNoteId: parentNoteId,
parentNoteId,
title: noteTitle || "",
content: "",
noteId: noteId,
noteId,
type: resolveNoteType(noteMeta?.type),
mime: noteMeta ? noteMeta.mime : "text/html",
prefix: noteMeta?.prefix || "",
@ -294,12 +295,12 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
noteId: getNewNoteId(noteMeta.noteId)
};
} else {
// don't check for noteMeta since it's not mandatory for notes
return {
noteId: getNoteId(noteMeta, absUrl)
};
}
// don't check for noteMeta since it's not mandatory for notes
return {
noteId: getNoteId(noteMeta, absUrl)
};
}
function processTextNoteContent(content: string, noteTitle: string, filePath: string, noteMeta?: NoteMeta) {
@ -312,13 +313,13 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
if (noteTitle.trim() === text.trim()) {
return ""; // remove whole H1 tag
} else {
return `<h2>${text}</h2>`;
}
return `<h2>${text}</h2>`;
});
if (taskContext.data?.safeImport) {
content = htmlSanitizer.sanitize(content);
content = sanitize.sanitizeHtml(content);
}
content = content.replace(/<html.*<body[^>]*>/gis, "");
@ -347,9 +348,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
return `src="api/attachments/${target.attachmentId}/image/${path.basename(url)}"`;
} else if (target.noteId) {
return `src="api/images/${target.noteId}/${path.basename(url)}"`;
} else {
return match;
}
return match;
});
content = content.replace(/href="([^"]*)"/g, (match, url) => {
@ -373,9 +374,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
return `href="#root/${target.noteId}?viewMode=attachments&attachmentId=${target.attachmentId}"`;
} else if (target.noteId) {
return `href="#root/${target.noteId}"`;
} else {
return match;
}
return match;
});
if (noteMeta) {
@ -525,9 +526,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
}
({ note } = noteService.createNewNote({
parentNoteId: parentNoteId,
parentNoteId,
title: noteTitle || "",
content: content,
content,
noteId,
type,
mime,
@ -536,7 +537,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
// root notePosition should be ignored since it relates to the original document
// now import root should be placed after existing notes into new parent
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
isProtected: isProtected
isProtected
}));
createdNoteIds.add(note.noteId);
@ -648,7 +649,7 @@ function streamToBuffer(stream: Stream): Promise<Buffer> {
export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
return new Promise((res, rej) => {
zipfile.openReadStream(entry, function (err, readStream) {
zipfile.openReadStream(entry, (err, readStream) => {
if (err) rej(err);
if (!readStream) throw new Error("Unable to read content.");
@ -659,7 +660,7 @@ export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise
export function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => Promise<void>) {
return new Promise<void>((res, rej) => {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, function (err, zipfile) {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, (err, zipfile) => {
if (err) rej(err);
if (!zipfile) throw new Error("Unable to read zip file.");
@ -691,9 +692,9 @@ function resolveNoteType(type: string | undefined): NoteType {
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {
return type as NoteType;
} else {
return "text";
}
return "text";
}
export function removeTriliumTags(content: string) {
@ -702,7 +703,7 @@ export function removeTriliumTags(content: string) {
"<title data-trilium-title>([^<]*)<\/title>"
];
for (const tag of tagsToRemove) {
let re = new RegExp(tag, "gi");
const re = new RegExp(tag, "gi");
content = content.replace(re, "");
}

View File

@ -1,5 +0,0 @@
import { randomString } from "./utils.js";
const instanceId = randomString(12);
export default instanceId;

View File

@ -1,882 +1,2 @@
"use strict";
import optionService from "./options.js";
import log from "./log.js";
import { isElectron, isMac } from "./utils.js";
import type { ActionKeyboardShortcut, KeyboardShortcut } from "@triliumnext/commons";
import { t } from "i18next";
function getDefaultKeyboardActions() {
if (!t("keyboard_actions.note-navigation")) {
throw new Error("Keyboard actions loaded before translations.");
}
const DEFAULT_KEYBOARD_ACTIONS: KeyboardShortcut[] = [
{
separator: t("keyboard_actions.note-navigation")
},
{
actionName: "backInNoteHistory",
friendlyName: t("keyboard_action_names.back-in-note-history"),
iconClass: "bx bxs-chevron-left",
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
defaultShortcuts: isMac ? ["CommandOrControl+["] : ["Alt+Left"],
description: t("keyboard_actions.back-in-note-history"),
scope: "window"
},
{
actionName: "forwardInNoteHistory",
friendlyName: t("keyboard_action_names.forward-in-note-history"),
iconClass: "bx bxs-chevron-right",
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
defaultShortcuts: isMac ? ["CommandOrControl+]"] : ["Alt+Right"],
description: t("keyboard_actions.forward-in-note-history"),
scope: "window"
},
{
actionName: "jumpToNote",
friendlyName: t("keyboard_action_names.jump-to-note"),
defaultShortcuts: ["CommandOrControl+J"],
description: t("keyboard_actions.open-jump-to-note-dialog"),
scope: "window",
ignoreFromCommandPalette: true
},
{
actionName: "openTodayNote",
friendlyName: t("hidden-subtree.open-today-journal-note-title"),
iconClass: "bx bx-calendar",
defaultShortcuts: [],
description: t("hidden-subtree.open-today-journal-note-title"),
scope: "window"
},
{
actionName: "commandPalette",
friendlyName: t("keyboard_action_names.command-palette"),
defaultShortcuts: ["CommandOrControl+Shift+J"],
description: t("keyboard_actions.open-command-palette"),
scope: "window",
ignoreFromCommandPalette: true
},
{
actionName: "scrollToActiveNote",
friendlyName: t("keyboard_action_names.scroll-to-active-note"),
defaultShortcuts: ["CommandOrControl+."],
iconClass: "bx bx-current-location",
description: t("keyboard_actions.scroll-to-active-note"),
scope: "window"
},
{
actionName: "quickSearch",
friendlyName: t("keyboard_action_names.quick-search"),
iconClass: "bx bx-search",
defaultShortcuts: ["CommandOrControl+S"],
description: t("keyboard_actions.quick-search"),
scope: "window"
},
{
actionName: "searchInSubtree",
friendlyName: t("keyboard_action_names.search-in-subtree"),
defaultShortcuts: ["CommandOrControl+Shift+S"],
iconClass: "bx bx-search-alt",
description: t("keyboard_actions.search-in-subtree"),
scope: "note-tree"
},
{
actionName: "expandSubtree",
friendlyName: t("keyboard_action_names.expand-subtree"),
defaultShortcuts: [],
iconClass: "bx bx-layer-plus",
description: t("keyboard_actions.expand-subtree"),
scope: "note-tree"
},
{
actionName: "collapseTree",
friendlyName: t("keyboard_action_names.collapse-tree"),
defaultShortcuts: ["Alt+C"],
iconClass: "bx bx-layer-minus",
description: t("keyboard_actions.collapse-tree"),
scope: "window"
},
{
actionName: "collapseSubtree",
friendlyName: t("keyboard_action_names.collapse-subtree"),
iconClass: "bx bxs-layer-minus",
defaultShortcuts: ["Alt+-"],
description: t("keyboard_actions.collapse-subtree"),
scope: "note-tree"
},
{
actionName: "sortChildNotes",
friendlyName: t("keyboard_action_names.sort-child-notes"),
iconClass: "bx bx-sort-down",
defaultShortcuts: ["Alt+S"],
description: t("keyboard_actions.sort-child-notes"),
scope: "note-tree"
},
{
separator: t("keyboard_actions.creating-and-moving-notes")
},
{
actionName: "createNoteAfter",
friendlyName: t("keyboard_action_names.create-note-after"),
iconClass: "bx bx-plus",
defaultShortcuts: ["CommandOrControl+O"],
description: t("keyboard_actions.create-note-after"),
scope: "window"
},
{
actionName: "createNoteInto",
friendlyName: t("keyboard_action_names.create-note-into"),
iconClass: "bx bx-plus",
defaultShortcuts: ["CommandOrControl+P"],
description: t("keyboard_actions.create-note-into"),
scope: "window"
},
{
actionName: "createNoteIntoInbox",
friendlyName: t("keyboard_action_names.create-note-into-inbox"),
iconClass: "bx bxs-inbox",
defaultShortcuts: ["global:CommandOrControl+Alt+P"],
description: t("keyboard_actions.create-note-into-inbox"),
scope: "window"
},
{
actionName: "deleteNotes",
friendlyName: t("keyboard_action_names.delete-notes"),
iconClass: "bx bx-trash",
defaultShortcuts: ["Delete"],
description: t("keyboard_actions.delete-note"),
scope: "note-tree"
},
{
actionName: "moveNoteUp",
friendlyName: t("keyboard_action_names.move-note-up"),
iconClass: "bx bx-up-arrow-alt",
defaultShortcuts: isMac ? ["Alt+Up"] : ["CommandOrControl+Up"],
description: t("keyboard_actions.move-note-up"),
scope: "note-tree"
},
{
actionName: "moveNoteDown",
friendlyName: t("keyboard_action_names.move-note-down"),
iconClass: "bx bx-down-arrow-alt",
defaultShortcuts: isMac ? ["Alt+Down"] : ["CommandOrControl+Down"],
description: t("keyboard_actions.move-note-down"),
scope: "note-tree"
},
{
actionName: "moveNoteUpInHierarchy",
friendlyName: t("keyboard_action_names.move-note-up-in-hierarchy"),
iconClass: "bx bx-arrow-from-bottom",
defaultShortcuts: isMac ? ["Alt+Left"] : ["CommandOrControl+Left"],
description: t("keyboard_actions.move-note-up-in-hierarchy"),
scope: "note-tree"
},
{
actionName: "moveNoteDownInHierarchy",
friendlyName: t("keyboard_action_names.move-note-down-in-hierarchy"),
iconClass: "bx bx-arrow-from-top",
defaultShortcuts: isMac ? ["Alt+Right"] : ["CommandOrControl+Right"],
description: t("keyboard_actions.move-note-down-in-hierarchy"),
scope: "note-tree"
},
{
actionName: "editNoteTitle",
friendlyName: t("keyboard_action_names.edit-note-title"),
iconClass: "bx bx-rename",
defaultShortcuts: ["Enter"],
description: t("keyboard_actions.edit-note-title"),
scope: "note-tree"
},
{
actionName: "editBranchPrefix",
friendlyName: t("keyboard_action_names.edit-branch-prefix"),
iconClass: "bx bx-rename",
defaultShortcuts: ["F2"],
description: t("keyboard_actions.edit-branch-prefix"),
scope: "note-tree"
},
{
actionName: "cloneNotesTo",
friendlyName: t("keyboard_action_names.clone-notes-to"),
iconClass: "bx bx-duplicate",
defaultShortcuts: ["CommandOrControl+Shift+C"],
description: t("keyboard_actions.clone-notes-to"),
scope: "window"
},
{
actionName: "moveNotesTo",
friendlyName: t("keyboard_action_names.move-notes-to"),
iconClass: "bx bx-transfer",
defaultShortcuts: ["CommandOrControl+Shift+X"],
description: t("keyboard_actions.move-notes-to"),
scope: "window"
},
{
separator: t("keyboard_actions.note-clipboard")
},
{
actionName: "copyNotesToClipboard",
friendlyName: t("keyboard_action_names.copy-notes-to-clipboard"),
iconClass: "bx bx-copy",
defaultShortcuts: ["CommandOrControl+C"],
description: t("keyboard_actions.copy-notes-to-clipboard"),
scope: "note-tree"
},
{
actionName: "pasteNotesFromClipboard",
friendlyName: t("keyboard_action_names.paste-notes-from-clipboard"),
iconClass: "bx bx-paste",
defaultShortcuts: ["CommandOrControl+V"],
description: t("keyboard_actions.paste-notes-from-clipboard"),
scope: "note-tree"
},
{
actionName: "cutNotesToClipboard",
friendlyName: t("keyboard_action_names.cut-notes-to-clipboard"),
iconClass: "bx bx-cut",
defaultShortcuts: ["CommandOrControl+X"],
description: t("keyboard_actions.cut-notes-to-clipboard"),
scope: "note-tree"
},
{
actionName: "selectAllNotesInParent",
friendlyName: t("keyboard_action_names.select-all-notes-in-parent"),
iconClass: "bx bx-select-multiple",
defaultShortcuts: ["CommandOrControl+A"],
description: t("keyboard_actions.select-all-notes-in-parent"),
scope: "note-tree"
},
{
actionName: "addNoteAboveToSelection",
friendlyName: t("keyboard_action_names.add-note-above-to-selection"),
defaultShortcuts: ["Shift+Up"],
description: t("keyboard_actions.add-note-above-to-the-selection"),
scope: "note-tree",
ignoreFromCommandPalette: true
},
{
actionName: "addNoteBelowToSelection",
friendlyName: t("keyboard_action_names.add-note-below-to-selection"),
defaultShortcuts: ["Shift+Down"],
description: t("keyboard_actions.add-note-below-to-selection"),
scope: "note-tree",
ignoreFromCommandPalette: true
},
{
actionName: "duplicateSubtree",
friendlyName: t("keyboard_action_names.duplicate-subtree"),
iconClass: "bx bx-outline",
defaultShortcuts: [],
description: t("keyboard_actions.duplicate-subtree"),
scope: "note-tree"
},
{
separator: t("keyboard_actions.tabs-and-windows")
},
{
actionName: "openNewTab",
friendlyName: t("keyboard_action_names.open-new-tab"),
iconClass: "bx bx-plus",
defaultShortcuts: isElectron ? ["CommandOrControl+T"] : [],
description: t("keyboard_actions.open-new-tab"),
scope: "window"
},
{
actionName: "closeActiveTab",
friendlyName: t("keyboard_action_names.close-active-tab"),
iconClass: "bx bx-minus",
defaultShortcuts: isElectron ? ["CommandOrControl+W"] : [],
description: t("keyboard_actions.close-active-tab"),
scope: "window"
},
{
actionName: "reopenLastTab",
friendlyName: t("keyboard_action_names.reopen-last-tab"),
iconClass: "bx bx-undo",
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+T"] : [],
isElectronOnly: true,
description: t("keyboard_actions.reopen-last-tab"),
scope: "window"
},
{
actionName: "activateNextTab",
friendlyName: t("keyboard_action_names.activate-next-tab"),
iconClass: "bx bx-skip-next",
defaultShortcuts: isElectron ? ["CommandOrControl+Tab", "CommandOrControl+PageDown"] : [],
description: t("keyboard_actions.activate-next-tab"),
scope: "window"
},
{
actionName: "activatePreviousTab",
friendlyName: t("keyboard_action_names.activate-previous-tab"),
iconClass: "bx bx-skip-previous",
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+Tab", "CommandOrControl+PageUp"] : [],
description: t("keyboard_actions.activate-previous-tab"),
scope: "window"
},
{
actionName: "openNewWindow",
friendlyName: t("keyboard_action_names.open-new-window"),
iconClass: "bx bx-window-open",
defaultShortcuts: [],
description: t("keyboard_actions.open-new-window"),
scope: "window"
},
{
actionName: "toggleTray",
friendlyName: t("keyboard_action_names.toggle-system-tray-icon"),
iconClass: "bx bx-show",
defaultShortcuts: [],
isElectronOnly: true,
description: t("keyboard_actions.toggle-tray"),
scope: "window"
},
{
actionName: "toggleZenMode",
friendlyName: t("keyboard_action_names.toggle-zen-mode"),
iconClass: "bx bxs-yin-yang",
defaultShortcuts: ["F9"],
description: t("keyboard_actions.toggle-zen-mode"),
scope: "window"
},
{
actionName: "firstTab",
friendlyName: t("keyboard_action_names.switch-to-first-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+1"],
description: t("keyboard_actions.first-tab"),
scope: "window"
},
{
actionName: "secondTab",
friendlyName: t("keyboard_action_names.switch-to-second-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+2"],
description: t("keyboard_actions.second-tab"),
scope: "window"
},
{
actionName: "thirdTab",
friendlyName: t("keyboard_action_names.switch-to-third-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+3"],
description: t("keyboard_actions.third-tab"),
scope: "window"
},
{
actionName: "fourthTab",
friendlyName: t("keyboard_action_names.switch-to-fourth-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+4"],
description: t("keyboard_actions.fourth-tab"),
scope: "window"
},
{
actionName: "fifthTab",
friendlyName: t("keyboard_action_names.switch-to-fifth-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+5"],
description: t("keyboard_actions.fifth-tab"),
scope: "window"
},
{
actionName: "sixthTab",
friendlyName: t("keyboard_action_names.switch-to-sixth-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+6"],
description: t("keyboard_actions.sixth-tab"),
scope: "window"
},
{
actionName: "seventhTab",
friendlyName: t("keyboard_action_names.switch-to-seventh-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+7"],
description: t("keyboard_actions.seventh-tab"),
scope: "window"
},
{
actionName: "eigthTab",
friendlyName: t("keyboard_action_names.switch-to-eighth-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+8"],
description: t("keyboard_actions.eight-tab"),
scope: "window"
},
{
actionName: "ninthTab",
friendlyName: t("keyboard_action_names.switch-to-ninth-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+9"],
description: t("keyboard_actions.ninth-tab"),
scope: "window"
},
{
actionName: "lastTab",
friendlyName: t("keyboard_action_names.switch-to-last-tab"),
iconClass: "bx bx-rectangle",
defaultShortcuts: ["CommandOrControl+0"],
description: t("keyboard_actions.last-tab"),
scope: "window"
},
{
separator: t("keyboard_actions.dialogs")
},
{
friendlyName: t("keyboard_action_names.show-note-source"),
actionName: "showNoteSource",
iconClass: "bx bx-code",
defaultShortcuts: [],
description: t("keyboard_actions.show-note-source"),
scope: "window"
},
{
friendlyName: t("keyboard_action_names.show-options"),
actionName: "showOptions",
iconClass: "bx bx-cog",
defaultShortcuts: [],
description: t("keyboard_actions.show-options"),
scope: "window"
},
{
friendlyName: t("keyboard_action_names.show-revisions"),
actionName: "showRevisions",
iconClass: "bx bx-history",
defaultShortcuts: [],
description: t("keyboard_actions.show-revisions"),
scope: "window"
},
{
friendlyName: t("keyboard_action_names.show-recent-changes"),
actionName: "showRecentChanges",
iconClass: "bx bx-history",
defaultShortcuts: [],
description: t("keyboard_actions.show-recent-changes"),
scope: "window"
},
{
friendlyName: t("keyboard_action_names.show-sql-console"),
actionName: "showSQLConsole",
iconClass: "bx bx-data",
defaultShortcuts: ["Alt+O"],
description: t("keyboard_actions.show-sql-console"),
scope: "window"
},
{
friendlyName: t("keyboard_action_names.show-backend-log"),
actionName: "showBackendLog",
iconClass: "bx bx-detail",
defaultShortcuts: [],
description: t("keyboard_actions.show-backend-log"),
scope: "window"
},
{
friendlyName: t("keyboard_action_names.show-help"),
actionName: "showHelp",
iconClass: "bx bx-help-circle",
defaultShortcuts: ["F1"],
description: t("keyboard_actions.show-help"),
scope: "window"
},
{
friendlyName: t("keyboard_action_names.show-cheatsheet"),
actionName: "showCheatsheet",
iconClass: "bx bxs-keyboard",
defaultShortcuts: ["Shift+F1"],
description: t("keyboard_actions.show-cheatsheet"),
scope: "window"
},
{
separator: t("keyboard_actions.text-note-operations")
},
{
friendlyName: t("keyboard_action_names.add-link-to-text"),
actionName: "addLinkToText",
iconClass: "bx bx-link",
defaultShortcuts: ["CommandOrControl+L"],
description: t("keyboard_actions.add-link-to-text"),
scope: "text-detail"
},
{
friendlyName: t("keyboard_action_names.follow-link-under-cursor"),
actionName: "followLinkUnderCursor",
iconClass: "bx bx-link-external",
defaultShortcuts: ["CommandOrControl+Enter"],
description: t("keyboard_actions.follow-link-under-cursor"),
scope: "text-detail"
},
{
friendlyName: t("keyboard_action_names.insert-date-and-time-to-text"),
actionName: "insertDateTimeToText",
iconClass: "bx bx-calendar-event",
defaultShortcuts: ["Alt+T"],
description: t("keyboard_actions.insert-date-and-time-to-text"),
scope: "text-detail"
},
{
friendlyName: t("keyboard_action_names.paste-markdown-into-text"),
actionName: "pasteMarkdownIntoText",
iconClass: "bx bxl-markdown",
defaultShortcuts: [],
description: t("keyboard_actions.paste-markdown-into-text"),
scope: "text-detail"
},
{
friendlyName: t("keyboard_action_names.cut-into-note"),
actionName: "cutIntoNote",
iconClass: "bx bx-cut",
defaultShortcuts: [],
description: t("keyboard_actions.cut-into-note"),
scope: "text-detail"
},
{
friendlyName: t("keyboard_action_names.add-include-note-to-text"),
actionName: "addIncludeNoteToText",
iconClass: "bx bx-note",
defaultShortcuts: [],
description: t("keyboard_actions.add-include-note-to-text"),
scope: "text-detail"
},
{
friendlyName: t("keyboard_action_names.edit-read-only-note"),
actionName: "editReadOnlyNote",
iconClass: "bx bx-edit-alt",
defaultShortcuts: [],
description: t("keyboard_actions.edit-readonly-note"),
scope: "window"
},
{
separator: t("keyboard_actions.attributes-labels-and-relations")
},
{
friendlyName: t("keyboard_action_names.add-new-label"),
actionName: "addNewLabel",
iconClass: "bx bx-hash",
defaultShortcuts: ["Alt+L"],
description: t("keyboard_actions.add-new-label"),
scope: "window"
},
{
friendlyName: t("keyboard_action_names.add-new-relation"),
actionName: "addNewRelation",
iconClass: "bx bx-transfer",
defaultShortcuts: ["Alt+R"],
description: t("keyboard_actions.create-new-relation"),
scope: "window"
},
{
separator: t("keyboard_actions.ribbon-tabs")
},
{
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-classic-editor"),
actionName: "toggleRibbonTabClassicEditor",
iconClass: "bx bx-text",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-classic-editor-toolbar"),
scope: "window"
},
{
actionName: "toggleRibbonTabBasicProperties",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-basic-properties"),
iconClass: "bx bx-slider",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-basic-properties"),
scope: "window"
},
{
actionName: "toggleRibbonTabBookProperties",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-book-properties"),
iconClass: "bx bx-book",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-book-properties"),
scope: "window"
},
{
actionName: "toggleRibbonTabFileProperties",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-file-properties"),
iconClass: "bx bx-file",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-file-properties"),
scope: "window"
},
{
actionName: "toggleRibbonTabImageProperties",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-image-properties"),
iconClass: "bx bx-image",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-image-properties"),
scope: "window"
},
{
actionName: "toggleRibbonTabOwnedAttributes",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-owned-attributes"),
iconClass: "bx bx-list-check",
defaultShortcuts: ["Alt+A"],
description: t("keyboard_actions.toggle-owned-attributes"),
scope: "window"
},
{
actionName: "toggleRibbonTabInheritedAttributes",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-inherited-attributes"),
iconClass: "bx bx-list-plus",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-inherited-attributes"),
scope: "window"
},
// TODO: Remove or change since promoted attributes have been changed.
{
actionName: "toggleRibbonTabPromotedAttributes",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-promoted-attributes"),
iconClass: "bx bx-star",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-promoted-attributes"),
scope: "window"
},
{
actionName: "toggleRibbonTabNoteMap",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-note-map"),
iconClass: "bx bxs-network-chart",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-link-map"),
scope: "window"
},
{
actionName: "toggleRibbonTabNoteInfo",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-note-info"),
iconClass: "bx bx-info-circle",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-note-info"),
scope: "window"
},
{
actionName: "toggleRibbonTabNotePaths",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-note-paths"),
iconClass: "bx bx-collection",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-note-paths"),
scope: "window"
},
{
actionName: "toggleRibbonTabSimilarNotes",
friendlyName: t("keyboard_action_names.toggle-ribbon-tab-similar-notes"),
iconClass: "bx bx-bar-chart",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-similar-notes"),
scope: "window"
},
{
separator: t("keyboard_actions.other")
},
{
actionName: "toggleRightPane",
friendlyName: t("keyboard_action_names.toggle-right-pane"),
iconClass: "bx bx-dock-right",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-right-pane"),
scope: "window"
},
{
actionName: "printActiveNote",
friendlyName: t("keyboard_action_names.print-active-note"),
iconClass: "bx bx-printer",
defaultShortcuts: [],
description: t("keyboard_actions.print-active-note"),
scope: "window"
},
{
actionName: "exportAsPdf",
friendlyName: t("keyboard_action_names.export-active-note-as-pdf"),
iconClass: "bx bxs-file-pdf",
defaultShortcuts: [],
description: t("keyboard_actions.export-as-pdf"),
scope: "window"
},
{
actionName: "openNoteExternally",
friendlyName: t("keyboard_action_names.open-note-externally"),
iconClass: "bx bx-file-find",
defaultShortcuts: [],
description: t("keyboard_actions.open-note-externally"),
scope: "window"
},
{
actionName: "renderActiveNote",
friendlyName: t("keyboard_action_names.render-active-note"),
iconClass: "bx bx-refresh",
defaultShortcuts: [],
description: t("keyboard_actions.render-active-note"),
scope: "window"
},
{
actionName: "runActiveNote",
friendlyName: t("keyboard_action_names.run-active-note"),
iconClass: "bx bx-play",
defaultShortcuts: ["CommandOrControl+Enter"],
description: t("keyboard_actions.run-active-note"),
scope: "code-detail"
},
{
actionName: "toggleNoteHoisting",
friendlyName: t("keyboard_action_names.toggle-note-hoisting"),
iconClass: "bx bx-chevrons-up",
defaultShortcuts: ["Alt+H"],
description: t("keyboard_actions.toggle-note-hoisting"),
scope: "window"
},
{
actionName: "unhoist",
friendlyName: t("keyboard_action_names.unhoist-note"),
iconClass: "bx bx-door-open",
defaultShortcuts: ["Alt+U"],
description: t("keyboard_actions.unhoist"),
scope: "window"
},
{
actionName: "reloadFrontendApp",
friendlyName: t("keyboard_action_names.reload-frontend-app"),
iconClass: "bx bx-refresh",
defaultShortcuts: ["F5", "CommandOrControl+R"],
description: t("keyboard_actions.reload-frontend-app"),
scope: "window"
},
{
actionName: "openDevTools",
friendlyName: t("keyboard_action_names.open-developer-tools"),
iconClass: "bx bx-bug-alt",
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+I"] : [],
isElectronOnly: true,
description: t("keyboard_actions.open-dev-tools"),
scope: "window"
},
{
actionName: "findInText",
friendlyName: t("keyboard_action_names.find-in-text"),
iconClass: "bx bx-search",
defaultShortcuts: isElectron ? ["CommandOrControl+F"] : [],
description: t("keyboard_actions.find-in-text"),
scope: "window"
},
{
actionName: "toggleLeftPane",
friendlyName: t("keyboard_action_names.toggle-left-pane"),
iconClass: "bx bx-sidebar",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-left-note-tree-panel"),
scope: "window"
},
{
actionName: "toggleFullscreen",
friendlyName: t("keyboard_action_names.toggle-full-screen"),
iconClass: "bx bx-fullscreen",
defaultShortcuts: ["F11"],
description: t("keyboard_actions.toggle-full-screen"),
scope: "window"
},
{
actionName: "zoomOut",
friendlyName: t("keyboard_action_names.zoom-out"),
iconClass: "bx bx-zoom-out",
defaultShortcuts: isElectron ? ["CommandOrControl+-"] : [],
isElectronOnly: true,
description: t("keyboard_actions.zoom-out"),
scope: "window"
},
{
actionName: "zoomIn",
friendlyName: t("keyboard_action_names.zoom-in"),
iconClass: "bx bx-zoom-in",
description: t("keyboard_actions.zoom-in"),
defaultShortcuts: isElectron ? ["CommandOrControl+="] : [],
isElectronOnly: true,
scope: "window"
},
{
actionName: "zoomReset",
friendlyName: t("keyboard_action_names.reset-zoom-level"),
iconClass: "bx bx-search-alt",
description: t("keyboard_actions.reset-zoom-level"),
defaultShortcuts: isElectron ? ["CommandOrControl+0"] : [],
isElectronOnly: true,
scope: "window"
},
{
actionName: "copyWithoutFormatting",
friendlyName: t("keyboard_action_names.copy-without-formatting"),
iconClass: "bx bx-copy-alt",
defaultShortcuts: ["CommandOrControl+Alt+C"],
description: t("keyboard_actions.copy-without-formatting"),
scope: "text-detail"
},
{
actionName: "forceSaveRevision",
friendlyName: t("keyboard_action_names.force-save-revision"),
iconClass: "bx bx-save",
defaultShortcuts: [],
description: t("keyboard_actions.force-save-revision"),
scope: "window"
}
];
/*
* Apply macOS-specific tweaks.
*/
const platformModifier = isMac ? "Meta" : "Ctrl";
for (const action of DEFAULT_KEYBOARD_ACTIONS) {
if ("defaultShortcuts" in action && action.defaultShortcuts) {
action.defaultShortcuts = action.defaultShortcuts.map((shortcut) => shortcut.replace("CommandOrControl", platformModifier));
}
}
return DEFAULT_KEYBOARD_ACTIONS;
}
function getKeyboardActions() {
const actions: KeyboardShortcut[] = JSON.parse(JSON.stringify(getDefaultKeyboardActions()));
for (const action of actions) {
if ("effectiveShortcuts" in action && action.effectiveShortcuts) {
action.effectiveShortcuts = action.defaultShortcuts ? action.defaultShortcuts.slice() : [];
}
}
for (const option of optionService.getOptions()) {
if (option.name.startsWith("keyboardShortcuts")) {
let actionName = option.name.substring(17);
actionName = actionName.charAt(0).toLowerCase() + actionName.slice(1);
const action = actions.find((ea) => "actionName" in ea && ea.actionName === actionName) as ActionKeyboardShortcut;
if (action) {
try {
action.effectiveShortcuts = JSON.parse(option.value);
} catch (e) {
log.error(`Could not parse shortcuts for action ${actionName}`);
}
} else {
log.info(`Keyboard action ${actionName} found in database, but not in action definition.`);
}
}
}
return actions;
}
export default {
getDefaultKeyboardActions,
getKeyboardActions
};
import { keyboard_actions } from "@triliumnext/core";
export default keyboard_actions;

View File

@ -1,32 +1,28 @@
import options from '../options.js';
import eventService from '../events.js';
import type { AIService, ChatCompletionOptions, ChatResponse, Message } from './ai_interface.js';
import { AnthropicService } from './providers/anthropic_service.js';
import { ContextExtractor } from './context/index.js';
import agentTools from './context_extractors/index.js';
import contextService from './context/services/context_service.js';
import log from '../log.js';
import { OllamaService } from './providers/ollama_service.js';
import { OpenAIService } from './providers/openai_service.js';
// Import interfaces
import type {
ServiceProviders,
IAIServiceManager,
ProviderMetadata
} from './interfaces/ai_service_interfaces.js';
import type { NoteSearchResult } from './interfaces/context_interfaces.js';
import options from '../options.js';
import type { AIService, ChatCompletionOptions, ChatResponse, Message } from './ai_interface.js';
// Import new configuration system
import {
getSelectedProvider,
parseModelIdentifier,
isAIEnabled,
getDefaultModelForProvider,
clearConfigurationCache,
getDefaultModelForProvider,
getSelectedProvider,
isAIEnabled,
parseModelIdentifier,
validateConfiguration
} from './config/configuration_helpers.js';
import { ContextExtractor } from './context/index.js';
import contextService from './context/services/context_service.js';
import agentTools from './context_extractors/index.js';
// Import interfaces
import type {
IAIServiceManager,
ProviderMetadata,
ServiceProviders} from './interfaces/ai_service_interfaces.js';
import type { ProviderType } from './interfaces/configuration_interfaces.js';
import type { NoteSearchResult } from './interfaces/context_interfaces.js';
import { AnthropicService } from './providers/anthropic_service.js';
import { OllamaService } from './providers/ollama_service.js';
import { OpenAIService } from './providers/openai_service.js';
/**
* Interface representing relevant note context
@ -173,7 +169,7 @@ export class AIServiceManager implements IAIServiceManager {
/**
* Get list of available providers
*/
getAvailableProviders(): ServiceProviders[] {
getAvailableProviders(): ServiceProviders[] {
this.ensureInitialized();
const allProviders: ServiceProviders[] = ['openai', 'anthropic', 'ollama'];

View File

@ -1,11 +1,12 @@
import sanitizeHtml from 'sanitize-html';
import { sanitize } from '@triliumnext/core';
import log from '../../../log.js';
import type { Message } from '../../ai_interface.js';
import { CONTEXT_PROMPTS, FORMATTING_PROMPTS } from '../../constants/llm_prompt_constants.js';
import { LLM_CONSTANTS } from '../../constants/provider_constants.js';
import type { IContextFormatter, NoteSearchResult } from '../../interfaces/context_interfaces.js';
import modelCapabilitiesService from '../../model_capabilities_service.js';
import { calculateAvailableContextSize } from '../../interfaces/model_capabilities.js';
import type { Message } from '../../ai_interface.js';
import modelCapabilitiesService from '../../model_capabilities_service.js';
// Use constants from the centralized file
// const CONTEXT_WINDOW = {
@ -44,7 +45,7 @@ export class ContextFormatter implements IContextFormatter {
try {
// Get model name from provider
let modelName = providerId;
const modelName = providerId;
// Look up model capabilities
const modelCapabilities = await modelCapabilitiesService.getChatModelCapabilities(modelName);
@ -59,9 +60,9 @@ export class ContextFormatter implements IContextFormatter {
// Use the calculated size or fall back to constants
const maxTotalLength = availableContextSize || (
providerId === 'openai' ? LLM_CONSTANTS.CONTEXT_WINDOW.OPENAI :
providerId === 'anthropic' ? LLM_CONSTANTS.CONTEXT_WINDOW.ANTHROPIC :
providerId === 'ollama' ? LLM_CONSTANTS.CONTEXT_WINDOW.OLLAMA :
LLM_CONSTANTS.CONTEXT_WINDOW.DEFAULT
providerId === 'anthropic' ? LLM_CONSTANTS.CONTEXT_WINDOW.ANTHROPIC :
providerId === 'ollama' ? LLM_CONSTANTS.CONTEXT_WINDOW.OLLAMA :
LLM_CONSTANTS.CONTEXT_WINDOW.DEFAULT
);
// DEBUG: Log context window size
@ -239,11 +240,11 @@ export class ContextFormatter implements IContextFormatter {
// Handle line breaks
.replace(/<br\s*\/?>/gi, '\n');
// Then use sanitize-html to remove remaining HTML
const sanitized = sanitizeHtml(contentWithMarkdown, {
// Then sanitize to remove remaining HTML
const sanitized = sanitize.sanitizeHtmlCustom(contentWithMarkdown, {
allowedTags: [], // No tags allowed (strip all HTML)
allowedAttributes: {}, // No attributes allowed
textFilter: function(text) {
textFilter(text) {
return text
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
@ -264,7 +265,7 @@ export class ContextFormatter implements IContextFormatter {
if (type === 'code' || mime?.includes('application/')) {
// For code, limit to a reasonable size
if (content.length > 2000) {
return content.substring(0, 2000) + '...\n\n[Content truncated for brevity]';
return `${content.substring(0, 2000) }...\n\n[Content truncated for brevity]`;
}
return content;
}
@ -288,7 +289,7 @@ export class ContextFormatter implements IContextFormatter {
try {
// First remove any HTML
let plaintext = sanitizeHtml(content, {
let plaintext = sanitize.sanitizeHtmlCustom(content, {
allowedTags: [],
allowedAttributes: {},
textFilter: (text) => text

View File

@ -1,4 +1,5 @@
import sanitizeHtml from 'sanitize-html';
import { sanitize } from '@triliumnext/core';
import becca from '../../../becca/becca.js';
// Define interfaces for JSON structures
@ -98,13 +99,13 @@ export function formatNoteContent(content: string, type: string, mime: string, t
switch (type) {
case 'text':
// Remove HTML formatting for text notes
formattedContent += sanitizeHtml(content);
formattedContent += sanitize.sanitizeHtml(content);
break;
case 'code':
// For code, we'll handle this in code_handlers.ts
// Just use basic formatting here
formattedContent += '```\n' + content + '\n```';
formattedContent += `\`\`\`\n${ content }\n\`\`\``;
break;
case 'canvas':
@ -119,7 +120,7 @@ export function formatNoteContent(content: string, type: string, mime: string, t
.filter((element) => element.type === 'text' && element.text)
.map((element) => element.text as string);
formattedContent += 'Canvas content:\n' + texts.join('\n');
formattedContent += `Canvas content:\n${ texts.join('\n')}`;
} else {
formattedContent += '[Empty canvas]';
}
@ -154,7 +155,7 @@ export function formatNoteContent(content: string, type: string, mime: string, t
};
if (jsonContent.root) {
formattedContent += 'Mind map content:\n' + extractMindMapNodes(jsonContent.root).join('\n');
formattedContent += `Mind map content:\n${ extractMindMapNodes(jsonContent.root).join('\n')}`;
} else {
formattedContent += '[Empty mind map]';
}
@ -178,14 +179,14 @@ export function formatNoteContent(content: string, type: string, mime: string, t
let result = 'Relation map content:\n';
if (jsonContent.notes && Array.isArray(jsonContent.notes)) {
result += 'Notes: ' + jsonContent.notes
result += `Notes: ${ jsonContent.notes
.map((note) => note.title || note.name)
.filter(Boolean)
.join(', ') + '\n';
.join(', ') }\n`;
}
if (jsonContent.relations && Array.isArray(jsonContent.relations)) {
result += 'Relations: ' + jsonContent.relations
result += `Relations: ${ jsonContent.relations
.map((rel) => {
const sourceNote = jsonContent.notes?.find((n) => n.noteId === rel.sourceNoteId);
const targetNote = jsonContent.notes?.find((n) => n.noteId === rel.targetNoteId);
@ -193,7 +194,7 @@ export function formatNoteContent(content: string, type: string, mime: string, t
const target = targetNote ? (targetNote.title || targetNote.name) : 'unknown';
return `${source}${rel.name || ''}${target}`;
})
.join('; ');
.join('; ')}`;
}
formattedContent += result;
@ -219,7 +220,7 @@ export function formatNoteContent(content: string, type: string, mime: string, t
if (jsonContent.markers.length > 0) {
result += jsonContent.markers
.map((marker) => {
return `Location: ${marker.title || ''} (${marker.lat}, ${marker.lng})${marker.description ? ' - ' + marker.description : ''}`;
return `Location: ${marker.title || ''} (${marker.lat}, ${marker.lng})${marker.description ? ` - ${ marker.description}` : ''}`;
})
.join('\n');
} else {
@ -242,7 +243,7 @@ export function formatNoteContent(content: string, type: string, mime: string, t
case 'mermaid':
// Format mermaid diagrams as code blocks
formattedContent += '```mermaid\n' + content + '\n```';
formattedContent += `\`\`\`mermaid\n${ content }\n\`\`\``;
break;
case 'image':
@ -252,7 +253,7 @@ export function formatNoteContent(content: string, type: string, mime: string, t
default:
// For other notes, just use the content as is
formattedContent += sanitizeHtml(content);
formattedContent += sanitize.sanitizeHtml(content);
}
return formattedContent;
@ -265,7 +266,7 @@ export function sanitizeHtmlContent(html: string): string {
if (!html) return '';
// Use sanitizeHtml to remove all HTML tags
let content = sanitizeHtml(html, {
let content = sanitize.sanitizeHtmlCustom(html, {
allowedTags: [],
allowedAttributes: {},
textFilter: (text) => {

View File

@ -1,16 +1,16 @@
import sanitizeHtml from 'sanitize-html';
import { sanitize } from '@triliumnext/core';
import type { Message } from '../ai_interface.js';
import type { MessageFormatter } from '../interfaces/message_formatter.js';
import { DEFAULT_SYSTEM_PROMPT, PROVIDER_PROMPTS } from '../constants/llm_prompt_constants.js';
import {
HTML_ALLOWED_TAGS,
HTML_ALLOWED_ATTRIBUTES,
HTML_TRANSFORMS,
HTML_TO_MARKDOWN_PATTERNS,
HTML_ENTITY_REPLACEMENTS,
ENCODING_FIXES,
FORMATTER_LOGS
} from '../constants/formatter_constants.js';
FORMATTER_LOGS,
HTML_ALLOWED_ATTRIBUTES,
HTML_ALLOWED_TAGS,
HTML_ENTITY_REPLACEMENTS,
HTML_TO_MARKDOWN_PATTERNS,
HTML_TRANSFORMS} from '../constants/formatter_constants.js';
import { DEFAULT_SYSTEM_PROMPT, PROVIDER_PROMPTS } from '../constants/llm_prompt_constants.js';
import type { MessageFormatter } from '../interfaces/message_formatter.js';
/**
* Base formatter with common functionality for all providers
@ -49,7 +49,7 @@ export abstract class BaseMessageFormatter implements MessageFormatter {
const fixedContent = this.fixEncodingIssues(content);
// Convert HTML to markdown for better readability
const cleaned = sanitizeHtml(fixedContent, {
const cleaned = sanitize.sanitizeHtmlCustom(fixedContent, {
allowedTags: HTML_ALLOWED_TAGS.STANDARD,
allowedAttributes: HTML_ALLOWED_ATTRIBUTES.STANDARD,
transformTags: HTML_TRANSFORMS.STANDARD

View File

@ -1,15 +1,15 @@
import { sanitize } from '@triliumnext/core';
import log from '../../log.js';
import type { Message } from '../ai_interface.js';
import { BaseMessageFormatter } from './base_formatter.js';
import sanitizeHtml from 'sanitize-html';
import {
FORMATTER_LOGS,
HTML_ALLOWED_ATTRIBUTES,
HTML_ALLOWED_TAGS,
OLLAMA_CLEANING} from '../constants/formatter_constants.js';
import { PROVIDER_PROMPTS } from '../constants/llm_prompt_constants.js';
import { LLM_CONSTANTS } from '../constants/provider_constants.js';
import {
HTML_ALLOWED_TAGS,
HTML_ALLOWED_ATTRIBUTES,
OLLAMA_CLEANING,
FORMATTER_LOGS
} from '../constants/formatter_constants.js';
import log from '../../log.js';
import { BaseMessageFormatter } from './base_formatter.js';
/**
* Ollama-specific message formatter
@ -196,7 +196,7 @@ export class OllamaMessageFormatter extends BaseMessageFormatter {
// Then apply Ollama-specific aggressive cleaning
// Remove any remaining HTML using sanitizeHtml while keeping our markers
let plaintext = sanitizeHtml(sanitized, {
let plaintext = sanitize.sanitizeHtmlCustom(sanitized, {
allowedTags: HTML_ALLOWED_TAGS.NONE,
allowedAttributes: HTML_ALLOWED_ATTRIBUTES.NONE,
textFilter: (text) => text

View File

@ -1,16 +1,16 @@
import sanitizeHtml from 'sanitize-html';
import { sanitize } from '@triliumnext/core';
import log from '../../log.js';
import type { Message } from '../ai_interface.js';
import { BaseMessageFormatter } from './base_formatter.js';
import {
FORMATTER_LOGS,
HTML_ALLOWED_ATTRIBUTES,
HTML_ALLOWED_TAGS,
HTML_ENTITY_REPLACEMENTS,
HTML_TO_MARKDOWN_PATTERNS} from '../constants/formatter_constants.js';
import { PROVIDER_PROMPTS } from '../constants/llm_prompt_constants.js';
import { LLM_CONSTANTS } from '../constants/provider_constants.js';
import {
HTML_ALLOWED_TAGS,
HTML_ALLOWED_ATTRIBUTES,
HTML_TO_MARKDOWN_PATTERNS,
HTML_ENTITY_REPLACEMENTS,
FORMATTER_LOGS
} from '../constants/formatter_constants.js';
import log from '../../log.js';
import { BaseMessageFormatter } from './base_formatter.js';
/**
* OpenAI-specific message formatter
@ -54,18 +54,18 @@ export class OpenAIMessageFormatter extends BaseMessageFormatter {
// If we don't have explicit context but have a system prompt
else if (!hasSystemMessage && systemPrompt) {
let baseSystemPrompt = systemPrompt || PROVIDER_PROMPTS.COMMON.DEFAULT_ASSISTANT_INTRO;
// Check if this is a tool-using conversation
const hasPreviousToolCalls = messages.some(msg => msg.tool_calls && msg.tool_calls.length > 0);
const hasToolResults = messages.some(msg => msg.role === 'tool');
const isToolUsingConversation = useTools || hasPreviousToolCalls || hasToolResults;
// Add tool instructions for OpenAI when tools are being used
if (isToolUsingConversation && PROVIDER_PROMPTS.OPENAI.TOOL_INSTRUCTIONS) {
log.info('Adding tool instructions to system prompt for OpenAI');
baseSystemPrompt = `${baseSystemPrompt}\n\n${PROVIDER_PROMPTS.OPENAI.TOOL_INSTRUCTIONS}`;
}
formattedMessages.push({
role: 'system',
content: baseSystemPrompt
@ -111,7 +111,7 @@ export class OpenAIMessageFormatter extends BaseMessageFormatter {
try {
// Convert HTML to Markdown for better readability
const cleaned = sanitizeHtml(content, {
const cleaned = sanitize.sanitizeHtmlCustom(content, {
allowedTags: HTML_ALLOWED_TAGS.STANDARD,
allowedAttributes: HTML_ALLOWED_ATTRIBUTES.STANDARD
});

View File

@ -4,16 +4,16 @@
* This tool allows the LLM to read the content of a specific note.
*/
import type { Tool, ToolHandler } from './tool_interfaces.js';
import log from '../../log.js';
import becca from '../../../becca/becca.js';
import log from '../../log.js';
import type { Tool, ToolHandler } from './tool_interfaces.js';
// Define type for note response
interface NoteResponse {
noteId: string;
title: string;
type: string;
content: string | Buffer;
content: string | Uint8Array;
attributes?: Array<{
name: string;
value: string;

View File

@ -1,27 +0,0 @@
import type { NoteType } from "@triliumnext/commons";
export interface NoteParams {
/** optionally can force specific noteId */
noteId?: string;
branchId?: string;
parentNoteId: string;
templateNoteId?: string;
title: string;
content: string | Buffer;
/** text, code, file, image, search, book, relationMap, canvas, webView */
type: NoteType;
/** default value is derived from default mimes for type */
mime?: string;
/** default is false */
isProtected?: boolean;
/** default is false */
isExpanded?: boolean;
/** default is empty string */
prefix?: string;
/** default is the last existing notePosition in a parent + 10 */
notePosition?: number;
dateCreated?: string;
utcDateCreated?: string;
ignoreForbiddenParents?: boolean;
target?: "into";
}

View File

@ -1,34 +1,2 @@
const noteTypes = [
{ type: "text", defaultMime: "text/html" },
{ type: "code", defaultMime: "text/plain" },
{ type: "render", defaultMime: "" },
{ type: "file", defaultMime: "application/octet-stream" },
{ type: "image", defaultMime: "" },
{ type: "search", defaultMime: "" },
{ type: "relationMap", defaultMime: "application/json" },
{ type: "book", defaultMime: "" },
{ type: "noteMap", defaultMime: "" },
{ type: "mermaid", defaultMime: "text/vnd.mermaid" },
{ type: "canvas", defaultMime: "application/json" },
{ type: "webView", defaultMime: "" },
{ type: "launcher", defaultMime: "" },
{ type: "doc", defaultMime: "" },
{ type: "contentWidget", defaultMime: "" },
{ type: "mindMap", defaultMime: "application/json" },
{ type: "aiChat", defaultMime: "application/json" }
];
function getDefaultMimeForNoteType(typeName: string) {
const typeRec = noteTypes.find((nt) => nt.type === typeName);
if (!typeRec) {
throw new Error(`Cannot find note type '${typeName}'`);
}
return typeRec.defaultMime;
}
export default {
getNoteTypeNames: () => noteTypes.map((nt) => nt.type),
getDefaultMimeForNoteType
};
import { note_types } from "@triliumnext/core";
export default note_types;

File diff suppressed because it is too large Load Diff

View File

@ -1,145 +1,2 @@
/**
* @module
*
* Options are key-value pairs that are used to store information such as user preferences (for example
* the current theme, sync server information), but also information about the state of the application.
*
* Although options internally are represented as strings, their value can be interpreted as a number or
* boolean by calling the appropriate methods from this service (e.g. {@link #getOptionInt}).\
*
* Generally options are shared across multiple instances of the application via the sync mechanism,
* however it is possible to have options that are local to an instance. For example, the user can select
* a theme on a device and it will not affect other devices.
*/
import becca from "../becca/becca.js";
import BOption from "../becca/entities/boption.js";
import type { OptionRow } from "@triliumnext/commons";
import type { FilterOptionsByType, OptionDefinitions, OptionMap, OptionNames } from "@triliumnext/commons";
import sql from "./sql.js";
function getOptionOrNull(name: OptionNames): string | null {
let option;
if (becca.loaded) {
option = becca.getOption(name);
} else {
// e.g. in initial sync becca is not loaded because DB is not initialized
try {
option = sql.getRow<OptionRow>("SELECT * FROM options WHERE name = ?", [name]);
} catch (e: unknown) {
// DB is not initialized.
return null;
}
}
return option ? option.value : null;
}
function getOption(name: OptionNames) {
const val = getOptionOrNull(name);
if (val === null) {
throw new Error(`Option '${name}' doesn't exist`);
}
return val;
}
function getOptionInt(name: FilterOptionsByType<number>, defaultValue?: number): number {
const val = getOption(name);
const intVal = parseInt(val);
if (isNaN(intVal)) {
if (defaultValue === undefined) {
throw new Error(`Could not parse '${val}' into integer for option '${name}'`);
} else {
return defaultValue;
}
}
return intVal;
}
function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
const val = getOption(name);
if (typeof val !== "string" || !["true", "false"].includes(val)) {
throw new Error(`Could not parse '${val}' into boolean for option '${name}'`);
}
return val === "true";
}
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
const option = becca.getOption(name);
if (option) {
option.value = value as string;
option.save();
} else {
createOption(name, value, false);
}
// Clear current AI provider when AI-related options change
const aiOptions = [
'aiSelectedProvider', 'openaiApiKey', 'openaiBaseUrl', 'openaiDefaultModel',
'anthropicApiKey', 'anthropicBaseUrl', 'anthropicDefaultModel',
'ollamaBaseUrl', 'ollamaDefaultModel'
];
if (aiOptions.includes(name)) {
// Import dynamically to avoid circular dependencies
setImmediate(async () => {
try {
const aiServiceManager = (await import('./llm/ai_service_manager.js')).default;
aiServiceManager.getInstance().clearCurrentProvider();
console.log(`Cleared AI provider after ${name} option changed`);
} catch (error) {
console.log(`Could not clear AI provider: ${error}`);
}
});
}
}
/**
* Creates a new option in the database, with the given name, value and whether it should be synced.
*
* @param name the name of the option to be created.
* @param value the value of the option, as a string. It can then be interpreted as other types such as a number of boolean.
* @param isSynced `true` if the value should be synced across multiple instances (e.g. locale) or `false` if it should be local-only (e.g. theme).
*/
function createOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T], isSynced: boolean) {
new BOption({
name: name,
value: value as string,
isSynced: isSynced
}).save();
}
function getOptions() {
return Object.values(becca.options);
}
function getOptionMap() {
const map: Record<string, string> = {};
for (const option of Object.values(becca.options)) {
map[option.name] = option.value;
}
return map as OptionMap;
}
export default {
getOption,
getOptionInt,
getOptionBool,
setOption,
createOption,
getOptions,
getOptionMap,
getOptionOrNull
};
import { options } from "@triliumnext/core";
export default options;

View File

@ -1,280 +1,2 @@
import { type KeyboardShortcutWithRequiredActionName, type OptionMap, type OptionNames, SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
import appInfo from "./app_info.js";
import dateUtils from "./date_utils.js";
import keyboardActions from "./keyboard_actions.js";
import log from "./log.js";
import optionService from "./options.js";
import { isWindows, randomSecureToken } from "./utils.js";
function initDocumentOptions() {
optionService.createOption("documentId", randomSecureToken(16), false);
optionService.createOption("documentSecret", randomSecureToken(16), false);
}
/**
* Contains additional options to be initialized for a new database, containing the information entered by the user.
*/
interface NotSyncedOpts {
syncServerHost?: string;
syncProxy?: string;
}
/**
* Represents a correspondence between an option and its default value, to be initialized when the database is missing that particular option (after a migration from an older version, or when creating a new database).
*/
interface DefaultOption {
name: OptionNames;
/**
* The value to initialize the option with, if the option is not already present in the database.
*
* If a function is passed Gin instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized.
*/
value: string | ((options: OptionMap) => string);
isSynced: boolean;
}
/**
* Initializes the default options for new databases only.
*
* @param initialized `true` if the database has been fully initialized (i.e. a new database was created), or `false` if the database is created for sync.
* @param opts additional options to be initialized, for example the sync configuration.
*/
async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = {}) {
optionService.createOption(
"openNoteContexts",
JSON.stringify([
{
notePath: "root",
active: true
}
]),
false
);
optionService.createOption("lastDailyBackupDate", dateUtils.utcNowDateTime(), false);
optionService.createOption("lastWeeklyBackupDate", dateUtils.utcNowDateTime(), false);
optionService.createOption("lastMonthlyBackupDate", dateUtils.utcNowDateTime(), false);
optionService.createOption("dbVersion", appInfo.dbVersion.toString(), false);
optionService.createOption("initialized", initialized ? "true" : "false", false);
optionService.createOption("lastSyncedPull", "0", false);
optionService.createOption("lastSyncedPush", "0", false);
optionService.createOption("theme", "next", false);
optionService.createOption("textNoteEditorType", "ckeditor-classic", true);
optionService.createOption("syncServerHost", opts.syncServerHost || "", false);
optionService.createOption("syncServerTimeout", "120000", false);
optionService.createOption("syncProxy", opts.syncProxy || "", false);
}
/**
* Contains all the default options that must be initialized on new and existing databases (at startup). The value can also be determined based on other options, provided they have already been initialized.
*/
const defaultOptions: DefaultOption[] = [
{ name: "revisionSnapshotTimeInterval", value: "600", isSynced: true },
{ name: "revisionSnapshotTimeIntervalTimeScale", value: "60", isSynced: true }, // default to Minutes
{ name: "revisionSnapshotNumberLimit", value: "-1", isSynced: true },
{ name: "protectedSessionTimeout", value: "600", isSynced: true },
{ name: "protectedSessionTimeoutTimeScale", value: "60", isSynced: true },
{ name: "zoomFactor", value: isWindows ? "0.9" : "1.0", isSynced: false },
{ name: "overrideThemeFonts", value: "false", isSynced: false },
{ name: "mainFontFamily", value: "theme", isSynced: false },
{ name: "mainFontSize", value: "100", isSynced: false },
{ name: "treeFontFamily", value: "theme", isSynced: false },
{ name: "treeFontSize", value: "100", isSynced: false },
{ name: "detailFontFamily", value: "theme", isSynced: false },
{ name: "detailFontSize", value: "110", isSynced: false },
{ name: "monospaceFontFamily", value: "theme", isSynced: false },
{ name: "monospaceFontSize", value: "110", isSynced: false },
{ name: "spellCheckEnabled", value: "true", isSynced: false },
{ name: "spellCheckLanguageCode", value: "en-US", isSynced: false },
{ name: "imageMaxWidthHeight", value: "2000", isSynced: true },
{ name: "imageJpegQuality", value: "75", isSynced: true },
{ name: "autoFixConsistencyIssues", value: "true", isSynced: false },
{ name: "vimKeymapEnabled", value: "false", isSynced: false },
{ name: "codeLineWrapEnabled", value: "true", isSynced: false },
{
name: "codeNotesMimeTypes",
value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-elixir","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml","text/x-sh","application/typescript"]',
isSynced: true
},
{ name: "leftPaneWidth", value: "25", isSynced: false },
{ name: "leftPaneVisible", value: "true", isSynced: false },
{ name: "rightPaneWidth", value: "25", isSynced: false },
{ name: "rightPaneVisible", value: "true", isSynced: false },
{ name: "rightPaneCollapsedItems", value: "[]", isSynced: false },
{ name: "nativeTitleBarVisible", value: "false", isSynced: false },
{ name: "eraseEntitiesAfterTimeInSeconds", value: "604800", isSynced: true }, // default is 7 days
{ name: "eraseEntitiesAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day
{ name: "hideArchivedNotes_main", value: "false", isSynced: false },
{ name: "debugModeEnabled", value: "false", isSynced: false },
{ name: "headingStyle", value: "underline", isSynced: true },
{ name: "autoCollapseNoteTree", value: "true", isSynced: true },
{ name: "autoReadonlySizeText", value: "32000", isSynced: false },
{ name: "autoReadonlySizeCode", value: "64000", isSynced: false },
{ name: "dailyBackupEnabled", value: "true", isSynced: false },
{ name: "weeklyBackupEnabled", value: "true", isSynced: false },
{ name: "monthlyBackupEnabled", value: "true", isSynced: false },
{ name: "maxContentWidth", value: "1200", isSynced: false },
{ name: "centerContent", value: "false", isSynced: false },
{ name: "compressImages", value: "true", isSynced: true },
{ name: "downloadImagesAutomatically", value: "true", isSynced: true },
{ name: "minTocHeadings", value: "5", isSynced: true },
{ name: "highlightsList", value: '["underline","color","bgColor"]', isSynced: true },
{ name: "checkForUpdates", value: "true", isSynced: true },
{ name: "disableTray", value: "false", isSynced: false },
{ name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days
{ name: "eraseUnusedAttachmentsAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day
{ name: "logRetentionDays", value: "90", isSynced: false }, // default 90 days
{ name: "customSearchEngineName", value: "DuckDuckGo", isSynced: true },
{ name: "customSearchEngineUrl", value: "https://duckduckgo.com/?q={keyword}", isSynced: true },
{ name: "editedNotesOpenInRibbon", value: "true", isSynced: true },
{ name: "mfaEnabled", value: "false", isSynced: false },
{ name: "mfaMethod", value: "totp", isSynced: false },
{ name: "encryptedRecoveryCodes", value: "false", isSynced: false },
{ name: "userSubjectIdentifierSaved", value: "false", isSynced: false },
// Appearance
{ name: "splitEditorOrientation", value: "horizontal", isSynced: true },
{
name: "codeNoteTheme",
value: (optionsMap) => {
switch (optionsMap.theme) {
case "light":
case "next-light":
return "default:vs-code-light";
case "dark":
case "next-dark":
default:
return "default:vs-code-dark";
}
},
isSynced: false
},
{ name: "motionEnabled", value: "true", isSynced: false },
{ name: "shadowsEnabled", value: "true", isSynced: false },
{ name: "backdropEffectsEnabled", value: "true", isSynced: false },
{ name: "smoothScrollEnabled", value: "true", isSynced: false },
{ name: "newLayout", value: "true", isSynced: true },
// Internationalization
{ name: "locale", value: "en", isSynced: true },
{ name: "formattingLocale", value: "", isSynced: true }, // no value means auto-detect
{ name: "firstDayOfWeek", value: "1", isSynced: true },
{ name: "firstWeekOfYear", value: "0", isSynced: true },
{ name: "minDaysInFirstWeek", value: "4", isSynced: true },
{ name: "languages", value: "[]", isSynced: true },
// Code block configuration
{
name: "codeBlockTheme",
value: (optionsMap) => {
if (optionsMap.theme === "light") {
return "default:stackoverflow-light";
}
return "default:stackoverflow-dark";
},
isSynced: false
},
{ name: "codeBlockWordWrap", value: "false", isSynced: true },
// Text note configuration
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true },
{ name: "textNoteEditorMultilineToolbar", value: "false", isSynced: true },
{ name: "textNoteEmojiCompletionEnabled", value: "true", isSynced: true },
{ name: "textNoteCompletionEnabled", value: "true", isSynced: true },
{ name: "textNoteSlashCommandsEnabled", value: "true", isSynced: true },
// HTML import configuration
{ name: "layoutOrientation", value: "vertical", isSynced: false },
{ name: "backgroundEffects", value: "true", isSynced: false },
{
name: "allowedHtmlTags",
value: JSON.stringify(SANITIZER_DEFAULT_ALLOWED_TAGS),
isSynced: true
},
// Share settings
{ name: "redirectBareDomain", value: "false", isSynced: true },
{ name: "showLoginInShareTheme", value: "false", isSynced: true },
// AI Options
{ name: "aiEnabled", value: "false", isSynced: true },
{ name: "openaiApiKey", value: "", isSynced: false },
{ name: "openaiDefaultModel", value: "", isSynced: true },
{ name: "openaiBaseUrl", value: "https://api.openai.com/v1", isSynced: true },
{ name: "anthropicApiKey", value: "", isSynced: false },
{ name: "anthropicDefaultModel", value: "", isSynced: true },
{ name: "voyageApiKey", value: "", isSynced: false },
{ name: "anthropicBaseUrl", value: "https://api.anthropic.com/v1", isSynced: true },
{ name: "ollamaEnabled", value: "false", isSynced: true },
{ name: "ollamaDefaultModel", value: "", isSynced: true },
{ name: "ollamaBaseUrl", value: "http://localhost:11434", isSynced: true },
{ name: "aiTemperature", value: "0.7", isSynced: true },
{ name: "aiSystemPrompt", value: "", isSynced: true },
{ name: "aiSelectedProvider", value: "openai", isSynced: true },
{
name: "seenCallToActions",
value: JSON.stringify([
"new_layout", "background_effects", "next_theme"
]),
isSynced: true
},
{ name: "experimentalFeatures", value: "[]", isSynced: true }
];
/**
* Initializes the options, by checking which options from {@link #allDefaultOptions()} are missing and registering them. It will also check some environment variables such as safe mode, to make any necessary adjustments.
*
* This method is called regardless of whether a new database is created, or an existing database is used.
*/
function initStartupOptions() {
const optionsMap = optionService.getOptionMap();
const allDefaultOptions = defaultOptions.concat(getKeyboardDefaultOptions());
for (const { name, value, isSynced } of allDefaultOptions) {
if (!(name in optionsMap)) {
let resolvedValue;
if (typeof value === "function") {
resolvedValue = value(optionsMap);
} else {
resolvedValue = value;
}
optionService.createOption(name, resolvedValue, isSynced);
log.info(`Created option "${name}" with default value "${resolvedValue}"`);
}
}
if (process.env.TRILIUM_START_NOTE_ID || process.env.TRILIUM_SAFE_MODE) {
optionService.setOption(
"openNoteContexts",
JSON.stringify([
{
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
active: true
}
])
);
}
}
function getKeyboardDefaultOptions() {
return (keyboardActions.getDefaultKeyboardActions().filter((ka) => "actionName" in ka) as KeyboardShortcutWithRequiredActionName[]).map((ka) => ({
name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`,
value: JSON.stringify(ka.defaultShortcuts),
isSynced: false
})) as DefaultOption[];
}
export default {
initDocumentOptions,
initNotSyncedOptions,
initStartupOptions
};
import { options_init } from "@triliumnext/core";
export default options_init;

View File

@ -1,8 +0,0 @@
export interface DefinitionObject {
isPromoted?: boolean;
labelType?: string;
multiplicity?: string;
numberPrecision?: number;
promotedAlias?: string;
inverseRelation?: string;
}

View File

@ -1,49 +1,2 @@
"use strict";
import log from "./log.js";
import sql from "./sql.js";
import protectedSessionService from "./protected_session.js";
import dateUtils from "./date_utils.js";
import type BNote from "../becca/entities/bnote.js";
function protectRevisions(note: BNote) {
if (!protectedSessionService.isProtectedSessionAvailable()) {
throw new Error(`Cannot (un)protect revisions of note '${note.noteId}' without active protected session`);
}
for (const revision of note.getRevisions()) {
if (note.isProtected !== revision.isProtected) {
try {
const content = revision.getContent();
revision.isProtected = !!note.isProtected;
// this will force de/encryption
revision.setContent(content, { forceSave: true });
} catch (e) {
log.error(`Could not un/protect note revision '${revision.revisionId}'`);
throw e;
}
}
for (const attachment of revision.getAttachments()) {
if (note.isProtected !== attachment.isProtected) {
try {
const content = attachment.getContent();
attachment.isProtected = note.isProtected;
attachment.setContent(content, { forceSave: true });
} catch (e) {
log.error(`Could not un/protect attachment '${attachment.attachmentId}'`);
throw e;
}
}
}
}
}
export default {
protectRevisions
};
import { revisions } from "@triliumnext/core";
export default revisions;

View File

@ -1,6 +0,0 @@
export default function sanitizeAttributeName(origName: string) {
const fixedName = origName === "" ? "unnamed" : origName.replace(/[^\p{L}\p{N}_:]/gu, "_");
// any not allowed character should be replaced with underscore
return fixedName;
}

View File

@ -1,3 +1,4 @@
import { binary_utils } from "@triliumnext/core";
import { transform } from "sucrase";
import becca from "../becca/becca.js";
@ -217,8 +218,8 @@ return module.exports;
return bundle;
}
export function buildJsx(contentRaw: string | Buffer) {
const content = Buffer.isBuffer(contentRaw) ? contentRaw.toString("utf-8") : contentRaw;
export function buildJsx(contentRaw: string | Uint8Array) {
const content = binary_utils.unwrapStringOrBuffer(contentRaw);
const output = transform(content, {
transforms: ["jsx", "imports"],
jsxPragma: "api.preact.h",

View File

@ -1,24 +1,23 @@
"use strict";
import type { NoteRow } from "@triliumnext/commons";
import type SearchContext from "../search_context.js";
import Expression from "./expression.js";
import NoteSet from "../note_set.js";
import log from "../../log.js";
import becca from "../../../becca/becca.js";
import protectedSessionService from "../../protected_session.js";
import striptags from "striptags";
import { normalize } from "../../utils.js";
import becca from "../../../becca/becca.js";
import log from "../../log.js";
import protectedSessionService from "../../protected_session.js";
import sql from "../../sql.js";
import {
normalizeSearchText,
calculateOptimizedEditDistance,
validateFuzzySearchTokens,
validateAndPreprocessContent,
import { normalize } from "../../utils.js";
import NoteSet from "../note_set.js";
import type SearchContext from "../search_context.js";
import {
calculateOptimizedEditDistance,
FUZZY_SEARCH_CONFIG,
fuzzyMatchWord,
FUZZY_SEARCH_CONFIG
} from "../utils/text_utils.js";
normalizeSearchText,
validateAndPreprocessContent,
validateFuzzySearchTokens} from "../utils/text_utils.js";
import Expression from "./expression.js";
const ALLOWED_OPERATORS = new Set(["=", "!=", "*=*", "*=", "=*", "%=", "~=", "~*"]);
@ -295,7 +294,7 @@ class NoteContentFulltextExp extends Expression {
return content;
}
preprocessContent(content: string | Buffer, type: string, mime: string) {
preprocessContent(content: string | Uint8Array, type: string, mime: string) {
content = normalize(content.toString());
if (type === "text" && mime === "text/html") {
@ -338,16 +337,16 @@ class NoteContentFulltextExp extends Expression {
private tokenMatchesContent(token: string, content: string, noteId: string): boolean {
const normalizedToken = normalizeSearchText(token);
const normalizedContent = normalizeSearchText(content);
if (normalizedContent.includes(normalizedToken)) {
return true;
}
// Check flat text for default fulltext search
if (!this.flatText || !becca.notes[noteId].getFlatText().includes(token)) {
return false;
}
return true;
}
@ -358,15 +357,15 @@ class NoteContentFulltextExp extends Expression {
try {
const normalizedContent = normalizeSearchText(content);
const flatText = this.flatText ? normalizeSearchText(becca.notes[noteId].getFlatText()) : "";
// For phrase matching, check if tokens appear within reasonable proximity
if (this.tokens.length > 1) {
return this.matchesPhrase(normalizedContent, flatText);
}
// Single token fuzzy matching
const token = normalizeSearchText(this.tokens[0]);
return this.fuzzyMatchToken(token, normalizedContent) ||
return this.fuzzyMatchToken(token, normalizedContent) ||
(this.flatText && this.fuzzyMatchToken(token, flatText));
} catch (error) {
log.error(`Error in fuzzy matching for note ${noteId}: ${error}`);
@ -379,45 +378,45 @@ class NoteContentFulltextExp extends Expression {
*/
private matchesPhrase(content: string, flatText: string): boolean {
const searchText = this.flatText ? `${content} ${flatText}` : content;
// Apply content size limits for phrase matching
const limitedText = validateAndPreprocessContent(searchText);
if (!limitedText) {
return false;
}
const words = limitedText.toLowerCase().split(/\s+/);
// Only skip phrase matching for truly extreme word counts that could crash the system
if (words.length > FUZZY_SEARCH_CONFIG.ABSOLUTE_MAX_WORD_COUNT) {
console.error(`Phrase matching skipped due to extreme word count that could cause system instability: ${words.length} words`);
return false;
}
// Warn about large word counts but still attempt matching
if (words.length > FUZZY_SEARCH_CONFIG.PERFORMANCE_WARNING_WORDS) {
console.info(`Large word count for phrase matching: ${words.length} words - may take longer but will attempt full matching`);
}
// Find positions of each token
const tokenPositions: number[][] = this.tokens.map(token => {
const normalizedToken = normalizeSearchText(token);
const positions: number[] = [];
words.forEach((word, index) => {
if (this.fuzzyMatchSingle(normalizedToken, word)) {
positions.push(index);
}
});
return positions;
});
// Check if we found all tokens
if (tokenPositions.some(positions => positions.length === 0)) {
return false;
}
// Check for phrase proximity using configurable distance
return this.hasProximityMatch(tokenPositions, FUZZY_SEARCH_CONFIG.MAX_PHRASE_PROXIMITY);
}
@ -431,18 +430,18 @@ class NoteContentFulltextExp extends Expression {
const [pos1, pos2] = tokenPositions;
return pos1.some(p1 => pos2.some(p2 => Math.abs(p1 - p2) <= maxDistance));
}
// For more tokens, check if we can find a sequence where all tokens are within range
const findSequence = (remaining: number[][], currentPos: number): boolean => {
if (remaining.length === 0) return true;
const [nextPositions, ...rest] = remaining;
return nextPositions.some(pos =>
Math.abs(pos - currentPos) <= maxDistance &&
return nextPositions.some(pos =>
Math.abs(pos - currentPos) <= maxDistance &&
findSequence(rest, pos)
);
};
const [firstPositions, ...rest] = tokenPositions;
return firstPositions.some(startPos => findSequence(rest, startPos));
}
@ -455,12 +454,12 @@ class NoteContentFulltextExp extends Expression {
// For short tokens, require exact match to avoid too many false positives
return content.includes(token);
}
const words = content.split(/\s+/);
// Only limit word processing for truly extreme cases to prevent system instability
const limitedWords = words.slice(0, FUZZY_SEARCH_CONFIG.ABSOLUTE_MAX_WORD_COUNT);
return limitedWords.some(word => this.fuzzyMatchSingle(token, word));
}

View File

@ -1,14 +1,12 @@
"use strict";
import { becca_service } from "@triliumnext/core";
import type BNote from "../../../becca/entities/bnote.js";
import type SearchContext from "../search_context.js";
import Expression from "./expression.js";
import NoteSet from "../note_set.js";
import becca from "../../../becca/becca.js";
import type BNote from "../../../becca/entities/bnote.js";
import { normalize } from "../../utils.js";
import { normalizeSearchText, fuzzyMatchWord, fuzzyMatchWordWithResult } from "../utils/text_utils.js";
import beccaService from "../../../becca/becca_service.js";
import NoteSet from "../note_set.js";
import type SearchContext from "../search_context.js";
import { fuzzyMatchWord, fuzzyMatchWordWithResult,normalizeSearchText } from "../utils/text_utils.js";
import Expression from "./expression.js";
class NoteFlatTextExp extends Expression {
tokens: string[];
@ -60,7 +58,7 @@ class NoteFlatTextExp extends Expression {
// Add defensive checks for undefined properties
const typeMatches = note.type && note.type.includes(token);
const mimeMatches = note.mime && note.mime.includes(token);
if (typeMatches || mimeMatches) {
foundAttrTokens.push(token);
}
@ -78,7 +76,7 @@ class NoteFlatTextExp extends Expression {
}
for (const parentNote of note.parents) {
const title = normalizeSearchText(beccaService.getNoteTitle(note.noteId, parentNote.noteId));
const title = normalizeSearchText(becca_service.getNoteTitle(note.noteId, parentNote.noteId));
const foundTokens: string[] = foundAttrTokens.slice();
for (const token of remainingTokens) {
@ -112,7 +110,7 @@ class NoteFlatTextExp extends Expression {
// Add defensive checks for undefined properties
const typeMatches = note.type && note.type.includes(token);
const mimeMatches = note.mime && note.mime.includes(token);
if (typeMatches || mimeMatches) {
foundAttrTokens.push(token);
}
@ -125,7 +123,7 @@ class NoteFlatTextExp extends Expression {
}
for (const parentNote of note.parents) {
const title = normalizeSearchText(beccaService.getNoteTitle(note.noteId, parentNote.noteId));
const title = normalizeSearchText(becca_service.getNoteTitle(note.noteId, parentNote.noteId));
const foundTokens = foundAttrTokens.slice();
for (const token of this.tokens) {
@ -190,7 +188,7 @@ class NoteFlatTextExp extends Expression {
if (text.includes(token)) {
return true;
}
// Fuzzy fallback only if enabled and for tokens >= 4 characters
if (searchContext?.enableFuzzyMatching && token.length >= 4) {
const matchedWord = fuzzyMatchWordWithResult(token, text);
@ -202,7 +200,7 @@ class NoteFlatTextExp extends Expression {
return true;
}
}
return false;
}
}

View File

@ -1,67 +1,2 @@
"use strict";
import type BNote from "../../becca/entities/bnote.js";
class NoteSet {
private noteIdSet: Set<string>;
notes: BNote[];
sorted: boolean;
constructor(notes: BNote[] = []) {
this.notes = notes;
this.noteIdSet = new Set(notes.map((note) => note.noteId));
this.sorted = false;
}
add(note: BNote) {
if (!this.hasNote(note)) {
this.notes.push(note);
this.noteIdSet.add(note.noteId);
}
}
addAll(notes: BNote[]) {
for (const note of notes) {
this.add(note);
}
}
hasNote(note: BNote) {
return this.hasNoteId(note.noteId);
}
hasNoteId(noteId: string) {
return this.noteIdSet.has(noteId);
}
mergeIn(anotherNoteSet: NoteSet) {
this.addAll(anotherNoteSet.notes);
}
minus(anotherNoteSet: NoteSet) {
const newNoteSet = new NoteSet();
for (const note of this.notes) {
if (!anotherNoteSet.hasNoteId(note.noteId)) {
newNoteSet.add(note);
}
}
return newNoteSet;
}
intersection(anotherNoteSet: NoteSet) {
const newNoteSet = new NoteSet();
for (const note of this.notes) {
if (anotherNoteSet.hasNote(note)) {
newNoteSet.add(note);
}
}
return newNoteSet;
}
}
import { NoteSet } from "@triliumnext/core";
export default NoteSet;

View File

@ -1,12 +1,10 @@
"use strict";
import { becca_service } from "@triliumnext/core";
import beccaService from "../../becca/becca_service.js";
import becca from "../../becca/becca.js";
import {
normalizeSearchText,
calculateOptimizedEditDistance,
FUZZY_SEARCH_CONFIG
} from "./utils/text_utils.js";
import {
calculateOptimizedEditDistance,
FUZZY_SEARCH_CONFIG,
normalizeSearchText} from "./utils/text_utils.js";
// Scoring constants for better maintainability
const SCORE_WEIGHTS = {
@ -41,7 +39,7 @@ class SearchResult {
constructor(notePathArray: string[]) {
this.notePathArray = notePathArray;
this.notePathTitle = beccaService.getNoteTitleForPath(notePathArray);
this.notePathTitle = becca_service.getNoteTitleForPath(notePathArray);
this.score = 0;
this.fuzzyScore = 0;
}
@ -98,7 +96,7 @@ class SearchResult {
for (const chunk of chunks) {
for (const token of tokens) {
const normalizedToken = normalizeSearchText(token.toLowerCase());
if (chunk === normalizedToken) {
tokenScore += SCORE_WEIGHTS.TOKEN_EXACT_MATCH * token.length * factor;
} else if (chunk.startsWith(normalizedToken)) {
@ -108,10 +106,10 @@ class SearchResult {
} else {
// Try fuzzy matching for individual tokens with caps applied
const editDistance = calculateOptimizedEditDistance(chunk, normalizedToken, FUZZY_SEARCH_CONFIG.MAX_EDIT_DISTANCE);
if (editDistance <= FUZZY_SEARCH_CONFIG.MAX_EDIT_DISTANCE &&
if (editDistance <= FUZZY_SEARCH_CONFIG.MAX_EDIT_DISTANCE &&
normalizedToken.length >= FUZZY_SEARCH_CONFIG.MIN_FUZZY_TOKEN_LENGTH &&
this.fuzzyScore < SCORE_WEIGHTS.MAX_TOTAL_FUZZY_SCORE) {
const fuzzyWeight = SCORE_WEIGHTS.TOKEN_FUZZY_MATCH * (1 - editDistance / FUZZY_SEARCH_CONFIG.MAX_EDIT_DISTANCE);
// Apply caps: limit token length multiplier and per-token contribution
const cappedTokenLength = Math.min(token.length, SCORE_WEIGHTS.MAX_FUZZY_TOKEN_LENGTH_MULTIPLIER);
@ -119,7 +117,7 @@ class SearchResult {
fuzzyWeight * cappedTokenLength * factor,
SCORE_WEIGHTS.MAX_FUZZY_SCORE_PER_TOKEN
);
tokenScore += fuzzyTokenScore;
this.fuzzyScore += fuzzyTokenScore;
}
@ -134,8 +132,8 @@ class SearchResult {
* Checks if the query matches as a complete word in the text
*/
private isWordMatch(text: string, query: string): boolean {
return text.includes(` ${query} `) ||
text.startsWith(`${query} `) ||
return text.includes(` ${query} `) ||
text.startsWith(`${query} `) ||
text.endsWith(` ${query}`);
}
@ -147,21 +145,21 @@ class SearchResult {
if (this.fuzzyScore >= SCORE_WEIGHTS.MAX_TOTAL_FUZZY_SCORE) {
return 0;
}
const editDistance = calculateOptimizedEditDistance(title, query, FUZZY_SEARCH_CONFIG.MAX_EDIT_DISTANCE);
const maxLen = Math.max(title.length, query.length);
// Only apply fuzzy matching if the query is reasonably long and edit distance is small
if (query.length >= FUZZY_SEARCH_CONFIG.MIN_FUZZY_TOKEN_LENGTH &&
editDistance <= FUZZY_SEARCH_CONFIG.MAX_EDIT_DISTANCE &&
if (query.length >= FUZZY_SEARCH_CONFIG.MIN_FUZZY_TOKEN_LENGTH &&
editDistance <= FUZZY_SEARCH_CONFIG.MAX_EDIT_DISTANCE &&
editDistance / maxLen <= 0.3) {
const similarity = 1 - (editDistance / maxLen);
const baseFuzzyScore = SCORE_WEIGHTS.TITLE_WORD_MATCH * similarity * 0.7; // Reduced weight for fuzzy matches
// Apply cap to ensure fuzzy title matches don't exceed reasonable bounds
return Math.min(baseFuzzyScore, SCORE_WEIGHTS.MAX_TOTAL_FUZZY_SCORE * 0.3);
}
return 0;
}

View File

@ -1,24 +1,22 @@
"use strict";
import { becca_service } from "@triliumnext/core";
import normalizeString from "normalize-strings";
import lex from "./lex.js";
import handleParens from "./handle_parens.js";
import parse from "./parse.js";
import SearchResult from "../search_result.js";
import SearchContext from "../search_context.js";
import becca from "../../../becca/becca.js";
import beccaService from "../../../becca/becca_service.js";
import { normalize, escapeHtml, escapeRegExp } from "../../utils.js";
import log from "../../log.js";
import hoistedNoteService from "../../hoisted_note.js";
import type BNote from "../../../becca/entities/bnote.js";
import type BAttribute from "../../../becca/entities/battribute.js";
import type { SearchParams, TokenStructure } from "./types.js";
import type Expression from "../expressions/expression.js";
import sql from "../../sql.js";
import scriptService from "../../script.js";
import striptags from "striptags";
import becca from "../../../becca/becca.js";
import type BNote from "../../../becca/entities/bnote.js";
import hoistedNoteService from "../../hoisted_note.js";
import log from "../../log.js";
import protectedSessionService from "../../protected_session.js";
import scriptService from "../../script.js";
import sql from "../../sql.js";
import { escapeHtml, escapeRegExp } from "../../utils.js";
import type Expression from "../expressions/expression.js";
import SearchContext from "../search_context.js";
import SearchResult from "../search_result.js";
import handleParens from "./handle_parens.js";
import lex from "./lex.js";
import parse from "./parse.js";
import type { SearchParams, TokenStructure } from "./types.js";
export interface SearchNoteResult {
searchResultNoteIds: string[];
@ -67,7 +65,7 @@ function searchFromNote(note: BNote): SearchNoteResult {
return {
searchResultNoteIds: searchResultNoteIds.filter((resultNoteId) => !["root", note.noteId].includes(resultNoteId)),
highlightedTokens,
error: error
error
};
}
@ -252,21 +250,21 @@ function findResultsWithExpression(expression: Expression, searchContext: Search
// Phase 1: Try exact matches first (without fuzzy matching)
const exactResults = performSearch(expression, searchContext, false);
// Check if we have sufficient high-quality results
const minResultThreshold = 5;
const minScoreForQuality = 10; // Minimum score to consider a result "high quality"
const highQualityResults = exactResults.filter(result => result.score >= minScoreForQuality);
// If we have enough high-quality exact matches, return them
if (highQualityResults.length >= minResultThreshold) {
return exactResults;
}
// Phase 2: Add fuzzy matching as fallback when exact matches are insufficient
const fuzzyResults = performSearch(expression, searchContext, true);
// Merge results, ensuring exact matches always rank higher than fuzzy matches
return mergeExactAndFuzzyResults(exactResults, fuzzyResults);
}
@ -326,10 +324,10 @@ function performSearch(expression: Expression, searchContext: SearchContext, ena
function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: SearchResult[]): SearchResult[] {
// Create a map of exact result note IDs for deduplication
const exactNoteIds = new Set(exactResults.map(result => result.noteId));
// Add fuzzy results that aren't already in exact results
const additionalFuzzyResults = fuzzyResults.filter(result => !exactNoteIds.has(result.noteId));
// Sort exact results by score (best exact matches first)
exactResults.sort((a, b) => {
if (a.score > b.score) {
@ -345,7 +343,7 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S
return a.notePathArray.length < b.notePathArray.length ? -1 : 1;
});
// Sort fuzzy results by score (best fuzzy matches first)
additionalFuzzyResults.sort((a, b) => {
if (a.score > b.score) {
@ -361,7 +359,7 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S
return a.notePathArray.length < b.notePathArray.length ? -1 : 1;
});
// CRITICAL: Always put exact matches before fuzzy matches, regardless of scores
return [...exactResults, ...additionalFuzzyResults];
}
@ -417,10 +415,10 @@ function findResultsWithQuery(query: string, searchContext: SearchContext): Sear
}
// If the query starts with '#', it's a pure expression query.
// Don't use progressive search for these as they may have complex
// Don't use progressive search for these as they may have complex
// ordering or other logic that shouldn't be interfered with.
const isPureExpressionQuery = query.trim().startsWith('#');
if (isPureExpressionQuery) {
// For pure expression queries, use standard search without progressive phases
return performSearch(expression, searchContext, searchContext.enableFuzzyMatching);
@ -448,7 +446,7 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
try {
let content = note.getContent();
if (!content || typeof content !== "string") {
return "";
}
@ -489,7 +487,7 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
for (const token of searchTokens) {
const normalizedToken = normalizeString(token.toLowerCase());
const matchIndex = normalizedContent.indexOf(normalizedToken);
if (matchIndex !== -1) {
// Center the snippet around the match
snippetStart = Math.max(0, matchIndex - maxLength / 2);
@ -528,7 +526,7 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
snippet = lines.slice(0, 4).join('\n');
}
// Add ellipsis if we truncated lines
snippet = snippet + "...";
snippet = `${snippet }...`;
} else if (lines.length > 1) {
// For multi-line snippets that are 4 or fewer lines, keep them as-is
// No need to truncate
@ -540,15 +538,15 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
if (firstSpace > 0 && firstSpace < 20) {
snippet = snippet.substring(firstSpace + 1);
}
snippet = "..." + snippet;
snippet = `...${ snippet}`;
}
if (snippetStart + maxLength < content.length) {
const lastSpace = snippet.search(/\s[^\s]*$/);
if (lastSpace > snippet.length - 20 && lastSpace > 0) {
snippet = snippet.substring(0, lastSpace);
}
snippet = snippet + "...";
snippet = `${snippet }...`;
}
}
@ -572,20 +570,20 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng
return "";
}
let matchingAttributes: Array<{name: string, value: string, type: string}> = [];
const matchingAttributes: Array<{name: string, value: string, type: string}> = [];
// Look for attributes that match the search tokens
for (const attr of attributes) {
const attrName = attr.name?.toLowerCase() || "";
const attrValue = attr.value?.toLowerCase() || "";
const attrType = attr.type || "";
// Check if any search token matches the attribute name or value
const hasMatch = searchTokens.some(token => {
const normalizedToken = normalizeString(token.toLowerCase());
return attrName.includes(normalizedToken) || attrValue.includes(normalizedToken);
});
if (hasMatch) {
matchingAttributes.push({
name: attr.name || "",
@ -611,20 +609,20 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng
const targetTitle = targetNote ? targetNote.title : attr.value;
line = `~${attr.name}="${targetTitle}"`;
}
if (line) {
lines.push(line);
}
}
let snippet = lines.join('\n');
// Apply length limit while preserving line structure
if (snippet.length > maxLength) {
// Try to truncate at word boundaries but keep lines intact
const truncated = snippet.substring(0, maxLength);
const lastNewline = truncated.lastIndexOf('\n');
if (lastNewline > maxLength / 2) {
// If we can keep most content by truncating to last complete line
snippet = truncated.substring(0, lastNewline);
@ -632,7 +630,7 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng
// Otherwise just truncate and add ellipsis
const lastSpace = truncated.lastIndexOf(' ');
snippet = truncated.substring(0, lastSpace > maxLength / 2 ? lastSpace : maxLength - 3);
snippet = snippet + "...";
snippet = `${snippet }...`;
}
}
@ -645,7 +643,7 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng
function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
const searchContext = new SearchContext({
fastSearch: fastSearch,
fastSearch,
includeArchivedNotes: false,
includeHiddenNotes: true,
fuzzyAttributeSearch: true,
@ -666,7 +664,7 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
highlightSearchResults(trimmed, searchContext.highlightedTokens, searchContext.ignoreInternalAttributes);
return trimmed.map((result) => {
const { title, icon } = beccaService.getNoteTitleAndIcon(result.noteId);
const { title, icon } = becca_service.getNoteTitleAndIcon(result.noteId);
return {
notePath: result.notePath,
noteTitle: title,
@ -698,7 +696,7 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
for (const result of searchResults) {
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, "");
// Initialize highlighted content snippet
if (result.contentSnippet) {
// Escape HTML but preserve newlines for later conversion to <br>
@ -706,7 +704,7 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
// Remove any stray < { } that might interfere with our highlighting markers
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/[<{}]/g, "");
}
// Initialize highlighted attribute snippet
if (result.attributeSnippet) {
// Escape HTML but preserve newlines for later conversion to <br>
@ -767,14 +765,14 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
if (result.highlightedNotePathTitle) {
result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/{/g, "<b>").replace(/}/g, "</b>");
}
if (result.highlightedContentSnippet) {
// Replace highlighting markers with HTML tags
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>");
// Convert newlines to <br> tags for HTML display
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/\n/g, "<br>");
}
if (result.highlightedAttributeSnippet) {
// Replace highlighting markers with HTML tags
result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>");

View File

@ -1,24 +1,25 @@
import log from "./log.js";
import { deferred, type OptionRow } from "@triliumnext/commons";
import { events as eventService } from "@triliumnext/core";
import fs from "fs";
import resourceDir from "./resource_dir.js";
import sql from "./sql.js";
import { isElectron } from "./utils.js";
import optionService from "./options.js";
import port from "./port.js";
import { t } from "i18next";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
import BOption from "../becca/entities/boption.js";
import TaskContext from "./task_context.js";
import migrationService from "./migration.js";
import backup from "./backup.js";
import cls from "./cls.js";
import config from "./config.js";
import { deferred, type OptionRow } from "@triliumnext/commons";
import BNote from "../becca/entities/bnote.js";
import BBranch from "../becca/entities/bbranch.js";
import zipImportService from "./import/zip.js";
import password from "./encryption/password.js";
import backup from "./backup.js";
import eventService from "./events.js";
import { t } from "i18next";
import hidden_subtree from "./hidden_subtree.js";
import zipImportService from "./import/zip.js";
import log from "./log.js";
import migrationService from "./migration.js";
import optionService from "./options.js";
import port from "./port.js";
import resourceDir from "./resource_dir.js";
import sql from "./sql.js";
import TaskContext from "./task_context.js";
import { isElectron } from "./utils.js";
export const dbReady = deferred<void>();
@ -65,7 +66,7 @@ async function initDbConnection() {
isSetup TEXT DEFAULT "false",
UNIQUE (tmpID),
PRIMARY KEY (tmpID)
);`)
);`);
dbReady.resolve();
}
@ -88,7 +89,7 @@ async function createInitialDatabase(skipDemoDb?: boolean) {
// We have to import async since options init requires keyboard actions which require translations.
const optionsInitService = (await import("./options_init.js")).default;
const becca_loader = (await import("../becca/becca_loader.js")).default;
const becca_loader = (await import("@triliumnext/core")).becca_loader;
sql.transactional(() => {
log.info("Creating database schema ...");

View File

@ -1,27 +1,24 @@
"use strict";
import log from "./log.js";
import sql from "./sql.js";
import optionService from "./options.js";
import { hmac, randomString, timeLimit } from "./utils.js";
import instanceId from "./instance_id.js";
import dateUtils from "./date_utils.js";
import syncUpdateService from "./sync_update.js";
import contentHashService from "./content_hash.js";
import appInfo from "./app_info.js";
import syncOptions from "./sync_options.js";
import syncMutexService from "./sync_mutex.js";
import cls from "./cls.js";
import request from "./request.js";
import ws from "./ws.js";
import entityChangesService from "./entity_changes.js";
import entityConstructor from "../becca/entity_constructor.js";
import becca from "../becca/becca.js";
import type { EntityChange, EntityChangeRecord, EntityRow } from "@triliumnext/commons";
import { becca_loader, binary_utils, entity_constructor, getInstanceId } from "@triliumnext/core";
import becca from "../becca/becca.js";
import appInfo from "./app_info.js";
import cls from "./cls.js";
import consistency_checks from "./consistency_checks.js";
import contentHashService from "./content_hash.js";
import dateUtils from "./date_utils.js";
import entityChangesService from "./entity_changes.js";
import log from "./log.js";
import optionService from "./options.js";
import request from "./request.js";
import type { CookieJar, ExecOpts } from "./request_interface.js";
import setupService from "./setup.js";
import consistency_checks from "./consistency_checks.js";
import becca_loader from "../becca/becca_loader.js";
import sql from "./sql.js";
import syncMutexService from "./sync_mutex.js";
import syncOptions from "./sync_options.js";
import syncUpdateService from "./sync_update.js";
import { hmac, randomString, timeLimit } from "./utils.js";
import ws from "./ws.js";
let proxyToggle = true;
@ -94,16 +91,16 @@ async function sync() {
success: false,
message: "No connection to sync server."
};
} else {
log.info(`Sync failed: '${e.message}', stack: ${e.stack}`);
ws.syncFailed();
return {
success: false,
message: e.message
};
}
log.info(`Sync failed: '${e.message}', stack: ${e.stack}`);
ws.syncFailed();
return {
success: false,
message: e.message
};
}
}
@ -123,16 +120,16 @@ async function doLogin(): Promise<SyncContext> {
const syncContext: SyncContext = { cookieJar: {} };
const resp = await syncRequest<SyncResponse>(syncContext, "POST", "/api/login/sync", {
timestamp: timestamp,
timestamp,
syncVersion: appInfo.syncVersion,
hash: hash
hash
});
if (!resp) {
throw new Error("Got no response.");
}
if (resp.instanceId === instanceId) {
if (resp.instanceId === getInstanceId()) {
throw new Error(
`Sync server has instance ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`
);
@ -157,7 +154,7 @@ async function pullChanges(syncContext: SyncContext) {
while (true) {
const lastSyncedPull = getLastSyncedPull();
const logMarkerId = randomString(10); // to easily pair sync events between client and server logs
const changesUri = `/api/sync/changed?instanceId=${instanceId}&lastEntityChangeId=${lastSyncedPull}&logMarkerId=${logMarkerId}`;
const changesUri = `/api/sync/changed?instanceId=${getInstanceId()}&lastEntityChangeId=${lastSyncedPull}&logMarkerId=${logMarkerId}`;
const startDate = Date.now();
@ -219,9 +216,9 @@ async function pushChanges(syncContext: SyncContext) {
lastSyncedPush = entityChange.id;
return false;
} else {
return true;
}
return true;
});
if (filteredEntityChanges.length === 0 && lastSyncedPush) {
@ -239,7 +236,7 @@ async function pushChanges(syncContext: SyncContext) {
await syncRequest(syncContext, "PUT", `/api/sync/update?logMarkerId=${logMarkerId}`, {
entities: entityChangesRecords,
instanceId
instanceId: getInstanceId()
});
ws.syncPushInProgress();
@ -319,7 +316,7 @@ async function syncRequest<T extends {}>(syncContext: SyncContext, method: strin
method,
url: syncOptions.getSyncServerHost() + requestPath,
cookieJar: syncContext.cookieJar,
timeout: timeout,
timeout,
paging: {
pageIndex,
pageCount,
@ -340,33 +337,33 @@ function getEntityChangeRow(entityChange: EntityChange) {
if (entityName === "note_reordering") {
return sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]);
} else {
const primaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
if (!primaryKey) {
throw new Error(`Unknown entity for entity change ${JSON.stringify(entityChange)}`);
}
const entityRow = sql.getRow<EntityRow>(/*sql*/`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (!entityRow) {
log.error(`Cannot find entity for entity change ${JSON.stringify(entityChange)}`);
return null;
}
if (entityName === "blobs" && entityRow.content !== null) {
if (typeof entityRow.content === "string") {
entityRow.content = Buffer.from(entityRow.content, "utf-8");
}
if (entityRow.content) {
entityRow.content = entityRow.content.toString("base64");
}
}
return entityRow;
}
const primaryKey = entity_constructor.getEntityFromEntityName(entityName).primaryKeyName;
if (!primaryKey) {
throw new Error(`Unknown entity for entity change ${JSON.stringify(entityChange)}`);
}
const entityRow = sql.getRow<EntityRow>(/*sql*/`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (!entityRow) {
log.error(`Cannot find entity for entity change ${JSON.stringify(entityChange)}`);
return null;
}
if (entityName === "blobs" && entityRow.content !== null) {
if (typeof entityRow.content === "string") {
entityRow.content = Buffer.from(entityRow.content, "utf-8");
}
if (entityRow.content) {
entityRow.content = binary_utils.encodeBase64(entityRow.content);
}
}
return entityRow;
}
function getEntityChangeRecords(entityChanges: EntityChange[]) {

Some files were not shown because too many files have changed in this diff Show More