mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 19:05:59 +01:00
Merge remote-tracking branch 'origin/develop' into feat/add-rootless-dockerfiles
This commit is contained in:
@@ -137,7 +137,7 @@ export default async function buildApp() {
|
||||
startScheduledCleanup();
|
||||
|
||||
if (utils.isElectron) {
|
||||
(await import("@electron/remote/main")).initialize();
|
||||
(await import("@electron/remote/main/index.js")).initialize();
|
||||
}
|
||||
|
||||
return app;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<body>
|
||||
<div class="container login-page">
|
||||
<div class="col-xs-12 col-sm-10 col-md-6 col-lg-4 col-xl-4 mx-auto pt-4">
|
||||
<img class="img-fluid d-block mx-auto" style="height: 8rem;" src="<%= assetPath %>/images/icon-color.svg" aria-hidden="true" draggable="false" >
|
||||
<img class="img-fluid d-block mx-auto" style="height: 8rem;" src="<%= assetPathFragment %>/images/icon-color.svg" aria-hidden="true" draggable="false" >
|
||||
<h1 class="text-center"><%= t("login.heading") %></h1>
|
||||
|
||||
<% if (ssoEnabled) { %>
|
||||
|
||||
@@ -36,7 +36,7 @@ interface DateLimits {
|
||||
maxDate: string;
|
||||
}
|
||||
|
||||
interface SimilarNote {
|
||||
export interface SimilarNote {
|
||||
score: number;
|
||||
notePath: string[];
|
||||
noteId: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import etapiTokenService from "../services/etapi_tokens.js";
|
||||
import config from "../services/config.js";
|
||||
import type { NextFunction, Request, RequestHandler, Response, Router } from "express";
|
||||
import type { ValidatorMap } from "./etapi-interface.js";
|
||||
import type { ApiRequestHandler } from "../routes/routes.js";
|
||||
import type { ApiRequestHandler } from "../routes/route_api.js";
|
||||
const GENERIC_CODE = "GENERIC";
|
||||
|
||||
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
|
||||
|
||||
@@ -116,7 +116,7 @@ function getLinkMap(req: Request) {
|
||||
}).notes;
|
||||
}
|
||||
|
||||
const noteIds = new Set(unfilteredNotes.filter((note) => ignoreExcludeFromNoteMap || !note.isLabelTruthy("excludeFromNoteMap")).map((note) => note.noteId));
|
||||
const noteIds = new Set<string>(unfilteredNotes.filter((note) => ignoreExcludeFromNoteMap || !note.isLabelTruthy("excludeFromNoteMap")).map((note) => note.noteId));
|
||||
|
||||
if (mapRootNote.type === "search") {
|
||||
noteIds.delete(mapRootNote.noteId);
|
||||
|
||||
@@ -118,7 +118,7 @@ function createNote(req: Request) {
|
||||
throw new ValidationError("Missing or incorrect type for target branch ID.");
|
||||
}
|
||||
|
||||
const { note, branch } = noteService.createNewNoteWithTarget(target, targetBranchId, params);
|
||||
const { note, branch } = noteService.createNewNoteWithTarget(target, String(targetBranchId), params);
|
||||
|
||||
return {
|
||||
note,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { assetUrlFragment } from "../services/asset_path.js";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import express from "express";
|
||||
import { getResourceDir, isDev } from "../services/utils.js";
|
||||
import type serveStatic from "serve-static";
|
||||
import proxy from "express-http-proxy";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
|
||||
if (!isDev) {
|
||||
@@ -17,7 +17,7 @@ const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOp
|
||||
};
|
||||
|
||||
async function register(app: express.Application) {
|
||||
const srcRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const srcRoot = path.join(__dirname, "..");
|
||||
const resourceDir = getResourceDir();
|
||||
|
||||
if (isDev) {
|
||||
@@ -29,14 +29,19 @@ async function register(app: express.Application) {
|
||||
proxyReqPathResolver: (req) => "/" + assetUrlFragment + `/@fs` + req.url
|
||||
}));
|
||||
} else {
|
||||
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(resourceDir, "public", "src")));
|
||||
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(resourceDir, "public", "stylesheets")));
|
||||
app.use(`/${assetUrlFragment}/libraries`, persistentCacheStatic(path.join(resourceDir, "public", "libraries")));
|
||||
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(resourceDir, "public", "fonts")));
|
||||
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(resourceDir, "public", "translations")));
|
||||
app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
|
||||
app.use(`/node_modules/`, persistentCacheStatic(path.join(resourceDir, "public/node_modules")));
|
||||
const publicDir = path.join(resourceDir, "public");
|
||||
if (!existsSync(publicDir)) {
|
||||
throw new Error("Public directory is missing at: " + path.resolve(publicDir));
|
||||
}
|
||||
|
||||
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(publicDir, "src")));
|
||||
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(publicDir, "stylesheets")));
|
||||
app.use(`/${assetUrlFragment}/libraries`, persistentCacheStatic(path.join(publicDir, "libraries")));
|
||||
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(publicDir, "fonts")));
|
||||
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(publicDir, "translations")));
|
||||
app.use(`/node_modules/`, persistentCacheStatic(path.join(publicDir, "node_modules")));
|
||||
}
|
||||
app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
|
||||
app.use(`/${assetUrlFragment}/doc_notes`, persistentCacheStatic(path.join(resourceDir, "assets", "doc_notes")));
|
||||
app.use(`/assets/vX/fonts`, express.static(path.join(srcRoot, "public/fonts")));
|
||||
app.use(`/assets/vX/images`, express.static(path.join(srcRoot, "..", "images")));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ipcMain } from "electron";
|
||||
import electron from "electron";
|
||||
|
||||
interface Response {
|
||||
statusCode: number;
|
||||
@@ -10,7 +10,7 @@ interface Response {
|
||||
}
|
||||
|
||||
function init(app: Express.Application) {
|
||||
ipcMain.on("server-request", (event, arg) => {
|
||||
electron.ipcMain.on("server-request", (event, arg) => {
|
||||
const req = {
|
||||
url: arg.url,
|
||||
method: arg.method,
|
||||
@@ -50,7 +50,7 @@ function init(app: Express.Application) {
|
||||
}
|
||||
};
|
||||
|
||||
return app._router.handle(req, res, () => {});
|
||||
return (app as any)._router.handle(req, res, () => {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import optionService from "../services/options.js";
|
||||
import myScryptService from "../services/encryption/my_scrypt.js";
|
||||
import log from "../services/log.js";
|
||||
import passwordService from "../services/encryption/password.js";
|
||||
import assetPath from "../services/asset_path.js";
|
||||
import assetPath, { assetUrlFragment } from "../services/asset_path.js";
|
||||
import appPath from "../services/app_path.js";
|
||||
import ValidationError from "../errors/validation_error.js";
|
||||
import type { Request, Response } from 'express';
|
||||
@@ -13,12 +13,14 @@ import openID from '../services/open_id.js';
|
||||
import openIDEncryption from '../services/encryption/open_id_encryption.js';
|
||||
|
||||
function loginPage(req: Request, res: Response) {
|
||||
// Login page is triggered twice. Once here, and another time if the password is failed.
|
||||
res.render('login', {
|
||||
wrongPassword: false,
|
||||
wrongTotp: false,
|
||||
totpEnabled: totp.isTotpEnabled(),
|
||||
ssoEnabled: openID.isOpenIDEnabled(),
|
||||
assetPath: assetPath,
|
||||
assetPathFragment: assetUrlFragment,
|
||||
appPath: appPath,
|
||||
});
|
||||
}
|
||||
@@ -169,6 +171,7 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to
|
||||
totpEnabled: totp.isTotpEnabled(),
|
||||
ssoEnabled: openID.isOpenIDEnabled(),
|
||||
assetPath: assetPath,
|
||||
assetPathFragment: assetUrlFragment,
|
||||
appPath: appPath,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import express from "express";
|
||||
import express, { type RequestHandler } from "express";
|
||||
import multer from "multer";
|
||||
import log from "../services/log.js";
|
||||
import cls from "../services/cls.js";
|
||||
@@ -166,7 +166,7 @@ function handleException(e: unknown | Error, method: HttpMethod, path: string, r
|
||||
|
||||
}
|
||||
|
||||
export function createUploadMiddleware() {
|
||||
export function createUploadMiddleware(): RequestHandler {
|
||||
const multerOptions: multer.Options = {
|
||||
fileFilter: (req: express.Request, file, cb) => {
|
||||
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
|
||||
|
||||
@@ -3,6 +3,7 @@ import session, { Store } from "express-session";
|
||||
import sessionSecret from "../services/session_secret.js";
|
||||
import config from "../services/config.js";
|
||||
import log from "../services/log.js";
|
||||
import type express from "express";
|
||||
|
||||
class SQLiteSessionStore extends Store {
|
||||
|
||||
@@ -51,7 +52,7 @@ class SQLiteSessionStore extends Store {
|
||||
|
||||
}
|
||||
|
||||
const sessionParser = session({
|
||||
const sessionParser: express.RequestHandler = session({
|
||||
secret: sessionSecret,
|
||||
resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request.
|
||||
saveUninitialized: false, // true forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import BUILTIN_ATTRIBUTES from "./builtin_attributes.js";
|
||||
import fs from "fs-extra";
|
||||
import fs from "fs";
|
||||
import dataDir from "./data_dir.js";
|
||||
import dateUtils from "./date_utils.js";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
@@ -224,14 +224,14 @@ export interface Api {
|
||||
* @param date in YYYY-MM-DD format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getDayNote(date: string, rootNote?: BNote): Promise<BNote | null>;
|
||||
getDayNote(date: string, rootNote?: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns today's day note. If such note doesn't exist, it is created.
|
||||
*
|
||||
* @param rootNote specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getTodayNote(rootNote?: BNote): Promise<BNote | null>;
|
||||
getTodayNote(rootNote?: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns note for the first date of the week of the given date.
|
||||
@@ -239,7 +239,7 @@ export interface Api {
|
||||
* @param date in YYYY-MM-DD format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getWeekFirstDayNote(date: string, rootNote: BNote): Promise<BNote | null>;
|
||||
getWeekFirstDayNote(date: string, rootNote: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns week note for given date. If such a note doesn't exist, it is created.
|
||||
@@ -247,7 +247,7 @@ export interface Api {
|
||||
* @param date in YYYY-MM-DD format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getWeekNote(date: string, rootNote: BNote): Promise<BNote | null>;
|
||||
getWeekNote(date: string, rootNote: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns month note for given date. If such a note doesn't exist, it is created.
|
||||
@@ -255,7 +255,7 @@ export interface Api {
|
||||
* @param date in YYYY-MM format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getMonthNote(date: string, rootNote: BNote): Promise<BNote | null>;
|
||||
getMonthNote(date: string, rootNote: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns quarter note for given date. If such a note doesn't exist, it is created.
|
||||
@@ -263,7 +263,7 @@ export interface Api {
|
||||
* @param date in YYYY-MM format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getQuarterNote(date: string, rootNote: BNote): Promise<BNote | null>;
|
||||
getQuarterNote(date: string, rootNote: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns year note for given year. If such a note doesn't exist, it is created.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import dateUtils from "./date_utils.js";
|
||||
import optionService from "./options.js";
|
||||
import fs from "fs-extra";
|
||||
import fs from "fs";
|
||||
import dataDir from "./data_dir.js";
|
||||
import log from "./log.js";
|
||||
import syncMutexService from "./sync_mutex.js";
|
||||
|
||||
@@ -4,19 +4,26 @@ import sql_init from "./sql_init.js";
|
||||
import { join } from "path";
|
||||
import { getResourceDir } from "./utils.js";
|
||||
import hidden_subtree from "./hidden_subtree.js";
|
||||
import { LOCALES, type Locale } from "@triliumnext/commons";
|
||||
import { LOCALES, type Locale, type LOCALE_IDS } from "@triliumnext/commons";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
|
||||
const DAYJS_LOCALE_MAP: Record<string, string> = {
|
||||
cn: "zh-cn",
|
||||
tw: "zh-tw"
|
||||
};
|
||||
|
||||
let dayjsLocale: string;
|
||||
const DAYJS_LOADER: Record<LOCALE_IDS, () => Promise<typeof import("dayjs/locale/en.js")>> = {
|
||||
"ar": () => import("dayjs/locale/ar.js"),
|
||||
"cn": () => import("dayjs/locale/zh-cn.js"),
|
||||
"de": () => import("dayjs/locale/de.js"),
|
||||
"en": () => import("dayjs/locale/en.js"),
|
||||
"es": () => import("dayjs/locale/es.js"),
|
||||
"fa": () => import("dayjs/locale/fa.js"),
|
||||
"fr": () => import("dayjs/locale/fr.js"),
|
||||
"he": () => import("dayjs/locale/he.js"),
|
||||
"ku": () => import("dayjs/locale/ku.js"),
|
||||
"ro": () => import("dayjs/locale/ro.js"),
|
||||
"tw": () => import("dayjs/locale/zh-tw.js")
|
||||
}
|
||||
|
||||
export async function initializeTranslations() {
|
||||
const resourceDir = getResourceDir();
|
||||
const Backend = (await import("i18next-fs-backend")).default;
|
||||
const Backend = (await import("i18next-fs-backend/cjs")).default;
|
||||
const locale = getCurrentLanguage();
|
||||
|
||||
// Initialize translations
|
||||
@@ -30,12 +37,7 @@ export async function initializeTranslations() {
|
||||
});
|
||||
|
||||
// Initialize dayjs locale.
|
||||
dayjsLocale = DAYJS_LOCALE_MAP[locale] ?? locale;
|
||||
try {
|
||||
await import(`dayjs/locale/${dayjsLocale}.js`);
|
||||
} catch (err) {
|
||||
console.warn(`Could not load locale ${dayjsLocale}`, err);
|
||||
}
|
||||
const dayjsLocale = await DAYJS_LOADER[locale]();
|
||||
dayjs.locale(dayjsLocale);
|
||||
}
|
||||
|
||||
@@ -48,8 +50,8 @@ export function getLocales(): Locale[] {
|
||||
return LOCALES;
|
||||
}
|
||||
|
||||
function getCurrentLanguage() {
|
||||
let language;
|
||||
function getCurrentLanguage(): LOCALE_IDS {
|
||||
let language: string;
|
||||
if (sql_init.isDbInitialized()) {
|
||||
language = options.getOptionOrNull("locale");
|
||||
}
|
||||
@@ -59,7 +61,7 @@ function getCurrentLanguage() {
|
||||
language = "en";
|
||||
}
|
||||
|
||||
return language;
|
||||
return language as LOCALE_IDS;
|
||||
}
|
||||
|
||||
export async function changeLanguage(locale: string) {
|
||||
|
||||
@@ -194,9 +194,43 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("converts multi-line display math expressions into Mathtex format", () => {
|
||||
const input = `$$
|
||||
\\sqrt{x^{2}+1} \\
|
||||
+ \\frac{1}{2}
|
||||
$$`;
|
||||
const expected = /*html*/`<span class="math-tex">\\[
|
||||
\\sqrt{x^{2}+1} \\
|
||||
+ \\frac{1}{2}
|
||||
\\]</span>`;
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("ignores math formulas inside code blocks and converts inline math expressions correctly", () => {
|
||||
const result = markdownService.renderToHtml(trimIndentation`\
|
||||
\`\`\`unknownlanguage
|
||||
$$a+b$$
|
||||
\`\`\`
|
||||
`, "title");
|
||||
expect(result).toBe(trimIndentation`\
|
||||
<pre><code class="language-text-x-trilium-auto">$$a+b$$</code></pre>`);
|
||||
});
|
||||
|
||||
it("converts specific inline math expression into Mathtex format", () => {
|
||||
const input = `This is a formula: $\\mathcal{L}_{task} + \\mathcal{L}_{od}$ inside a sentence.`;
|
||||
const expected = /*html*/`<p>This is a formula: <span class="math-tex">\\(\\mathcal{L}_{task} + \\mathcal{L}_{od}\\)</span> inside a sentence.</p>`;
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("converts math expressions inside list items into Mathtex format", () => {
|
||||
const input = `- First item with formula: $E = mc^2$`;
|
||||
const expected = /*html*/`<ul><li>First item with formula: <span class="math-tex">\\(E = mc^2\\)</span></li></ul>`;
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("converts display math expressions into Mathtex format", () => {
|
||||
const input = `$$\sqrt{x^{2}+1}$$`;
|
||||
const expected = /*html*/`<p><span class="math-tex">\\[\sqrt{x^{2}+1}\\]</span></p>`;
|
||||
const expected = /*html*/`<span class="math-tex">\\[\sqrt{x^{2}+1}\\]</span>`;
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -240,7 +274,7 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li
|
||||
});
|
||||
|
||||
it("imports todo lists properly", () => {
|
||||
const input = trimIndentation`\
|
||||
const input = trimIndentation`\
|
||||
- [x] Hello
|
||||
- [ ] World`;
|
||||
const expected = `<ul class="todo-list"><li><label class="todo-list__label"><input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">Hello</span></label></li><li><label class="todo-list__label"><input type="checkbox" disabled="disabled"><span class="todo-list__label__description">World</span></label></li></ul>`;
|
||||
|
||||
@@ -23,19 +23,7 @@ class CustomMarkdownRenderer extends Renderer {
|
||||
}
|
||||
|
||||
paragraph(data: Tokens.Paragraph): string {
|
||||
let text = super.paragraph(data).trimEnd();
|
||||
|
||||
if (text.includes("$")) {
|
||||
// Display math
|
||||
text = text.replaceAll(/(?<!\\)\$\$(.+)\$\$/g,
|
||||
`<span class="math-tex">\\\[$1\\\]</span>`);
|
||||
|
||||
// Inline math
|
||||
text = text.replaceAll(/(?<!\\)\$(.+?)\$/g,
|
||||
`<span class="math-tex">\\\($1\\\)</span>`);
|
||||
}
|
||||
|
||||
return text;
|
||||
return super.paragraph(data).trimEnd();
|
||||
}
|
||||
|
||||
code({ text, lang }: Tokens.Code): string {
|
||||
@@ -133,11 +121,17 @@ function renderToHtml(content: string, title: string) {
|
||||
// Double-escape slashes in math expression because they are otherwise consumed by the parser somewhere.
|
||||
content = content.replaceAll("\\$", "\\\\$");
|
||||
|
||||
let html = parse(content, {
|
||||
// Extract formulas and replace them with placeholders to prevent interference from Markdown rendering
|
||||
const { processedText, placeholderMap: formulaMap } = extractFormulas(content);
|
||||
|
||||
let html = parse(processedText, {
|
||||
async: false,
|
||||
renderer: renderer
|
||||
}) as string;
|
||||
|
||||
// After rendering, replace placeholders back with the formula HTML
|
||||
html = restoreFromMap(html, formulaMap);
|
||||
|
||||
// h1 handling needs to come before sanitization
|
||||
html = importUtils.handleH1(html, title);
|
||||
html = htmlSanitizer.sanitize(html);
|
||||
@@ -165,6 +159,59 @@ function getNormalizedMimeFromMarkdownLanguage(language: string | undefined) {
|
||||
return MIME_TYPE_AUTO;
|
||||
}
|
||||
|
||||
function extractCodeBlocks(text: string): { processedText: string, placeholderMap: Map<string, string> } {
|
||||
const codeMap = new Map<string, string>();
|
||||
let id = 0;
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Multi-line code block and Inline code
|
||||
text = text.replace(/```[\s\S]*?```/g, (m) => {
|
||||
const key = `<!--CODE_BLOCK_${timestamp}_${id++}-->`;
|
||||
codeMap.set(key, m);
|
||||
return key;
|
||||
}).replace(/`[^`\n]+`/g, (m) => {
|
||||
const key = `<!--INLINE_CODE_${timestamp}_${id++}-->`;
|
||||
codeMap.set(key, m);
|
||||
return key;
|
||||
});
|
||||
|
||||
return { processedText: text, placeholderMap: codeMap };
|
||||
}
|
||||
|
||||
function extractFormulas(text: string): { processedText: string, placeholderMap: Map<string, string> } {
|
||||
// Protect the $ signs inside code blocks from being recognized as formulas.
|
||||
const { processedText: noCodeText, placeholderMap: codeMap } = extractCodeBlocks(text);
|
||||
|
||||
const formulaMap = new Map<string, string>();
|
||||
let id = 0;
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Display math and Inline math
|
||||
let processedText = noCodeText.replace(/(?<!\\)\$\$((?:(?!\n{2,})[\s\S])+?)\$\$/g, (_, formula) => {
|
||||
const key = `<!--FORMULA_BLOCK_${timestamp}_${id++}-->`;
|
||||
const rendered = `<span class="math-tex">\\[${formula}\\]</span>`;
|
||||
formulaMap.set(key, rendered);
|
||||
return key;
|
||||
}).replace(/(?<!\\)\$(.+?)\$/g, (_, formula) => {
|
||||
const key = `<!--FORMULA_INLINE_${timestamp}_${id++}-->`;
|
||||
const rendered = `<span class="math-tex">\\(${formula}\\)</span>`;
|
||||
formulaMap.set(key, rendered);
|
||||
return key;
|
||||
});
|
||||
|
||||
processedText = restoreFromMap(processedText, codeMap);
|
||||
|
||||
return { processedText, placeholderMap: formulaMap };
|
||||
}
|
||||
|
||||
function restoreFromMap(text: string, map: Map<string, string>): string {
|
||||
if (map.size === 0) return text;
|
||||
const pattern = [...map.keys()]
|
||||
.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
.join('|');
|
||||
return text.replace(new RegExp(pattern, 'g'), match => map.get(match) ?? match);
|
||||
}
|
||||
|
||||
const renderer = new CustomMarkdownRenderer({ async: false });
|
||||
|
||||
export default {
|
||||
|
||||
@@ -26,7 +26,7 @@ import type {
|
||||
*
|
||||
* Manages and provides access to all available agent tools.
|
||||
*/
|
||||
class AgentToolsManager {
|
||||
export class AgentToolsManager {
|
||||
private vectorSearchTool: VectorSearchTool | null = null;
|
||||
private noteNavigatorTool: NoteNavigatorTool | null = null;
|
||||
private queryDecompositionTool: QueryDecompositionTool | null = null;
|
||||
|
||||
@@ -16,14 +16,12 @@ import vectorStore from "./embeddings/index.js";
|
||||
import providerManager from "./providers/providers.js";
|
||||
import { ContextExtractor } from "./context/index.js";
|
||||
import eventService from "../events.js";
|
||||
import type { NoteEmbeddingContext } from "./embeddings/embeddings_interface.js";
|
||||
import type { OptionDefinitions } from "@triliumnext/commons";
|
||||
import sql from "../sql.js";
|
||||
import sqlInit from "../sql_init.js";
|
||||
import { CONTEXT_PROMPTS } from './constants/llm_prompt_constants.js';
|
||||
import { SEARCH_CONSTANTS } from './constants/search_constants.js';
|
||||
|
||||
class IndexService {
|
||||
export class IndexService {
|
||||
private initialized = false;
|
||||
private indexingInProgress = false;
|
||||
private contextExtractor = new ContextExtractor();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import backupService from "./backup.js";
|
||||
import sql from "./sql.js";
|
||||
import fs from "fs-extra";
|
||||
import fs from "fs";
|
||||
import log from "./log.js";
|
||||
import { crash } from "./utils.js";
|
||||
import resourceDir from "./resource_dir.js";
|
||||
|
||||
@@ -207,7 +207,7 @@ async function getClient(opts: ClientOpts): Promise<Client> {
|
||||
// it's not clear how to explicitly configure proxy (as opposed to system proxy),
|
||||
// so in that case, we always use node's modules
|
||||
if (isElectron && !opts.proxy) {
|
||||
return (await import("electron")).net as Client;
|
||||
return (await import("electron")).net as unknown as Client;
|
||||
} else {
|
||||
const { protocol } = url.parse(opts.url);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import becca from "../becca/becca.js";
|
||||
import type BNote from "../becca/entities/bnote.js";
|
||||
import type { ApiParams } from "./backend_script_api_interface.js";
|
||||
|
||||
interface Bundle {
|
||||
export interface Bundle {
|
||||
note?: BNote;
|
||||
noteId?: string;
|
||||
script: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ import log from "./log.js";
|
||||
import type { Statement, Database as DatabaseType, RunResult } from "better-sqlite3";
|
||||
import dataDir from "./data_dir.js";
|
||||
import cls from "./cls.js";
|
||||
import fs from "fs-extra";
|
||||
import fs from "fs";
|
||||
import Database from "better-sqlite3";
|
||||
import ws from "./ws.js";
|
||||
import becca_loader from "../becca/becca_loader.js";
|
||||
@@ -352,7 +352,6 @@ function disableSlowQueryLogging<T>(cb: () => T) {
|
||||
}
|
||||
|
||||
export default {
|
||||
dbConnection,
|
||||
insert,
|
||||
replace,
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BrowserWindow, Menu, Tray, ipcMain, nativeTheme } from "electron";
|
||||
import electron from "electron";
|
||||
import type { BrowserWindow, Tray } from "electron";
|
||||
import { default as i18next, t } from "i18next";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import becca from "../becca/becca.js";
|
||||
import becca_service from "../becca/becca_service.js";
|
||||
@@ -33,7 +33,7 @@ function getTrayIconPath() {
|
||||
}
|
||||
|
||||
function getIconPath(name: string) {
|
||||
const suffix = !isMac && nativeTheme.shouldUseDarkColors ? "-inverted" : "";
|
||||
const suffix = !isMac && electron.nativeTheme.shouldUseDarkColors ? "-inverted" : "";
|
||||
return path.resolve(path.join(getResourceDir(), "assets", "images", "tray", `${name}Template${suffix}.png`));
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ function updateTrayMenu() {
|
||||
}
|
||||
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
const contextMenu = electron.Menu.buildFromTemplate([
|
||||
...windowVisibilityMenuItems,
|
||||
{ type: "separator" },
|
||||
{
|
||||
@@ -255,7 +255,7 @@ function updateTrayMenu() {
|
||||
type: "normal",
|
||||
icon: getIconPath("close"),
|
||||
click: () => {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
const windows = electron.BrowserWindow.getAllWindows();
|
||||
windows.forEach(window => {
|
||||
window.close();
|
||||
});
|
||||
@@ -287,7 +287,7 @@ function createTray() {
|
||||
return;
|
||||
}
|
||||
|
||||
tray = new Tray(getTrayIconPath());
|
||||
tray = new electron.Tray(getTrayIconPath());
|
||||
tray.setToolTip(t("tray.tooltip"));
|
||||
// Restore focus
|
||||
tray.on("click", changeVisibility);
|
||||
@@ -295,9 +295,9 @@ function createTray() {
|
||||
|
||||
if (!isMac) {
|
||||
// macOS uses template icons which work great on dark & light themes.
|
||||
nativeTheme.on("updated", updateTrayMenu);
|
||||
electron.nativeTheme.on("updated", updateTrayMenu);
|
||||
}
|
||||
ipcMain.on("reload-tray", updateTrayMenu);
|
||||
electron.ipcMain.on("reload-tray", updateTrayMenu);
|
||||
i18next.on("languageChanged", updateTrayMenu);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import entityChangesService from "./entity_changes.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import type BNote from "../becca/entities/bnote.js";
|
||||
|
||||
interface ValidationResponse {
|
||||
export interface ValidationResponse {
|
||||
branch: BBranch | null;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
|
||||
@@ -9,8 +9,6 @@ import escape from "escape-html";
|
||||
import sanitize from "sanitize-filename";
|
||||
import mimeTypes from "mime-types";
|
||||
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";
|
||||
@@ -226,7 +224,7 @@ export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?
|
||||
});
|
||||
}
|
||||
|
||||
interface DeferredPromise<T> extends Promise<T> {
|
||||
export interface DeferredPromise<T> extends Promise<T> {
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
@@ -299,7 +297,7 @@ export function getResourceDir() {
|
||||
return path.dirname(process.argv[1]);
|
||||
}
|
||||
|
||||
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
return path.join(__dirname, "..");
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with src/public/app/services/utils.ts
|
||||
|
||||
@@ -7,14 +7,11 @@ import log from "./log.js";
|
||||
import sqlInit from "./sql_init.js";
|
||||
import cls from "./cls.js";
|
||||
import keyboardActionsService from "./keyboard_actions.js";
|
||||
import * as remoteMain from "@electron/remote/main";
|
||||
import { BrowserWindow, shell, type App, type BrowserWindowConstructorOptions, type WebContents } from "electron";
|
||||
import { dialog, ipcMain } from "electron";
|
||||
import electron from "electron";
|
||||
import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents } from "electron";
|
||||
import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
import { t } from "i18next";
|
||||
import { RESOURCE_DIR } from "./resource_dir.js";
|
||||
|
||||
// Prevent the window being garbage collected
|
||||
let mainWindow: BrowserWindow | null;
|
||||
@@ -28,14 +25,14 @@ function trackWindowFocus(win: BrowserWindow) {
|
||||
allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win);
|
||||
allWindows.push(win);
|
||||
if (!optionService.getOptionBool("disableTray")) {
|
||||
ipcMain.emit("reload-tray");
|
||||
electron.ipcMain.emit("reload-tray");
|
||||
}
|
||||
});
|
||||
|
||||
win.on("closed", () => {
|
||||
allWindows = allWindows.filter(w => !w.isDestroyed());
|
||||
if (!optionService.getOptionBool("disableTray")) {
|
||||
ipcMain.emit("reload-tray");
|
||||
electron.ipcMain.emit("reload-tray");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -66,7 +63,7 @@ async function createExtraWindow(extraWindowHash: string) {
|
||||
trackWindowFocus(win);
|
||||
}
|
||||
|
||||
ipcMain.on("create-extra-window", (event, arg) => {
|
||||
electron.ipcMain.on("create-extra-window", (event, arg) => {
|
||||
createExtraWindow(arg.extraWindowHash);
|
||||
});
|
||||
|
||||
@@ -76,13 +73,13 @@ interface ExportAsPdfOpts {
|
||||
pageSize: "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Legal" | "Letter" | "Tabloid" | "Ledger";
|
||||
}
|
||||
|
||||
ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
|
||||
const browserWindow = BrowserWindow.fromWebContents(e.sender);
|
||||
electron.ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
|
||||
const browserWindow = electron.BrowserWindow.fromWebContents(e.sender);
|
||||
if (!browserWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = dialog.showSaveDialogSync(browserWindow, {
|
||||
const filePath = electron.dialog.showSaveDialogSync(browserWindow, {
|
||||
defaultPath: formatDownloadTitle(opts.title, "file", "application/pdf"),
|
||||
filters: [
|
||||
{
|
||||
@@ -111,18 +108,18 @@ ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
|
||||
`
|
||||
});
|
||||
} catch (e) {
|
||||
dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, buffer);
|
||||
} catch (e) {
|
||||
dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
shell.openPath(filePath);
|
||||
electron.shell.openPath(filePath);
|
||||
});
|
||||
|
||||
async function createMainWindow(app: App) {
|
||||
@@ -226,7 +223,8 @@ function getWindowExtraOpts() {
|
||||
return extraOpts;
|
||||
}
|
||||
|
||||
function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
|
||||
async function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
|
||||
const remoteMain = (await import("@electron/remote/main/index.js")).default;
|
||||
remoteMain.enable(webContents);
|
||||
|
||||
webContents.setWindowOpenHandler((details) => {
|
||||
@@ -259,7 +257,7 @@ function configureWebContents(webContents: WebContents, spellcheckEnabled: boole
|
||||
}
|
||||
|
||||
function getIcon() {
|
||||
return path.join(dirname(fileURLToPath(import.meta.url)), "../../images/app-icons/png/256x256" + (isDev ? "-dev" : "") + ".png");
|
||||
return path.join(RESOURCE_DIR, "images/app-icons/png/256x256" + (isDev ? "-dev" : "") + ".png");
|
||||
}
|
||||
|
||||
async function createSetupWindow() {
|
||||
|
||||
@@ -195,9 +195,11 @@ function register(router: Router) {
|
||||
try {
|
||||
const content = templateNote.getContent();
|
||||
if (typeof content === "string") {
|
||||
const ejsResult = ejs.render(content, opts, { includer });
|
||||
res.send(ejsResult);
|
||||
useDefaultView = false; // Rendering went okay, don't use default view
|
||||
import("ejs").then((ejs) => {
|
||||
const ejsResult = ejs.render(content, opts, { includer });
|
||||
res.send(ejsResult);
|
||||
useDefaultView = false; // Rendering went okay, don't use default view
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||
|
||||
Reference in New Issue
Block a user