Merge remote-tracking branch 'origin/develop' into port/client_ts

This commit is contained in:
Elian Doran
2025-02-24 10:14:46 +02:00
338 changed files with 8874 additions and 2488 deletions
+2 -2
View File
@@ -5,8 +5,8 @@ import build from "./build.js";
import packageJson from "../../package.json" with { type: "json" };
import dataDir from "./data_dir.js";
const APP_DB_VERSION = 228;
const SYNC_VERSION = 34;
const APP_DB_VERSION = 229;
const SYNC_VERSION = 35;
const CLIPPER_PROTOCOL_VERSION = "1.0";
export default {
+12 -1
View File
@@ -7,6 +7,8 @@ import { isElectron } from "./utils.js";
import passwordEncryptionService from "./encryption/password_encryption.js";
import config from "./config.js";
import passwordService from "./encryption/password.js";
import options from "./options.js";
import attributes from "./attributes.js";
import type { NextFunction, Request, Response } from "express";
const noAuthentication = config.General && config.General.noAuthentication === true;
@@ -15,7 +17,16 @@ function checkAuth(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.redirect("setup");
} else if (!req.session.loggedIn && !isElectron && !noAuthentication) {
res.redirect("login");
const redirectToShare = options.getOptionBool("redirectBareDomain");
if (redirectToShare) {
// Check if any note has the #shareRoot label
const shareRootNotes = attributes.getNotesWithLabel("shareRoot");
if (shareRootNotes.length === 0) {
res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." });
return;
}
}
res.redirect(redirectToShare ? "share" : "login");
} else {
next();
}
+2 -2
View File
@@ -1,4 +1,4 @@
export default {
buildDate: "2024-09-07T18:36:34Z",
buildRevision: "7c0d6930fa8f20d269dcfbcbc8f636a25f6bb9a7"
buildDate: "2025-02-22T11:59:41Z",
buildRevision: "60da36757009d908ed93a394d030b18b3eab4d98"
};
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { getStylesDirectory, readThemesFromFileSystem } from "./code_block_theme.js";
import themeNames from "./code_block_theme_names.json" with { type: "json" };
describe("Code block theme", () => {
it("all themes are mapped", () => {
const themes = readThemesFromFileSystem(getStylesDirectory());
const mappedThemeNames = new Set(Object.values(themeNames));
const unmappedThemeNames = new Set<string>();
for (const theme of themes) {
if (!mappedThemeNames.has(theme.title)) {
unmappedThemeNames.add(theme.title);
}
}
expect(unmappedThemeNames.size, `Unmapped themes: ${Array.from(unmappedThemeNames).join(", ")}`).toBe(0);
});
});
+2 -2
View File
@@ -44,7 +44,7 @@ export function listSyntaxHighlightingThemes() {
};
}
function getStylesDirectory() {
export function getStylesDirectory() {
if (isElectron && !isDev) {
return "styles";
}
@@ -60,7 +60,7 @@ function getStylesDirectory() {
* @param path the path to read from. Usually this is the highlight.js `styles` directory.
* @returns the list of themes.
*/
function readThemesFromFileSystem(path: string): ColorTheme[] {
export function readThemesFromFileSystem(path: string): ColorTheme[] {
return fs
.readdirSync(path)
.filter((el) => el.endsWith(".min.css"))
+7
View File
@@ -14,6 +14,10 @@
"brown paper": "Brown Paper (Light)",
"codepen embed": "CodePen Embed (Dark)",
"color brewer": "Color Brewer (Light)",
"cybertopia cherry": "Cybertopia Cherry (Dark)",
"cybertopia dimmer": "Cybertopia Dimmer (Dark)",
"cybertopia icecap": "Cybertopia Icecap (Dark)",
"cybertopia saturated": "Cybertopia Saturated (Dark)",
"dark": "Dark",
"default": "Original highlight.js Theme (Light)",
"devibeans": "devibeans (Dark)",
@@ -58,6 +62,9 @@
"qtcreator light": "Qt Creator (Light)",
"rainbow": "Rainbow (Dark)",
"routeros": "RouterOS Script (Light)",
"rose pine dawn": "Rose Pine Dawn (Light)",
"rose pine moon": "Rose Pine Moon (Dark)",
"rose pine": "Rose Pine (Dark)",
"school book": "School Book (Light)",
"shades of purple": "Shades of Purple (Dark)",
"srcery": "Srcery (Dark)",
+5 -1
View File
@@ -34,6 +34,7 @@ export interface TriliumConfig {
};
Session: {
cookiePath: string;
cookieMaxAge: number;
}
Sync: {
syncServerHost: string;
@@ -81,7 +82,10 @@ const config: TriliumConfig = {
Session: {
cookiePath:
process.env.TRILIUM_SESSION_COOKIEPATH || iniConfig?.Session?.cookiePath || "/"
process.env.TRILIUM_SESSION_COOKIEPATH || iniConfig?.Session?.cookiePath || "/",
cookieMaxAge:
parseInt(String(process.env.TRILIUM_SESSION_COOKIEMAXAGE)) || parseInt(iniConfig?.Session?.cookieMaxAge) || 21 * 24 * 60 * 60 // 21 Days in Seconds
},
Sync: {
+1 -1
View File
@@ -888,7 +888,7 @@ class ConsistencyChecks {
return `${tableName}: ${count}`;
}
const tables = ["notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs"];
const tables = ["notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs", "tasks"];
log.info(`Table counts: ${tables.map((tableName) => getTableRowCount(tableName)).join(", ")}`);
}
+24
View File
@@ -74,4 +74,28 @@ describe("Markdown export", () => {
const expected = "~~hello~~Hello ~~world~~";
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
it("exports headings properly", () => {
const html = trimIndentation`\
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
`;
const expected = trimIndentation`\
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
});
+4 -1
View File
@@ -24,7 +24,10 @@ const fencedCodeBlockFilter: TurndownService.Rule = {
function toMarkdown(content: string) {
if (instance === null) {
instance = new TurndownService({ codeBlockStyle: "fenced" });
instance = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced"
});
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
instance.use(turndownPluginGfm.gfm);
+12
View File
@@ -407,6 +407,10 @@ ${markdownContent}`;
}
function saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) {
if (!navigationMeta.dataFileName) {
return;
}
function saveNavigationInner(meta: NoteMeta) {
let html = "<li>";
@@ -451,6 +455,10 @@ ${markdownContent}`;
let firstNonEmptyNote;
let curMeta = rootMeta;
if (!indexMeta.dataFileName) {
return;
}
while (!firstNonEmptyNote) {
if (curMeta.dataFileName && curMeta.noteId) {
firstNonEmptyNote = getNoteTargetUrl(curMeta.noteId, rootMeta);
@@ -479,6 +487,10 @@ ${markdownContent}`;
}
function saveCss(rootMeta: NoteMeta, cssMeta: NoteMeta) {
if (!cssMeta.dataFileName) {
return;
}
const cssContent = fs.readFileSync(`${RESOURCE_DIR}/libraries/ckeditor/ckeditor-content.css`);
archive.append(cssContent, { name: cssMeta.dataFileName });
+19 -8
View File
@@ -6,7 +6,7 @@ import noteService from "./notes.js";
import log from "./log.js";
import migrationService from "./migration.js";
import { t } from "i18next";
import { getHelpHiddenSubtreeData } from "./in_app_help.js";
import { cleanUpHelp, getHelpHiddenSubtreeData } from "./in_app_help.js";
import buildLaunchBarConfig from "./hidden_subtree_launcherbar.js";
const LBTPL_ROOT = "_lbTplRoot";
@@ -58,7 +58,7 @@ enum Command {
let hiddenSubtreeDefinition: HiddenSubtreeItem;
function buildHiddenSubtreeDefinition(): HiddenSubtreeItem {
function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenSubtreeItem {
const launchbarConfig = buildLaunchBarConfig();
return {
@@ -283,7 +283,7 @@ function buildHiddenSubtreeDefinition(): HiddenSubtreeItem {
title: t("hidden-subtree.user-guide"),
type: "book",
icon: "bx-help-circle",
children: getHelpHiddenSubtreeData(),
children: helpSubtree,
isExpanded: true
}
]
@@ -301,11 +301,19 @@ function checkHiddenSubtree(force = false, extraOpts: CheckHiddenExtraOpts = {})
return;
}
const helpSubtree = getHelpHiddenSubtreeData();
if (!hiddenSubtreeDefinition || force) {
hiddenSubtreeDefinition = buildHiddenSubtreeDefinition();
hiddenSubtreeDefinition = buildHiddenSubtreeDefinition(helpSubtree);
}
checkHiddenSubtreeRecursively("root", hiddenSubtreeDefinition, extraOpts);
try {
cleanUpHelp(helpSubtree);
} catch (e) {
// Non-critical operation should something go wrong.
console.error(e);
}
}
function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtreeItem, extraOpts: CheckHiddenExtraOpts = {}) {
@@ -400,10 +408,13 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
value: attr.value,
isInheritable: false
}).save();
} else if (attr.name === "docName") {
// Updating docname
existingAttribute.value = attr.value ?? "";
existingAttribute.save();
} else if (attr.name === "docName"
|| (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
if (existingAttribute.value !== attr.value) {
existingAttribute.value = attr.value ?? "";
console.log("Updating attribute ", attrId);
existingAttribute.save();
}
}
}
+9 -9
View File
@@ -34,6 +34,12 @@ export default function buildLaunchBarConfig() {
type: "launcher",
builtinWidget: "calendar",
icon: "bx bx-calendar"
},
recentChanges: {
title: t("hidden-subtree.recent-changes-title"),
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history"
}
};
@@ -63,14 +69,7 @@ export default function buildLaunchBarConfig() {
},
{ id: "_lbNoteMap", title: t("hidden-subtree.note-map-title"), type: "launcher", targetNoteId: "_globalNoteMap", icon: "bx bxs-network-chart" },
{ id: "_lbCalendar", ...sharedLaunchers.calendar },
{
id: "_lbRecentChanges",
title: t("hidden-subtree.recent-changes-title"),
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{ id: "_lbRecentChanges", ...sharedLaunchers.recentChanges },
{ id: "_lbSpacer1", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "50", growthFactor: "0" },
{ id: "_lbBookmarks", title: t("hidden-subtree.bookmarks-title"), type: "launcher", builtinWidget: "bookmarks", icon: "bx bx-bookmark" },
{ id: "_lbToday", ...sharedLaunchers.openToday },
@@ -90,7 +89,8 @@ export default function buildLaunchBarConfig() {
{ id: "_lbMobileBackInHistory", ...sharedLaunchers.backInHistory },
{ id: "_lbMobileForwardInHistory", ...sharedLaunchers.forwardInHistory },
{ id: "_lbMobileJumpTo", title: t("hidden-subtree.jump-to-note-title"), type: "launcher", command: "jumpToNote", icon: "bx bx-plus-circle" },
{ id: "_lbMobileCalendar", ...sharedLaunchers.calendar }
{ id: "_lbMobileCalendar", ...sharedLaunchers.calendar },
{ id: "_lbMobileRecentChanges", ...sharedLaunchers.recentChanges }
];
return {
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import html_sanitizer from "./html_sanitizer.js";
import { trimIndentation } from "../../spec/support/utils.js";
describe("sanitize", () => {
it("filters out position inline CSS", () => {
const dirty = `<div style="z-index:999999999;margin:0px;left:250px;height:100px;display:table;background:none;position:fixed;top:250px;"></div>`;
const clean = `<div></div>`;
expect(html_sanitizer.sanitize(dirty)).toBe(clean);
});
it("keeps inline styles defined in CKEDitor", () => {
const dirty = trimIndentation`\
<p>
<span style="color:hsl(0, 0%, 90%);">
Hi
</span>
<span style="background-color:hsl(30, 75%, 60%);">
there
</span>
</p>
<figure class="table" style="float:left;height:800px;width:600px;">
<table style="background-color:hsl(0, 0%, 90%);border-color:hsl(0, 0%, 0%);border-style:dotted;">
<tbody>
<tr>
<td style="border:2px groove hsl(60, 75%, 60%);"></td>
</tr>
</tbody>
</table>
</figure>`;
const clean = trimIndentation`\
<p>
<span style="color:hsl(0, 0%, 90%)">
Hi
</span>
<span style="background-color:hsl(30, 75%, 60%)">
there
</span>
</p>
<figure class="table" style="float:left;height:800px;width:600px">
<table style="background-color:hsl(0, 0%, 90%);border-color:hsl(0, 0%, 0%);border-style:dotted">
<tbody>
<tr>
<td style="border:2px groove hsl(60, 75%, 60%)"></td>
</tr>
</tbody>
</table>
</figure>`;
expect(html_sanitizer.sanitize(dirty)) .toBe(clean);
});
});
+32 -50
View File
@@ -2,6 +2,16 @@ import sanitizeHtml from "sanitize-html";
import sanitizeUrl from "@braintree/sanitize-url";
import optionService from "./options.js";
// Be consistent with `ALLOWED_PROTOCOLS` in `src\public\app\services\link.js`
// TODO: Deduplicate with client once we can.
export const ALLOWED_PROTOCOLS = [
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo',
'mid'
];
// Default list of allowed HTML tags
export const DEFAULT_ALLOWED_TAGS = [
"h1",
@@ -131,6 +141,9 @@ function sanitize(dirtyHtml: string) {
allowedTags = DEFAULT_ALLOWED_TAGS;
}
const colorRegex = [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/, /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/];
const sizeRegex = [/^\d+(?:px|em|%)$/];
// to minimize document changes, compress H
return sanitizeHtml(dirtyHtml, {
allowedTags,
@@ -138,56 +151,25 @@ function sanitize(dirtyHtml: string) {
"*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"],
input: ["type", "checked"]
},
// Be consistent with `allowedSchemes` in `src\public\app\services\link.js`
allowedSchemes: [
"http",
"https",
"ftp",
"ftps",
"mailto",
"data",
"evernote",
"file",
"facetime",
"gemini",
"git",
"gopher",
"imap",
"irc",
"irc6",
"jabber",
"jar",
"lastfm",
"ldap",
"ldaps",
"magnet",
"message",
"mumble",
"nfs",
"onenote",
"pop",
"rmi",
"s3",
"sftp",
"skype",
"sms",
"spotify",
"steam",
"svn",
"udp",
"view-source",
"vlc",
"vnc",
"ws",
"wss",
"xmpp",
"jdbc",
"slack",
"tel",
"smb",
"zotero",
"geo"
],
allowedStyles: {
"*": {
"color": colorRegex,
"background-color": colorRegex
},
"figure": {
"float": [ /^\s*(left|right|none)\s*$/ ],
"width": sizeRegex,
"height": sizeRegex
},
"table": {
"border-color": colorRegex,
"border-style": [ /^\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*$/ ]
},
"td": {
"border": [ /^\s*\d+(?:px|em|%)\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*(#(0x)?[0-9a-fA-F]+|rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)|hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\))\s*$/ ]
}
},
allowedSchemes: ALLOWED_PROTOCOLS,
nonTextTags: ["head"],
transformTags
});
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import * as i18n from "./i18n.js";
import path from "path";
import fs from "fs";
function checkTranslations(translationDir: string, translationFileName: string) {
const locales = i18n.getLocales();
for (const locale of locales) {
const translationPath = path.join(translationDir, locale.id, translationFileName);
const translationFile = fs.readFileSync(translationPath, { encoding: "utf-8" });
expect(() => {
JSON.parse(translationFile);
}, `JSON error while parsing locale '${locale.id}' at "${translationPath}"`).not.toThrow();
}
}
describe("i18n", () => {
it("frontend translations are valid JSON", () => {
checkTranslations("src/public/translations", "translation.json");
});
it("backend translations are valid JSON", () => {
checkTranslations("translations", "server.json");
});
});
+34
View File
@@ -20,6 +20,40 @@ export async function initializeTranslations() {
});
}
export function getLocales() {
// TODO: Currently hardcoded, needs to read the list of available languages.
return [
{
id: "en",
name: "English"
},
{
id: "de",
name: "Deutsch"
},
{
id: "es",
name: "Español"
},
{
id: "fr",
name: "Français"
},
{
id: "cn",
name: "简体中文"
},
{
id: "tw",
name: "繁體中文"
},
{
id: "ro",
name: "Română"
}
];
}
function getCurrentLanguage() {
let language;
if (sql_init.isDbInitialized()) {
+1 -5
View File
@@ -315,14 +315,10 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
resource.mime = resource.mime || "application/octet-stream";
const createFileNote = () => {
if (typeof resource.content !== "string") {
throw new Error("Missing or wrong content type for resource.");
}
const resourceNote = noteService.createNewNote({
parentNoteId: noteEntity.noteId,
title: resource.title,
content: resource.content,
content: resource.content ?? "",
type: "file",
mime: resource.mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
+1 -1
View File
@@ -88,7 +88,7 @@ function getType(options: TaskData, mime: string) {
const mimeLc = mime?.toLowerCase();
switch (true) {
case options.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown"].includes(mimeLc):
case options.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown", "text/mdx"].includes(mimeLc):
return "text";
case options.codeImportedAsCode && CODE_MIME_TYPES.has(mimeLc):
+21
View File
@@ -0,0 +1,21 @@
Page 1
Heading 1
---------
Heading 2
---------
### Heading 3
```
class Foo {
hoistedNoteChangedEvent({ ntxId }) {
if (this.isNoteContext(ntxId)) {
this.refresh();
}
}
}
```
Page 2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+81
View File
@@ -0,0 +1,81 @@
import { beforeAll, describe, expect, it } from "vitest";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import becca from "../../becca/becca.js";
import BNote from "../../becca/entities/bnote.js";
import TaskContext from "../task_context.js";
import cls from "../cls.js";
import sql_init from "../sql_init.js";
import { initializeTranslations } from "../i18n.js";
import single from "./single.js";
import stripBom from "strip-bom";
const scriptDir = dirname(fileURLToPath(import.meta.url));
async function testImport(fileName: string, mimetype: string) {
const buffer = fs.readFileSync(path.join(scriptDir, "samples", fileName));
const taskContext = TaskContext.getInstance("import-mdx", "import", {
textImportedAsText: true,
codeImportedAsCode: true
});
return new Promise<{ buffer: Buffer, importedNote: BNote }>((resolve, reject) => {
cls.init(async () => {
const rootNote = becca.getNote("root");
if (!rootNote) {
reject("Missing root note.");
}
const importedNote = single.importSingleFile(taskContext, {
originalname: fileName,
mimetype,
buffer: buffer
}, rootNote as BNote);
resolve({
buffer,
importedNote
});
});
});
}
describe("processNoteContent", () => {
beforeAll(async () => {
initializeTranslations();
sql_init.initializeDb();
await sql_init.dbReady;
});
it("treats single MDX as Markdown", async () => {
const { importedNote } = await testImport("Text Note.mdx", "text/mdx");
expect(importedNote.mime).toBe("text/html");
expect(importedNote.type).toBe("text");
expect(importedNote.title).toBe("Text Note");
});
it("supports HTML note with UTF-16 (w/ BOM) from Microsoft Outlook", async () => {
const { importedNote } = await testImport("IREN Reports Q2 FY25 Results.htm", "text/html");
expect(importedNote.mime).toBe("text/html");
expect(importedNote.title).toBe("IREN Reports Q2 FY25 Results");
expect(importedNote.getContent().toString().substring(0, 5)).toEqual("<html");
});
it("supports code note with UTF-16", async () => {
const { importedNote, buffer } = await testImport("UTF-16LE Code Note.json", "application/json");
expect(importedNote.mime).toBe("application/json");
expect(importedNote.getContent().toString()).toStrictEqual(stripBom(buffer.toString("utf-16le")));
});
it("supports plain text note with UTF-16", async () => {
const { importedNote } = await testImport("UTF-16LE Text Note.txt", "text/plain");
expect(importedNote.mime).toBe("text/html");
expect(importedNote.getContent().toString()).toBe("<p>Plain text goes here.<br></p>");
});
it("supports markdown note with UTF-16", async () => {
const { importedNote } = await testImport("UTF-16LE Text Note.md", "text/markdown");
expect(importedNote.mime).toBe("text/html");
expect(importedNote.getContent().toString()).toBe("<h2>Hello world</h2>\n<p>Plain text goes here.</p>\n");
});
})
+6 -6
View File
@@ -8,7 +8,7 @@ import imageService from "../../services/image.js";
import protectedSessionService from "../protected_session.js";
import markdownService from "./markdown.js";
import mimeService from "./mime.js";
import { getNoteTitle } from "../../services/utils.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";
@@ -19,7 +19,7 @@ function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNot
if (taskContext?.data?.textImportedAsText) {
if (mime === "text/html") {
return importHtml(taskContext, file, parentNote);
} else if (["text/markdown", "text/x-markdown"].includes(mime)) {
} else if (["text/markdown", "text/x-markdown", "text/mdx"].includes(mime)) {
return importMarkdown(taskContext, file, parentNote);
} else if (mime === "text/plain") {
return importPlainText(taskContext, file, parentNote);
@@ -69,7 +69,7 @@ function importFile(taskContext: TaskContext, file: File, parentNote: BNote) {
function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote) {
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
const content = file.buffer.toString("utf-8");
const content = processStringOrBuffer(file.buffer);
const detectedMime = mimeService.getMime(file.originalname) || file.mimetype;
const mime = mimeService.normalizeMimeType(detectedMime);
@@ -89,7 +89,7 @@ function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote)
function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote) {
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
const plainTextContent = file.buffer.toString("utf-8");
const plainTextContent = processStringOrBuffer(file.buffer);
const htmlContent = convertTextToHtml(plainTextContent);
const { note } = noteService.createNewNote({
@@ -125,7 +125,7 @@ function convertTextToHtml(text: string) {
function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote) {
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
const markdownContent = file.buffer.toString("utf-8");
const markdownContent = processStringOrBuffer(file.buffer);
let htmlContent = markdownService.renderToHtml(markdownContent, title);
if (taskContext.data?.safeImport) {
@@ -147,7 +147,7 @@ function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote)
}
function importHtml(taskContext: TaskContext, file: File, parentNote: BNote) {
let content = file.buffer.toString("utf-8");
let content = processStringOrBuffer(file.buffer);
// Try to get title from HTML first, fall back to filename
// We do this before sanitization since that turns all <h1>s into <h2>
+57
View File
@@ -0,0 +1,57 @@
import { beforeAll, describe, expect, it } from "vitest";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import zip from "./zip.js";
import becca from "../../becca/becca.js";
import BNote from "../../becca/entities/bnote.js";
import TaskContext from "../task_context.js";
import cls from "../cls.js";
import sql_init from "../sql_init.js";
import { initializeTranslations } from "../i18n.js";
const scriptDir = dirname(fileURLToPath(import.meta.url));
async function testImport(fileName: string) {
const mdxSample = fs.readFileSync(path.join(scriptDir, "samples", fileName));
const taskContext = TaskContext.getInstance("import-mdx", "import", {
textImportedAsText: true
});
return new Promise<{ importedNote: BNote; rootNote: BNote }>((resolve, reject) => {
cls.init(async () => {
const rootNote = becca.getNote("root");
if (!rootNote) {
expect(rootNote).toBeTruthy();
return;
}
const importedNote = await zip.importZip(taskContext, mdxSample, rootNote as BNote);
resolve({
importedNote,
rootNote
});
});
});
}
describe("processNoteContent", () => {
beforeAll(async () => {
initializeTranslations();
sql_init.initializeDb();
await sql_init.dbReady;
});
it("treats single MDX as Markdown in ZIP as text note", async () => {
const { importedNote } = await testImport("mdx.zip");
expect(importedNote.mime).toBe("text/mdx");
expect(importedNote.type).toBe("text");
expect(importedNote.title).toBe("Text Note");
});
it("can import email from Microsoft Outlook with UTF-16 with BOM", async () => {
const { rootNote, importedNote } = await testImport("IREN.Reports.Q2.FY25.Results_files.zip");
const htmlNote = rootNote.children.find((ch) => ch.title === "IREN Reports Q2 FY25 Results");
expect(htmlNote?.getContent().toString().substring(0, 4)).toEqual("<div");
});
})
+3 -3
View File
@@ -1,7 +1,7 @@
"use strict";
import BAttribute from "../../becca/entities/battribute.js";
import { removeTextFileExtension, newEntityId, getNoteTitle } from "../../services/utils.js";
import { removeTextFileExtension, newEntityId, getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import attributeService from "../../services/attributes.js";
@@ -386,7 +386,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
}
function processNoteContent(noteMeta: NoteMeta | undefined, type: string, mime: string, content: string | Buffer, noteTitle: string, filePath: string) {
if ((noteMeta?.format === "markdown" || (!noteMeta && taskContext.data?.textImportedAsText && ["text/markdown", "text/x-markdown"].includes(mime))) && typeof content === "string") {
if ((noteMeta?.format === "markdown" || (!noteMeta && taskContext.data?.textImportedAsText && ["text/markdown", "text/x-markdown", "text/mdx"].includes(mime))) && typeof content === "string") {
content = markdownService.renderToHtml(content, noteTitle);
}
@@ -457,7 +457,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
}
if (type !== "file" && type !== "image") {
content = content.toString("utf-8");
content = processStringOrBuffer(content);
}
const noteTitle = getNoteTitle(filePath, taskContext.data?.replaceUnderscoresWithSpaces || false, noteMeta);
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { parseNoteMeta } from "./in_app_help.js";
import type NoteMeta from "./meta/note_meta.js";
describe("In-app help", () => {
it("preserves custom folder icon", () => {
const meta: NoteMeta = {
"isClone": false,
"noteId": "yoAe4jV2yzbd",
"notePath": [
"OkOZllzB3fqN",
"yoAe4jV2yzbd"
],
"title": "Features",
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-star",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"attachments": [],
"dirFileName": "Features",
"children": []
};
const item = parseNoteMeta(meta, "/");
const icon = item.attributes?.find((a) => a.name === "iconClass");
expect(icon?.value).toBe("bx bx-star");
});
});
+76 -8
View File
@@ -5,6 +5,8 @@ import type NoteMeta from "./meta/note_meta.js";
import type { NoteMetaFile } from "./meta/note_meta.js";
import { fileURLToPath } from "url";
import { isDev } from "./utils.js";
import type BNote from "../becca/entities/bnote.js";
import becca from "../becca/becca.js";
export function getHelpHiddenSubtreeData() {
const srcRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -31,7 +33,7 @@ function parseNoteMetaFile(noteMetaFile: NoteMetaFile): HiddenSubtreeItem[] {
return parsedMetaRoot.children ?? [];
}
function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeItem {
export function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeItem {
let iconClass: string = "bx bx-file";
const item: HiddenSubtreeItem = {
id: `_help_${noteMeta.noteId}`,
@@ -40,19 +42,28 @@ function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeIt
attributes: []
};
// Handle attributes
for (const attribute of noteMeta.attributes ?? []) {
if (attribute.name === "iconClass") {
iconClass = attribute.value;
}
}
// Handle folder notes
if (!noteMeta.dataFileName) {
iconClass = "bx bx-folder";
item.type = "book";
}
// Handle attributes
for (const attribute of noteMeta.attributes ?? []) {
if (attribute.name === "iconClass") {
iconClass = attribute.value;
continue;
}
if (attribute.name === "webViewSrc") {
item.attributes?.push({
type: "label",
name: attribute.name,
value: attribute.value
});
}
}
// Handle text notes
if (noteMeta.type === "text" && noteMeta.dataFileName) {
const docPath = `${docNameRoot}/${path.basename(noteMeta.dataFileName, ".html")}`
@@ -64,6 +75,11 @@ function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeIt
});
}
// Handle web views
if (noteMeta.type === "webView") {
item.type = "webView";
}
// Handle children
if (noteMeta.children) {
const children: HiddenSubtreeItem[] = [];
@@ -84,3 +100,55 @@ function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeIt
return item;
}
/**
* Iterates recursively through the help subtree that the user has and compares it against the definition
* to remove any notes that are no longer present in the latest version of the help.
*
* @param helpDefinition the hidden subtree definition for the help, to compare against the user's structure.
*/
export function cleanUpHelp(helpDefinition: HiddenSubtreeItem[]) {
function getFlatIds(items: HiddenSubtreeItem | HiddenSubtreeItem[]) {
const ids: (string | string[])[] = [];
if (Array.isArray(items)) {
for (const item of items) {
ids.push(getFlatIds(item));
}
} else {
if (items.children) {
for (const child of items.children) {
ids.push(getFlatIds(child));
}
}
ids.push(items.id);
}
return ids.flat();
}
function getFlatIdsFromNote(note: BNote | null) {
if (!note) {
return [];
}
const ids: (string | string[])[] = [];
for (const subnote of note.getChildNotes()) {
ids.push(getFlatIdsFromNote(subnote));
}
ids.push(note.noteId);
return ids.flat();
}
const definitionHelpIds = new Set(getFlatIds(helpDefinition));
const realHelpIds = getFlatIdsFromNote(becca.getNote("_help"));
for (const realHelpId of realHelpIds) {
if (realHelpId === "_help") {
continue;
}
if (!definitionHelpIds.has(realHelpId)) {
becca.getNote(realHelpId)?.deleteNote();
}
}
}
+6
View File
@@ -238,6 +238,12 @@ function getDefaultKeyboardActions() {
description: t("keyboard_actions.toggle-tray"),
scope: "window"
},
{
actionName: "toggleZenMode",
defaultShortcuts: ["Alt+Z"],
description: t("keyboard_actions.toggle-zen-mode"),
scope: "window"
},
{
actionName: "firstTab",
defaultShortcuts: ["CommandOrControl+1"],
@@ -35,6 +35,7 @@ const enum KeyboardActionNamesEnum {
activatePreviousTab,
openNewWindow,
toggleTray,
toggleZenMode,
firstTab,
secondTab,
thirdTab,
+3 -2
View File
@@ -1,3 +1,4 @@
import type { NoteType } from "../../becca/entities/rows.js";
import type AttachmentMeta from "./attachment_meta.js";
import type AttributeMeta from "./attribute_meta.js";
@@ -15,11 +16,11 @@ export default interface NoteMeta {
notePosition?: number;
prefix?: string | null;
isExpanded?: boolean;
type?: string;
type?: NoteType;
mime?: string;
/** 'html' or 'markdown', applicable to text notes only */
format?: "html" | "markdown";
dataFileName: string;
dataFileName?: string;
dirFileName?: string;
/** this file should not be imported (e.g., HTML navigation) */
noImport?: boolean;
+4 -12
View File
@@ -6,6 +6,7 @@ import { crash } from "./utils.js";
import resourceDir from "./resource_dir.js";
import appInfo from "./app_info.js";
import cls from "./cls.js";
import { t } from "i18next";
interface MigrationInfo {
dbVersion: number;
@@ -18,9 +19,7 @@ async function migrate() {
const currentDbVersion = getDbVersion();
if (currentDbVersion < 214) {
log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.4 first and only then to this version.");
await crash();
await crash(t("migration.old_version"));
return;
}
@@ -83,10 +82,7 @@ async function migrate() {
log.info(`Migration to version ${mig.dbVersion} has been successful.`);
} catch (e: any) {
log.error(`error during migration to version ${mig.dbVersion}: ${e.stack}`);
log.error("migration failed, crashing hard"); // this is not very user-friendly :-/
crash();
crash(t("migration.error_message", { version: mig.dbVersion, stack: e.stack }));
break; // crash() is sometimes async
}
}
@@ -136,11 +132,7 @@ async function migrateIfNecessary() {
const currentDbVersion = getDbVersion();
if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== "true") {
log.error(
`Current DB version ${currentDbVersion} is newer than the current DB version ${appInfo.dbVersion}, which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue.`
);
await crash();
await crash(t("migration.wrong_db_version", { version: currentDbVersion, targetVersion: appInfo.dbVersion }));
}
if (!isDbUpToDate()) {
+2 -1
View File
@@ -15,7 +15,8 @@ const noteTypes = [
{ type: "doc", defaultMime: "" },
{ type: "contentWidget", defaultMime: "" },
{ type: "mindMap", defaultMime: "application/json" },
{ type: "geoMap", defaultMime: "application/json" }
{ type: "geoMap", defaultMime: "application/json" },
{ type: "taskList", defaultMime: "" }
];
function getDefaultMimeForNoteType(typeName: string) {
+11 -3
View File
@@ -75,8 +75,10 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
*/
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 },
@@ -96,7 +98,7 @@ const defaultOptions: DefaultOption[] = [
{ name: "codeLineWrapEnabled", value: "true", isSynced: false },
{
name: "codeNotesMimeTypes",
value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","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"]',
value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","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 },
@@ -105,6 +107,7 @@ const defaultOptions: DefaultOption[] = [
{ name: "rightPaneVisible", value: "true", 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 },
@@ -121,7 +124,8 @@ const defaultOptions: DefaultOption[] = [
{ name: "highlightsList", value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
{ name: "checkForUpdates", value: "true", isSynced: true },
{ name: "disableTray", value: "false", isSynced: false },
{ name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true },
{ name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days
{ name: "eraseUnusedAttachmentsAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day
{ name: "customSearchEngineName", value: "DuckDuckGo", isSynced: true },
{ name: "customSearchEngineUrl", value: "https://duckduckgo.com/?q={keyword}", isSynced: true },
{ name: "promotedAttributesOpenInRibbon", value: "true", isSynced: true },
@@ -251,7 +255,11 @@ const defaultOptions: DefaultOption[] = [
"tt"
]),
isSynced: true
}
},
// Share settings
{ name: "redirectBareDomain", value: "false", isSynced: true },
{ name: "showLoginInShareTheme", value: "false", isSynced: true }
];
/**
+8
View File
@@ -49,8 +49,10 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
lastSyncedPull: number;
lastSyncedPush: number;
revisionSnapshotTimeInterval: number;
revisionSnapshotTimeIntervalTimeScale: number;
revisionSnapshotNumberLimit: number;
protectedSessionTimeout: number;
protectedSessionTimeoutTimeScale: number;
zoomFactor: number;
mainFontSize: number;
treeFontSize: number;
@@ -61,11 +63,13 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
leftPaneWidth: number;
rightPaneWidth: number;
eraseEntitiesAfterTimeInSeconds: number;
eraseEntitiesAfterTimeScale: number;
autoReadonlySizeText: number;
autoReadonlySizeCode: number;
maxContentWidth: number;
minTocHeadings: number;
eraseUnusedAttachmentsAfterSeconds: number;
eraseUnusedAttachmentsAfterTimeScale: number;
firstDayOfWeek: number;
initialized: boolean;
@@ -93,6 +97,10 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
codeBlockWordWrap: boolean;
textNoteEditorMultilineToolbar: boolean;
backgroundEffects: boolean;
// Share settings
redirectBareDomain: boolean;
showLoginInShareTheme: boolean;
}
export type OptionNames = keyof OptionDefinitions;
+8 -8
View File
@@ -22,7 +22,7 @@ describe("Search", () => {
});
});
it.skip("simple path match", () => {
it("simple path match", () => {
rootNote.child(note("Europe").child(note("Austria")));
const searchContext = new SearchContext();
@@ -32,7 +32,7 @@ describe("Search", () => {
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it.skip("normal search looks also at attributes", () => {
it("normal search looks also at attributes", () => {
const austria = note("Austria");
const vienna = note("Vienna");
@@ -50,7 +50,7 @@ describe("Search", () => {
expect(findNoteByTitle(searchResults, "Vienna")).toBeTruthy();
});
it.skip("normal search looks also at type and mime", () => {
it("normal search looks also at type and mime", () => {
rootNote.child(note("Effective Java", { type: "book", mime: "" })).child(note("Hello World.java", { type: "code", mime: "text/x-java" }));
const searchContext = new SearchContext();
@@ -69,7 +69,7 @@ describe("Search", () => {
expect(searchResults.length).toEqual(2);
});
it.skip("only end leafs are results", () => {
it("only end leafs are results", () => {
rootNote.child(note("Europe").child(note("Austria")));
const searchContext = new SearchContext();
@@ -79,7 +79,7 @@ describe("Search", () => {
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
});
it.skip("only end leafs are results", () => {
it("only end leafs are results", () => {
rootNote.child(note("Europe").child(note("Austria").label("capital", "Vienna")));
const searchContext = new SearchContext();
@@ -132,7 +132,7 @@ describe("Search", () => {
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it.skip("inherited label comparison", () => {
it("inherited label comparison", () => {
rootNote.child(note("Europe").label("country", "", true).child(note("Austria")).child(note("Czech Republic")));
const searchContext = new SearchContext();
@@ -527,7 +527,7 @@ describe("Search", () => {
expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe");
});
it.skip("test note.text *=* something", () => {
it("test note.text *=* something", () => {
const italy = note("Italy").label("capital", "Rome");
const slovakia = note("Slovakia").label("capital", "Bratislava");
@@ -540,7 +540,7 @@ describe("Search", () => {
expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia");
});
it.skip("test that fulltext does not match archived notes", () => {
it("test that fulltext does not match archived notes", () => {
const italy = note("Italy").label("capital", "Rome");
const slovakia = note("Slovakia").label("capital", "Bratislava");
+28
View File
@@ -0,0 +1,28 @@
import becca from "../becca/becca.js";
import BTask from "../becca/entities/btask.js";
export function getTasks(parentNoteId: string) {
return becca.getTasks()
.filter((task) => task.parentNoteId === parentNoteId && !task.isDone);
}
interface CreateTaskParams {
parentNoteId: string;
title: string;
dueDate?: string;
}
export function createNewTask(params: CreateTaskParams) {
const task = new BTask(params);
task.save();
return {
task
}
}
export function toggleTaskDone(taskId: string) {
const task = becca.tasks[taskId];
task.isDone = !task.isDone;
task.save();
}
+3 -6
View File
@@ -167,12 +167,12 @@ describe("#getContentDisposition", () => {
const defaultFallBackDisposition = `file; filename="file"; filename*=UTF-8''file`;
const testCases: TestCase<typeof utils.getContentDisposition>[] = [
[
"when passed filename is empty, it should fallback to default value 'file'",
"when passed filename is empty, it should fallback to default value 'file'",
[" "],
defaultFallBackDisposition
],
[
"when passed filename '..' would cause sanitized filename to be empty, it should fallback to default value 'file'",
"when passed filename '..' would cause sanitized filename to be empty, it should fallback to default value 'file'",
[".."],
defaultFallBackDisposition
],
@@ -304,19 +304,16 @@ describe("#getNoteTitle", () => {
],
[
"when a noteMeta object is passed, it should use the title from the noteMeta, if present",
//@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
["test_file.md", true, { title: "some other title"}],
"some other title"
],
[
"when a noteMeta object is passed, but the title prop is empty, it should try to handle the filename as if no noteMeta was passed",
//@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
["test_file.md", true, { title: ""}],
"test file"
],
[
"when a noteMeta object is passed, but the title prop is empty, it should try to handle the filename as if no noteMeta was passed",
//@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
["test_file.json", false, { title: " "}],
"test_file.json"
]
@@ -627,4 +624,4 @@ describe("#formatDownloadTitle", () => {
expect(actual).toStrictEqual(expected);
});
});
});
});
+40 -2
View File
@@ -1,5 +1,7 @@
"use strict";
import chardet from "chardet";
import stripBom from "strip-bom";
import crypto from "crypto";
import { generator } from "rand-token";
import unescape from "unescape";
@@ -10,6 +12,8 @@ import path from "path";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import type NoteMeta from "./meta/note_meta.js";
import log from "./log.js";
import { t } from "i18next";
const randtoken = generator({ source: "crypto" });
@@ -105,10 +109,13 @@ export function escapeRegExp(str: string) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
export async function crash() {
export async function crash(message: string) {
if (isElectron) {
(await import("electron")).app.exit(1);
const electron = await import("electron");
electron.dialog.showErrorBox(t("modals.error_title"), message);
electron.app.exit(1);
} else {
log.error(message);
process.exit(1);
}
}
@@ -168,6 +175,7 @@ export function removeTextFileExtension(filePath: string) {
switch (extension) {
case ".md":
case ".mdx":
case ".markdown":
case ".html":
case ".htm":
@@ -324,6 +332,36 @@ function compareVersions(v1: string, v2: string): number {
return 0;
}
/**
* For buffers, they are scanned for a supported encoding and decoded (UTF-8, UTF-16). In some cases, the BOM is also stripped.
*
* For strings, they are returned immediately without any transformation.
*
* For nullish values, an empty string is returned.
*
* @param data the string or buffer to process.
* @returns the string representation of the buffer, or the same string is it's a string.
*/
export function processStringOrBuffer(data: string | Buffer | null) {
if (!data) {
return "";
}
if (!Buffer.isBuffer(data)) {
return data;
}
const detectedEncoding = chardet.detect(data);
console.log("Detected as ", detectedEncoding);
switch (detectedEncoding) {
case "UTF-16LE":
return stripBom(data.toString("utf-16le"));
case "UTF-8":
default:
return data.toString("utf-8");
}
}
export default {
compareVersions,
crash,
+6
View File
@@ -188,6 +188,12 @@ function fillInAdditionalProperties(entityChange: EntityChange) {
WHERE attachmentId = ?`,
[entityChange.entityId]
);
} else if (entityChange.entityName === "tasks") {
entityChange.entity = becca.getTask(entityChange.entity);
if (!entityChange.entity) {
entityChange.entity = sql.getRow(`SELECT * FROM tasks WHERE taskId = ?`, [entityChange.entityId]);
}
}
if (entityChange.entity instanceof AbstractBeccaEntity) {