diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 61698721f9..4e214545fa 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1,7 +1,7 @@ import { getLog, initializeCore, sql_init } from "@triliumnext/core"; import ClsHookedExecutionContext from "@triliumnext/server/src/cls_provider.js"; import NodejsCryptoProvider from "@triliumnext/server/src/crypto_provider.js"; -import NodejsZipProvider from "@triliumnext/server/src/zip_provider.js"; +import NodejsInAppHelpProvider from "@triliumnext/server/src/in_app_help_provider.js"; import dataDirs from "@triliumnext/server/src/services/data_dir.js"; import options from "@triliumnext/server/src/services/options.js"; import port from "@triliumnext/server/src/services/port.js"; @@ -10,6 +10,7 @@ import tray from "@triliumnext/server/src/services/tray.js"; import windowService from "@triliumnext/server/src/services/window.js"; import WebSocketMessagingProvider from "@triliumnext/server/src/services/ws_messaging_provider.js"; import BetterSqlite3Provider from "@triliumnext/server/src/sql_provider.js"; +import NodejsZipProvider from "@triliumnext/server/src/zip_provider.js"; import { app, BrowserWindow,globalShortcut } from "electron"; import electronDebug from "electron-debug"; import electronDl from "electron-dl"; @@ -143,6 +144,7 @@ async function main() { platform: new DesktopPlatformProvider(), translations: (await import("@triliumnext/server/src/services/i18n.js")).initializeTranslations, getDemoArchive: async () => fs.readFileSync(require.resolve("@triliumnext/server/src/assets/db/demo.zip")), + inAppHelp: new NodejsInAppHelpProvider(), extraAppInfo: { nodeVersion: process.version, dataDirectory: path.resolve(dataDirs.TRILIUM_DATA_DIR) diff --git a/apps/edit-docs/src/edit-docs.ts b/apps/edit-docs/src/edit-docs.ts index 239b52a0bb..d7846b627d 100644 --- a/apps/edit-docs/src/edit-docs.ts +++ b/apps/edit-docs/src/edit-docs.ts @@ -1,7 +1,7 @@ import debounce from "@triliumnext/client/src/services/debounce.js"; import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/core"; +import NodejsInAppHelpProvider from "@triliumnext/server/src/in_app_help_provider.js"; import cls from "@triliumnext/server/src/services/cls.js"; -import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js"; import type { NoteMetaFile } from "@triliumnext/server/src/services/meta/note_meta.js"; import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js"; import fs from "fs/promises"; @@ -241,7 +241,7 @@ async function cleanUpMeta(outputPath: string, minify: boolean) { } if (minify) { - const subtree = parseNoteMetaFile(meta); + const subtree = new NodejsInAppHelpProvider().parseNoteMetaFile(meta); await fs.writeFile(metaPath, JSON.stringify(subtree)); } else { await fs.writeFile(metaPath, JSON.stringify(meta, null, 4)); diff --git a/apps/server/spec/setup.ts b/apps/server/spec/setup.ts index c84112610e..4c40a4f3be 100644 --- a/apps/server/spec/setup.ts +++ b/apps/server/spec/setup.ts @@ -8,6 +8,7 @@ import NodejsCryptoProvider from "../src/crypto_provider.js"; import NodejsZipProvider from "../src/zip_provider.js"; import ServerPlatformProvider from "../src/platform_provider.js"; import BetterSqlite3Provider from "../src/sql_provider.js"; +import NodejsInAppHelpProvider from "../src/in_app_help_provider.js"; import { initializeTranslations } from "../src/services/i18n.js"; // Initialize environment variables. @@ -39,6 +40,7 @@ beforeAll(async () => { executionContext: new ClsHookedExecutionContext(), schema: readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"), platform: new ServerPlatformProvider(), - translations: initializeTranslations + translations: initializeTranslations, + inAppHelp: new NodejsInAppHelpProvider() }); }); diff --git a/apps/server/src/in_app_help_provider.ts b/apps/server/src/in_app_help_provider.ts new file mode 100644 index 0000000000..7dd6569f46 --- /dev/null +++ b/apps/server/src/in_app_help_provider.ts @@ -0,0 +1,165 @@ +import type { HiddenSubtreeItem } from "@triliumnext/commons"; +import type { InAppHelpProvider } from "@triliumnext/core"; +import fs from "fs"; +import path from "path"; + +import becca from "./becca/becca.js"; +import type BNote from "./becca/entities/bnote.js"; +import type NoteMeta from "./services/meta/note_meta.js"; +import type { NoteMetaFile } from "./services/meta/note_meta.js"; +import { RESOURCE_DIR } from "./services/resource_dir.js"; + +export default class NodejsInAppHelpProvider implements InAppHelpProvider { + + getHelpHiddenSubtreeData(): HiddenSubtreeItem[] { + const helpDir = path.join(RESOURCE_DIR, "doc_notes", "en", "User Guide"); + const metaFilePath = path.join(helpDir, "!!!meta.json"); + + try { + return JSON.parse(fs.readFileSync(metaFilePath).toString("utf-8")); + } catch (e) { + console.warn(e); + return []; + } + } + + parseNoteMetaFile(noteMetaFile: NoteMetaFile): HiddenSubtreeItem[] { + if (!noteMetaFile.files) { + console.log("No meta files"); + return []; + } + + const metaRoot = noteMetaFile.files[0]; + const parsedMetaRoot = this.parseNoteMeta(metaRoot, "/" + (metaRoot.dirFileName ?? "")); + return parsedMetaRoot?.children ?? []; + } + + parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeItem | null { + let iconClass: string = "bx bx-file"; + const item: HiddenSubtreeItem = { + id: `_help_${noteMeta.noteId}`, + title: noteMeta.title ?? "", + type: "doc", // can change + attributes: [] + }; + + // 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 + }); + } + + if (attribute.name === "shareHiddenFromTree") { + return null; + } + } + + // Handle text notes + if (noteMeta.type === "text" && noteMeta.dataFileName) { + const docPath = `${docNameRoot}/${path.basename(noteMeta.dataFileName, ".html")}`.substring(1); + item.attributes?.push({ + type: "label", + name: "docName", + value: docPath + }); + } + + // Handle web views + if (noteMeta.type === "webView") { + item.type = "webView"; + item.enforceAttributes = true; + } + + // Handle children + if (noteMeta.children) { + const children: HiddenSubtreeItem[] = []; + for (const childMeta of noteMeta.children) { + let newDocNameRoot = noteMeta.dirFileName ? `${docNameRoot}/${noteMeta.dirFileName}` : docNameRoot; + const item = this.parseNoteMeta(childMeta, newDocNameRoot); + if (item) { + children.push(item); + } + } + + item.children = children; + } + + // Handle note icon + item.attributes?.push({ + name: "iconClass", + value: iconClass, + type: "label" + }); + + 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. + */ + cleanUpHelp(helpDefinition: HiddenSubtreeItem[]): void { + 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(); + } + } + } + +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 9f54b0c9bd..f452616937 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -10,6 +10,7 @@ import path from "path"; import ClsHookedExecutionContext from "./cls_provider.js"; import NodejsCryptoProvider from "./crypto_provider.js"; +import NodejsInAppHelpProvider from "./in_app_help_provider.js"; import ServerPlatformProvider from "./platform_provider.js"; import dataDirs from "./services/data_dir.js"; import port from "./services/port.js"; @@ -61,6 +62,7 @@ async function startApplication() { platform: new ServerPlatformProvider(), translations: (await import("./services/i18n.js")).initializeTranslations, getDemoArchive: async () => fs.readFileSync(require.resolve("@triliumnext/server/src/assets/db/demo.zip")), + inAppHelp: new NodejsInAppHelpProvider(), extraAppInfo: { nodeVersion: process.version, dataDirectory: path.resolve(dataDirs.TRILIUM_DATA_DIR) diff --git a/apps/server/src/services/in_app_help.spec.ts b/apps/server/src/services/in_app_help.spec.ts index a9df31c1a2..7c41d4b47a 100644 --- a/apps/server/src/services/in_app_help.spec.ts +++ b/apps/server/src/services/in_app_help.spec.ts @@ -1,7 +1,10 @@ -import { describe, expect, expectTypeOf, it } from "vitest"; -import { parseNoteMeta } from "./in_app_help.js"; +import { describe, expect, it } from "vitest"; + +import NodejsInAppHelpProvider from "../in_app_help_provider.js"; import type NoteMeta from "./meta/note_meta.js"; +const provider = new NodejsInAppHelpProvider(); + describe("In-app help", () => { it("preserves custom folder icon", () => { const meta: NoteMeta = { @@ -29,7 +32,7 @@ describe("In-app help", () => { children: [] }; - const item = parseNoteMeta(meta, "/"); + const item = provider.parseNoteMeta(meta, "/"); const icon = item?.attributes?.find((a) => a.name === "iconClass"); expect(icon?.value).toBe("bx bx-star"); }); @@ -60,7 +63,7 @@ describe("In-app help", () => { children: [] }; - const item = parseNoteMeta(meta, "/"); + const item = provider.parseNoteMeta(meta, "/"); expect(item).toBeFalsy(); }); }); diff --git a/apps/server/src/services/in_app_help.ts b/apps/server/src/services/in_app_help.ts deleted file mode 100644 index 1424493aef..0000000000 --- a/apps/server/src/services/in_app_help.ts +++ /dev/null @@ -1,158 +0,0 @@ -import path from "path"; -import fs from "fs"; -import type NoteMeta from "./meta/note_meta.js"; -import type { NoteMetaFile } from "./meta/note_meta.js"; -import type BNote from "../becca/entities/bnote.js"; -import becca from "../becca/becca.js"; -import type { HiddenSubtreeItem } from "@triliumnext/commons"; -import { RESOURCE_DIR } from "./resource_dir.js"; - -export function getHelpHiddenSubtreeData() { - const helpDir = path.join(RESOURCE_DIR, "doc_notes", "en", "User Guide"); - const metaFilePath = path.join(helpDir, "!!!meta.json"); - - try { - return JSON.parse(fs.readFileSync(metaFilePath).toString("utf-8")); - } catch (e) { - console.warn(e); - return []; - } -} - -export function parseNoteMetaFile(noteMetaFile: NoteMetaFile): HiddenSubtreeItem[] { - if (!noteMetaFile.files) { - console.log("No meta files"); - return []; - } - - const metaRoot = noteMetaFile.files[0]; - const parsedMetaRoot = parseNoteMeta(metaRoot, "/" + (metaRoot.dirFileName ?? "")); - return parsedMetaRoot?.children ?? []; -} - -export function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeItem | null { - let iconClass: string = "bx bx-file"; - const item: HiddenSubtreeItem = { - id: `_help_${noteMeta.noteId}`, - title: noteMeta.title ?? "", - type: "doc", // can change - attributes: [] - }; - - // 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 - }); - } - - if (attribute.name === "shareHiddenFromTree") { - return null; - } - } - - // Handle text notes - if (noteMeta.type === "text" && noteMeta.dataFileName) { - const docPath = `${docNameRoot}/${path.basename(noteMeta.dataFileName, ".html")}`.substring(1); - item.attributes?.push({ - type: "label", - name: "docName", - value: docPath - }); - } - - // Handle web views - if (noteMeta.type === "webView") { - item.type = "webView"; - item.enforceAttributes = true; - } - - // Handle children - if (noteMeta.children) { - const children: HiddenSubtreeItem[] = []; - for (const childMeta of noteMeta.children) { - let newDocNameRoot = noteMeta.dirFileName ? `${docNameRoot}/${noteMeta.dirFileName}` : docNameRoot; - const item = parseNoteMeta(childMeta, newDocNameRoot); - if (item) { - children.push(item); - } - } - - item.children = children; - } - - // Handle note icon - item.attributes?.push({ - name: "iconClass", - value: iconClass, - type: "label" - }); - - 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(); - } - } -} diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index d2cfdec5a7..d0551be776 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -11,6 +11,7 @@ import appInfo from "./services/app_info"; import { type PlatformProvider, initPlatform } from "./services/platform"; import { type ZipProvider, initZipProvider } from "./services/zip_provider"; import { type ZipExportProviderFactory, initZipExportProviderFactory } from "./services/export/zip_export_provider_factory"; +import { type InAppHelpProvider, initInAppHelp } from "./services/in_app_help"; export { getLog } from "./services/log"; export type * from "./services/sql/types"; @@ -96,6 +97,7 @@ export { default as content_hash } from "./services/content_hash"; export { default as sync_mutex } from "./services/sync_mutex"; export { default as setup } from "./services/setup"; export { getPlatform, type PlatformProvider } from "./services/platform"; +export type { InAppHelpProvider } from "./services/in_app_help"; export { t } from "i18next"; export type { RequestProvider, ExecOpts, CookieJar } from "./services/request"; export type * from "./meta"; @@ -119,7 +121,7 @@ export { default as scriptService } from "./services/script"; export { default as BackendScriptApi, type Api as BackendScriptApiInterface } from "./services/backend_script_api"; export * as scheduler from "./services/scheduler"; -export async function initializeCore({ dbConfig, executionContext, crypto, zip, zipExportProviderFactory, translations, messaging, request, schema, extraAppInfo, platform, getDemoArchive }: { +export async function initializeCore({ dbConfig, executionContext, crypto, zip, zipExportProviderFactory, translations, messaging, request, schema, extraAppInfo, platform, getDemoArchive, inAppHelp }: { dbConfig: SqlServiceParams, executionContext: ExecutionContext, crypto: CryptoProvider, @@ -131,6 +133,7 @@ export async function initializeCore({ dbConfig, executionContext, crypto, zip, messaging?: MessagingProvider, request?: RequestProvider, getDemoArchive?: () => Promise, + inAppHelp?: InAppHelpProvider, extraAppInfo?: { nodeVersion: string; dataDirectory: string; @@ -155,4 +158,7 @@ export async function initializeCore({ dbConfig, executionContext, crypto, zip, if (request) { initRequest(request); } + if (inAppHelp) { + initInAppHelp(inAppHelp); + } }; diff --git a/packages/trilium-core/src/services/in_app_help.ts b/packages/trilium-core/src/services/in_app_help.ts index 4f377f0a6a..2da46190f2 100644 --- a/packages/trilium-core/src/services/in_app_help.ts +++ b/packages/trilium-core/src/services/in_app_help.ts @@ -1,8 +1,20 @@ -export function cleanUpHelp(items: unknown[]) { - // TODO: implement. +import type { HiddenSubtreeItem } from "@triliumnext/commons"; + +export interface InAppHelpProvider { + getHelpHiddenSubtreeData(): HiddenSubtreeItem[]; + cleanUpHelp(items: HiddenSubtreeItem[]): void; } -export function getHelpHiddenSubtreeData() { - // TODO: implement. - return []; +let provider: InAppHelpProvider | null = null; + +export function initInAppHelp(p: InAppHelpProvider) { + provider = p; +} + +export function getHelpHiddenSubtreeData(): HiddenSubtreeItem[] { + return provider?.getHelpHiddenSubtreeData() ?? []; +} + +export function cleanUpHelp(items: HiddenSubtreeItem[]): void { + provider?.cleanUpHelp(items); }