mirror of
https://github.com/TriliumNext/Notes.git
synced 2026-05-07 04:39:13 -05:00
Merge remote-tracking branch 'origin/develop' into port/client_ts
This commit is contained in:
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
buildDate: "2024-09-07T18:36:34Z",
|
||||
buildRevision: "7c0d6930fa8f20d269dcfbcbc8f636a25f6bb9a7"
|
||||
buildDate: "2025-02-22T11:59:41Z",
|
||||
buildRevision: "60da36757009d908ed93a394d030b18b3eab4d98"
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(", ")}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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.
@@ -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");
|
||||
});
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
})
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user