Compare commits
	
		
			6 Commits
		
	
	
		
			copilot/ad
			...
			fix/fix-eq
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8d88411fda | ||
| 
						 | 
					8e227a6146 | ||
| 
						 | 
					b03cb1ce1b | ||
| 
						 | 
					fb0d971e48 | ||
| 
						 | 
					4fa4112840 | ||
| 
						 | 
					50f0b88eff | 
							
								
								
									
										73
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,4 +1,6 @@
 | 
			
		||||
name: Deploy Documentation
 | 
			
		||||
# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages
 | 
			
		||||
# This workflow builds and deploys your MkDocs site when changes are pushed to main
 | 
			
		||||
name: Deploy MkDocs Documentation
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  # Trigger on push to main branch
 | 
			
		||||
@@ -9,8 +11,11 @@ on:
 | 
			
		||||
    # Only run when docs files change
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docs/**'
 | 
			
		||||
      - 'apps/edit-docs/**'
 | 
			
		||||
      - 'packages/share-theme/**'
 | 
			
		||||
      - 'README.md'  # README is synced to docs/index.md
 | 
			
		||||
      - 'mkdocs.yml'
 | 
			
		||||
      - 'requirements-docs.txt'
 | 
			
		||||
      - '.github/workflows/deploy-docs.yml'
 | 
			
		||||
      - 'scripts/fix-mkdocs-structure.ts'
 | 
			
		||||
 | 
			
		||||
  # Allow manual triggering from Actions tab
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
@@ -22,12 +27,15 @@ on:
 | 
			
		||||
      - master
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docs/**'
 | 
			
		||||
      - 'apps/edit-docs/**'
 | 
			
		||||
      - 'packages/share-theme/**'
 | 
			
		||||
      - 'README.md'  # README is synced to docs/index.md
 | 
			
		||||
      - 'mkdocs.yml'
 | 
			
		||||
      - 'requirements-docs.txt'
 | 
			
		||||
      - '.github/workflows/deploy-docs.yml'
 | 
			
		||||
      - 'scripts/fix-mkdocs-structure.ts'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build-and-deploy:
 | 
			
		||||
    name: Build and Deploy Documentation
 | 
			
		||||
    name: Build and Deploy MkDocs
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    timeout-minutes: 10
 | 
			
		||||
 | 
			
		||||
@@ -41,25 +49,72 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout Repository
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
 | 
			
		||||
 | 
			
		||||
      - name: Setup Python
 | 
			
		||||
        uses: actions/setup-python@v6
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.14'
 | 
			
		||||
          cache: 'pip'
 | 
			
		||||
          cache-dependency-path: 'requirements-docs.txt'
 | 
			
		||||
 | 
			
		||||
      - name: Install MkDocs and Dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          pip install --upgrade pip
 | 
			
		||||
          pip install -r requirements-docs.txt
 | 
			
		||||
        env:
 | 
			
		||||
          PIP_DISABLE_PIP_VERSION_CHECK: 1
 | 
			
		||||
 | 
			
		||||
      # Setup pnpm before fixing docs structure
 | 
			
		||||
      - name: Setup pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4
 | 
			
		||||
 | 
			
		||||
      # Setup Node.js with pnpm
 | 
			
		||||
      - name: Setup Node.js
 | 
			
		||||
        uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '24'
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
 | 
			
		||||
      # Install Node.js dependencies for the TypeScript script
 | 
			
		||||
      - name: Install Dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
        run: |
 | 
			
		||||
          pnpm install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
      - name: Trigger build of documentation
 | 
			
		||||
        run: pnpm docs:build
 | 
			
		||||
      - name: Fix Documentation Structure
 | 
			
		||||
        run: |
 | 
			
		||||
          # Fix duplicate navigation entries by moving overview pages to index.md
 | 
			
		||||
          pnpm run chore:fix-mkdocs-structure
 | 
			
		||||
 | 
			
		||||
      - name: Build MkDocs Site
 | 
			
		||||
        run: |
 | 
			
		||||
          # Build with strict mode but allow expected warnings
 | 
			
		||||
          mkdocs build --verbose || {
 | 
			
		||||
            EXIT_CODE=$?
 | 
			
		||||
            # Check if the only issue is expected warnings
 | 
			
		||||
            if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \
 | 
			
		||||
               [ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then
 | 
			
		||||
              echo "✅ Build succeeded with expected warnings"
 | 
			
		||||
              mkdocs build --verbose
 | 
			
		||||
            else
 | 
			
		||||
              echo "❌ Build failed with unexpected errors"
 | 
			
		||||
              exit $EXIT_CODE
 | 
			
		||||
            fi
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
      - name: Fix HTML Links
 | 
			
		||||
        run: |
 | 
			
		||||
          # Remove .md extensions from links in generated HTML
 | 
			
		||||
          pnpm tsx ./scripts/fix-html-links.ts site
 | 
			
		||||
 | 
			
		||||
      - name: Validate Built Site
 | 
			
		||||
        run: |
 | 
			
		||||
          # Basic validation that important files exist
 | 
			
		||||
          test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
 | 
			
		||||
          test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
 | 
			
		||||
          test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
 | 
			
		||||
          echo "✅ Site validation passed"
 | 
			
		||||
 | 
			
		||||
      - name: Deploy
 | 
			
		||||
        uses: ./.github/actions/deploy-to-cloudflare-pages
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/main-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -116,10 +116,10 @@ jobs:
 | 
			
		||||
          - dockerfile: Dockerfile
 | 
			
		||||
            platform: linux/arm64
 | 
			
		||||
            image: ubuntu-24.04-arm
 | 
			
		||||
          - dockerfile: Dockerfile.legacy
 | 
			
		||||
          - dockerfile: Dockerfile
 | 
			
		||||
            platform: linux/arm/v7
 | 
			
		||||
            image: ubuntu-24.04-arm
 | 
			
		||||
          - dockerfile: Dockerfile.legacy
 | 
			
		||||
          - dockerfile: Dockerfile
 | 
			
		||||
            platform: linux/arm/v8
 | 
			
		||||
            image: ubuntu-24.04-arm
 | 
			
		||||
    runs-on: ${{ matrix.image }}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,17 +38,19 @@
 | 
			
		||||
    "@playwright/test": "1.56.1",
 | 
			
		||||
    "@stylistic/eslint-plugin": "5.5.0",        
 | 
			
		||||
    "@types/express": "5.0.5",    
 | 
			
		||||
    "@types/node": "24.9.2",    
 | 
			
		||||
    "@types/node": "24.9.1",    
 | 
			
		||||
    "@types/yargs": "17.0.34",
 | 
			
		||||
    "@vitest/coverage-v8": "3.2.4",
 | 
			
		||||
    "eslint": "9.39.0",
 | 
			
		||||
    "eslint": "9.38.0",
 | 
			
		||||
    "eslint-plugin-simple-import-sort": "12.1.1",
 | 
			
		||||
    "esm": "3.2.25",
 | 
			
		||||
    "jsdoc": "4.0.5",
 | 
			
		||||
    "lorem-ipsum": "2.0.8",    
 | 
			
		||||
    "rcedit": "4.0.1",
 | 
			
		||||
    "rimraf": "6.1.0",    
 | 
			
		||||
    "tslib": "2.8.1" 
 | 
			
		||||
    "rimraf": "6.0.1",    
 | 
			
		||||
    "tslib": "2.8.1",    
 | 
			
		||||
    "typedoc": "0.28.14",
 | 
			
		||||
    "typedoc-plugin-missing-exports": "4.1.2"
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "appdmg": "0.6.6"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								_regroup/typedoc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "entryPoints": [
 | 
			
		||||
    "src/services/backend_script_entrypoint.ts",
 | 
			
		||||
    "src/public/app/services/frontend_script_entrypoint.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugin": [
 | 
			
		||||
    "typedoc-plugin-missing-exports"
 | 
			
		||||
  ],
 | 
			
		||||
  "outputs": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "html",
 | 
			
		||||
      "path": "./docs/Script API"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "build-docs",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "src/main.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "tsx ."
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "author": "Elian Doran <contact@eliandoran.me>",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "packageManager": "pnpm@10.19.0",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@redocly/cli": "2.10.0",
 | 
			
		||||
    "archiver": "7.0.1",
 | 
			
		||||
    "fs-extra": "11.3.2",
 | 
			
		||||
    "react": "19.2.0",
 | 
			
		||||
    "react-dom": "19.2.0",
 | 
			
		||||
    "typedoc": "0.28.14",
 | 
			
		||||
    "typedoc-plugin-missing-exports": "4.1.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The backend script API is accessible to code notes with the "JS (backend)" language.
 | 
			
		||||
 *
 | 
			
		||||
 * The entire API is exposed as a single global: {@link api}
 | 
			
		||||
 *
 | 
			
		||||
 * @module Backend Script API
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This file creates the entrypoint for TypeDoc that simulates the context from within a
 | 
			
		||||
 * script note on the server side.
 | 
			
		||||
 *
 | 
			
		||||
 * Make sure to keep in line with backend's `script_context.ts`.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js";
 | 
			
		||||
export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js";
 | 
			
		||||
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
 | 
			
		||||
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
 | 
			
		||||
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
 | 
			
		||||
export type { BNote };
 | 
			
		||||
export type { default as BOption } from "../../server/src/becca/entities/boption.js";
 | 
			
		||||
export type { default as BRecentNote } from "../../server/src/becca/entities/brecent_note.js";
 | 
			
		||||
export type { default as BRevision } from "../../server/src/becca/entities/brevision.js";
 | 
			
		||||
 | 
			
		||||
import BNote from "../../server/src/becca/entities/bnote.js";
 | 
			
		||||
import BackendScriptApi, { type Api } from "../../server/src/services/backend_script_api.js";
 | 
			
		||||
 | 
			
		||||
export type { Api };
 | 
			
		||||
 | 
			
		||||
const fakeNote = new BNote();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The `api` global variable allows access to the backend script API, which is documented in {@link Api}.
 | 
			
		||||
 */
 | 
			
		||||
export const api: Api = new BackendScriptApi(fakeNote, {});
 | 
			
		||||
@@ -1,127 +0,0 @@
 | 
			
		||||
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
 | 
			
		||||
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
 | 
			
		||||
process.env.NODE_ENV = "development";
 | 
			
		||||
 | 
			
		||||
import cls from "@triliumnext/server/src/services/cls.js";
 | 
			
		||||
import { dirname, join, resolve } from "path";
 | 
			
		||||
import * as fs from "fs/promises";
 | 
			
		||||
import * as fsExtra from "fs-extra";
 | 
			
		||||
import archiver from "archiver";
 | 
			
		||||
import { WriteStream } from "fs";
 | 
			
		||||
import { execSync } from "child_process";
 | 
			
		||||
import BuildContext from "./context.js";
 | 
			
		||||
 | 
			
		||||
const DOCS_ROOT = "../../../docs";
 | 
			
		||||
const OUTPUT_DIR = "../../site";
 | 
			
		||||
 | 
			
		||||
async function buildDocsInner() {
 | 
			
		||||
    const i18n = await import("@triliumnext/server/src/services/i18n.js");
 | 
			
		||||
    await i18n.initializeTranslations();
 | 
			
		||||
 | 
			
		||||
    const sqlInit = (await import("../../server/src/services/sql_init.js")).default;
 | 
			
		||||
    await sqlInit.createInitialDatabase(true);
 | 
			
		||||
 | 
			
		||||
    const note = await importData(join(__dirname, DOCS_ROOT, "User Guide"));
 | 
			
		||||
 | 
			
		||||
    // Export
 | 
			
		||||
    const zipFilePath = "output.zip";
 | 
			
		||||
    try {
 | 
			
		||||
        const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
 | 
			
		||||
        const branch = note.getParentBranches()[0];
 | 
			
		||||
        const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
 | 
			
		||||
            "no-progress-reporting",
 | 
			
		||||
            "export",
 | 
			
		||||
            null
 | 
			
		||||
        );
 | 
			
		||||
        const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
 | 
			
		||||
        await exportToZip(taskContext, branch, "share", fileOutputStream);
 | 
			
		||||
        await waitForStreamToFinish(fileOutputStream);
 | 
			
		||||
        await extractZip(zipFilePath, OUTPUT_DIR);
 | 
			
		||||
    } finally {
 | 
			
		||||
        if (await fsExtra.exists(zipFilePath)) {
 | 
			
		||||
            await fsExtra.rm(zipFilePath);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Copy favicon.
 | 
			
		||||
    await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
 | 
			
		||||
 | 
			
		||||
    console.log("Documentation built successfully!");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function importData(path: string) {
 | 
			
		||||
    const buffer = await createImportZip(path);
 | 
			
		||||
    const importService = (await import("@triliumnext/server/src/services/import/zip.js")).default;
 | 
			
		||||
    const TaskContext = (await import("@triliumnext/server/src/services/task_context.js")).default;
 | 
			
		||||
    const context = new TaskContext("no-progress-reporting", "importNotes", null);
 | 
			
		||||
    const becca = (await import("@triliumnext/server/src/becca/becca.js")).default;
 | 
			
		||||
 | 
			
		||||
    const rootNote = becca.getRoot();
 | 
			
		||||
    if (!rootNote) {
 | 
			
		||||
        throw new Error("Missing root note for import.");
 | 
			
		||||
    }
 | 
			
		||||
    return await importService.importZip(context, buffer, rootNote, {
 | 
			
		||||
        preserveIds: true
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function createImportZip(path: string) {
 | 
			
		||||
    const inputFile = "input.zip";
 | 
			
		||||
    const archive = archiver("zip", {
 | 
			
		||||
        zlib: { level: 0 }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    console.log("Archive path is ", resolve(path))
 | 
			
		||||
    archive.directory(path, "/");
 | 
			
		||||
 | 
			
		||||
    const outputStream = fsExtra.createWriteStream(inputFile);
 | 
			
		||||
    archive.pipe(outputStream);
 | 
			
		||||
    archive.finalize();
 | 
			
		||||
    await waitForStreamToFinish(outputStream);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await fsExtra.readFile(inputFile);
 | 
			
		||||
    } finally {
 | 
			
		||||
        await fsExtra.rm(inputFile);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForStreamToFinish(stream: WriteStream) {
 | 
			
		||||
    return new Promise<void>((res, rej) => {
 | 
			
		||||
        stream.on("finish", () => res());
 | 
			
		||||
        stream.on("error", (err) => rej(err));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
 | 
			
		||||
    const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
 | 
			
		||||
    await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
 | 
			
		||||
        // We ignore directories since they can appear out of order anyway.
 | 
			
		||||
        if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
 | 
			
		||||
            const destPath = join(outputPath, entry.fileName);
 | 
			
		||||
            const fileContent = await readContent(zip, entry);
 | 
			
		||||
 | 
			
		||||
            await fsExtra.mkdirs(dirname(destPath));
 | 
			
		||||
            await fs.writeFile(destPath, fileContent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        zip.readEntry();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function buildDocs({ gitRootDir }: BuildContext) {
 | 
			
		||||
    // Build the share theme.
 | 
			
		||||
    execSync(`pnpm run --filter share-theme build`, {
 | 
			
		||||
        stdio: "inherit",
 | 
			
		||||
        cwd: gitRootDir
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Trigger the actual build.
 | 
			
		||||
    await new Promise((res, rej) => {
 | 
			
		||||
        cls.init(() => {
 | 
			
		||||
            buildDocsInner()
 | 
			
		||||
                .catch(rej)
 | 
			
		||||
                .then(res);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
export default interface BuildContext {
 | 
			
		||||
    gitRootDir: string;
 | 
			
		||||
    baseDir: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The front script API is accessible to code notes with the "JS (frontend)" language.
 | 
			
		||||
 *
 | 
			
		||||
 * The entire API is exposed as a single global: {@link api}
 | 
			
		||||
 *
 | 
			
		||||
 * @module Frontend Script API
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This file creates the entrypoint for TypeDoc that simulates the context from within a
 | 
			
		||||
 * script note.
 | 
			
		||||
 *
 | 
			
		||||
 * Make sure to keep in line with frontend's `script_context.ts`.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
 | 
			
		||||
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
 | 
			
		||||
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
 | 
			
		||||
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
 | 
			
		||||
export type { default as FNote } from "../../client/src/entities/fnote.js";
 | 
			
		||||
export type { Api } from "../../client/src/services/frontend_script_api.js";
 | 
			
		||||
export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js";
 | 
			
		||||
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
 | 
			
		||||
 | 
			
		||||
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
 | 
			
		||||
 | 
			
		||||
//@ts-expect-error
 | 
			
		||||
export const api: Api = new FrontendScriptApi();
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import BuildContext from "./context";
 | 
			
		||||
import buildSwagger from "./swagger";
 | 
			
		||||
import { existsSync, mkdirSync, rmSync } from "fs";
 | 
			
		||||
import buildDocs from "./build-docs";
 | 
			
		||||
import buildScriptApi from "./script-api";
 | 
			
		||||
 | 
			
		||||
const context: BuildContext = {
 | 
			
		||||
    gitRootDir: join(__dirname, "../../../"),
 | 
			
		||||
    baseDir: join(__dirname, "../../../site")
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
    // Clean input dir.
 | 
			
		||||
    if (existsSync(context.baseDir)) {
 | 
			
		||||
        rmSync(context.baseDir, { recursive: true });
 | 
			
		||||
    }
 | 
			
		||||
    mkdirSync(context.baseDir);
 | 
			
		||||
 | 
			
		||||
    // Start building.
 | 
			
		||||
    await buildDocs(context);
 | 
			
		||||
    buildSwagger(context);
 | 
			
		||||
    buildScriptApi(context);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import { execSync } from "child_process";
 | 
			
		||||
import BuildContext from "./context";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
 | 
			
		||||
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
 | 
			
		||||
    // Generate types
 | 
			
		||||
    execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir });
 | 
			
		||||
 | 
			
		||||
    for (const config of [ "backend", "frontend" ]) {
 | 
			
		||||
        const outDir = join(baseDir, "script-api", config);
 | 
			
		||||
        execSync(`pnpm typedoc --options typedoc.${config}.json --html "${outDir}"`, {
 | 
			
		||||
            stdio: "inherit"
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
import BuildContext from "./context";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import { execSync } from "child_process";
 | 
			
		||||
import { mkdirSync } from "fs";
 | 
			
		||||
 | 
			
		||||
interface BuildInfo {
 | 
			
		||||
    specPath: string;
 | 
			
		||||
    outDir: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DIR_PREFIX = "rest-api";
 | 
			
		||||
 | 
			
		||||
const buildInfos: BuildInfo[] = [
 | 
			
		||||
    {
 | 
			
		||||
        // Paths are relative to Git root.
 | 
			
		||||
        specPath: "apps/server/internal.openapi.yaml",
 | 
			
		||||
        outDir: `${DIR_PREFIX}/internal`
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        specPath: "apps/server/etapi.openapi.yaml",
 | 
			
		||||
        outDir: `${DIR_PREFIX}/etapi`
 | 
			
		||||
    }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
 | 
			
		||||
    for (const { specPath, outDir } of buildInfos) {
 | 
			
		||||
        const absSpecPath = join(gitRootDir, specPath);
 | 
			
		||||
        const targetDir = join(baseDir, outDir);
 | 
			
		||||
        mkdirSync(targetDir, { recursive: true });
 | 
			
		||||
        execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "../../tsconfig.base.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "target": "ES2020",
 | 
			
		||||
    "outDir": "dist",
 | 
			
		||||
    "strict": false,
 | 
			
		||||
    "types": [
 | 
			
		||||
      "node",
 | 
			
		||||
      "express"
 | 
			
		||||
    ],
 | 
			
		||||
    "rootDir": "src",
 | 
			
		||||
    "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "src/**/*.ts",
 | 
			
		||||
    "../server/src/*.d.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "exclude": [
 | 
			
		||||
    "eslint.config.js",
 | 
			
		||||
    "eslint.config.cjs",
 | 
			
		||||
    "eslint.config.mjs"
 | 
			
		||||
  ],
 | 
			
		||||
  "references": [
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../server/tsconfig.app.json"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../desktop/tsconfig.app.json"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../client/tsconfig.app.json"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "../../tsconfig.base.json",
 | 
			
		||||
  "include": [],
 | 
			
		||||
  "references": [
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../server"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../client"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "./tsconfig.app.json"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://typedoc.org/schema.json",
 | 
			
		||||
  "name": "Trilium Backend API",
 | 
			
		||||
  "entryPoints": [
 | 
			
		||||
    "src/backend_script_entrypoint.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugin": [
 | 
			
		||||
    "typedoc-plugin-missing-exports"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://typedoc.org/schema.json",
 | 
			
		||||
  "name": "Trilium Frontend API",
 | 
			
		||||
  "entryPoints": [
 | 
			
		||||
    "src/frontend_script_entrypoint.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugin": [
 | 
			
		||||
    "typedoc-plugin-missing-exports"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
    "circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@eslint/js": "9.39.0",
 | 
			
		||||
    "@eslint/js": "9.38.0",
 | 
			
		||||
    "@excalidraw/excalidraw": "0.18.0",
 | 
			
		||||
    "@fullcalendar/core": "6.1.19",
 | 
			
		||||
    "@fullcalendar/daygrid": "6.1.19",
 | 
			
		||||
@@ -37,12 +37,12 @@
 | 
			
		||||
    "bootstrap": "5.3.8",
 | 
			
		||||
    "boxicons": "2.1.4",
 | 
			
		||||
    "color": "5.0.2",
 | 
			
		||||
    "dayjs": "1.11.19",
 | 
			
		||||
    "dayjs": "1.11.18",
 | 
			
		||||
    "dayjs-plugin-utc": "0.1.2",
 | 
			
		||||
    "debounce": "2.2.0",
 | 
			
		||||
    "draggabilly": "3.0.0",
 | 
			
		||||
    "force-graph": "1.51.0",
 | 
			
		||||
    "globals": "16.5.0",
 | 
			
		||||
    "globals": "16.4.0",
 | 
			
		||||
    "i18next": "25.6.0",
 | 
			
		||||
    "i18next-http-backend": "3.0.2",
 | 
			
		||||
    "jquery": "3.7.1",
 | 
			
		||||
@@ -59,7 +59,7 @@
 | 
			
		||||
    "normalize.css": "8.0.1",
 | 
			
		||||
    "panzoom": "9.4.3",
 | 
			
		||||
    "preact": "10.27.2",
 | 
			
		||||
    "react-i18next": "16.2.3",
 | 
			
		||||
    "react-i18next": "16.2.1",
 | 
			
		||||
    "reveal.js": "5.2.1",
 | 
			
		||||
    "svg-pan-zoom": "3.6.2",
 | 
			
		||||
    "tabulator-tables": "6.3.1",
 | 
			
		||||
@@ -76,7 +76,7 @@
 | 
			
		||||
    "@types/reveal.js": "5.2.1",
 | 
			
		||||
    "@types/tabulator-tables": "6.3.0",
 | 
			
		||||
    "copy-webpack-plugin": "13.0.1",
 | 
			
		||||
    "happy-dom": "20.0.10",
 | 
			
		||||
    "happy-dom": "20.0.8",
 | 
			
		||||
    "script-loader": "0.7.2",
 | 
			
		||||
    "vite-plugin-static-copy": "3.1.4"
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -270,7 +270,6 @@ export type CommandMappings = {
 | 
			
		||||
    closeThisNoteSplit: CommandData;
 | 
			
		||||
    moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
 | 
			
		||||
    jumpToNote: CommandData;
 | 
			
		||||
    openTodayNote: CommandData;
 | 
			
		||||
    commandPalette: CommandData;
 | 
			
		||||
 | 
			
		||||
    // Keyboard shortcuts
 | 
			
		||||
 
 | 
			
		||||
@@ -159,16 +159,6 @@ export default class Entrypoints extends Component {
 | 
			
		||||
        this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async openTodayNoteCommand() {
 | 
			
		||||
        const todayNote = await dateNoteService.getTodayNote();
 | 
			
		||||
        if (!todayNote) {
 | 
			
		||||
            console.warn("Missing today note.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await appContext.tabManager.openInSameTab(todayNote.noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async runActiveNoteCommand() {
 | 
			
		||||
        const noteContext = appContext.tabManager.getActiveContext();
 | 
			
		||||
        if (!noteContext) {
 | 
			
		||||
 
 | 
			
		||||
@@ -417,7 +417,7 @@ export default class FNote {
 | 
			
		||||
        return notePaths;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getSortedNotePathRecords(hoistedNoteId = "root", activeNotePath: string | null = null): NotePathRecord[] {
 | 
			
		||||
    getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
 | 
			
		||||
        const isHoistedRoot = hoistedNoteId === "root";
 | 
			
		||||
 | 
			
		||||
        const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
 | 
			
		||||
@@ -428,23 +428,7 @@ export default class FNote {
 | 
			
		||||
            isHidden: path.includes("_hidden")
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Calculate the length of the prefix match between two arrays
 | 
			
		||||
        const prefixMatchLength = (path: string[], target: string[]) => {
 | 
			
		||||
            const diffIndex = path.findIndex((seg, i) => seg !== target[i]);
 | 
			
		||||
            return diffIndex === -1 ? Math.min(path.length, target.length) : diffIndex;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        notePaths.sort((a, b) => {
 | 
			
		||||
            if (activeNotePath) {
 | 
			
		||||
                const activeSegments = activeNotePath.split('/');
 | 
			
		||||
                const aOverlap = prefixMatchLength(a.notePath, activeSegments);
 | 
			
		||||
                const bOverlap = prefixMatchLength(b.notePath, activeSegments);
 | 
			
		||||
                // Paths with more matching prefix segments are prioritized
 | 
			
		||||
                // when the match count is equal, other criteria are used for sorting
 | 
			
		||||
                if (bOverlap !== aOverlap) {
 | 
			
		||||
                    return bOverlap - aOverlap;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
 | 
			
		||||
                return a.isInHoistedSubTree ? -1 : 1;
 | 
			
		||||
            } else if (a.isArchived !== b.isArchived) {
 | 
			
		||||
@@ -465,11 +449,10 @@ export default class FNote {
 | 
			
		||||
     * Returns the note path considered to be the "best"
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} [hoistedNoteId='root']
 | 
			
		||||
     * @param {string|null} [activeNotePath=null]
 | 
			
		||||
     * @return {string[]} array of noteIds constituting the particular note path
 | 
			
		||||
     */
 | 
			
		||||
    getBestNotePath(hoistedNoteId = "root", activeNotePath: string | null = null) {
 | 
			
		||||
        return this.getSortedNotePathRecords(hoistedNoteId, activeNotePath)[0]?.notePath;
 | 
			
		||||
    getBestNotePath(hoistedNoteId = "root") {
 | 
			
		||||
        return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								apps/client/src/services/frontend_script_entrypoint.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,28 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The front script API is accessible to code notes with the "JS (frontend)" language.
 | 
			
		||||
 *
 | 
			
		||||
 * The entire API is exposed as a single global: {@link api}
 | 
			
		||||
 *
 | 
			
		||||
 * @module Frontend Script API
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This file creates the entrypoint for TypeDoc that simulates the context from within a
 | 
			
		||||
 * script note.
 | 
			
		||||
 *
 | 
			
		||||
 * Make sure to keep in line with frontend's `script_context.ts`.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type { default as BasicWidget } from "../widgets/basic_widget.js";
 | 
			
		||||
export type { default as FAttachment } from "../entities/fattachment.js";
 | 
			
		||||
export type { default as FAttribute } from "../entities/fattribute.js";
 | 
			
		||||
export type { default as FBranch } from "../entities/fbranch.js";
 | 
			
		||||
export type { default as FNote } from "../entities/fnote.js";
 | 
			
		||||
export type { Api } from "./frontend_script_api.js";
 | 
			
		||||
export type { default as NoteContextAwareWidget } from "../widgets/note_context_aware_widget.js";
 | 
			
		||||
export type { default as RightPanelWidget } from "../widgets/right_panel_widget.js";
 | 
			
		||||
 | 
			
		||||
import FrontendScriptApi, { type Api } from "./frontend_script_api.js";
 | 
			
		||||
 | 
			
		||||
//@ts-expect-error
 | 
			
		||||
export const api: Api = new FrontendScriptApi();
 | 
			
		||||
@@ -10,7 +10,7 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
 | 
			
		||||
    file: null,
 | 
			
		||||
    image: null,
 | 
			
		||||
    launcher: null,
 | 
			
		||||
    mermaid: "s1aBHPd79XYj",
 | 
			
		||||
    mermaid: null,
 | 
			
		||||
    mindMap: null,
 | 
			
		||||
    noteMap: null,
 | 
			
		||||
    relationMap: null,
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,21 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const path = notePath.split("/").reverse();
 | 
			
		||||
 | 
			
		||||
    if (!path.includes("root")) {
 | 
			
		||||
        path.push("root");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const effectivePathSegments: string[] = [];
 | 
			
		||||
    let childNoteId: string | null = null;
 | 
			
		||||
    let i = 0;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < path.length; i++) {
 | 
			
		||||
        const parentNoteId = path[i];
 | 
			
		||||
    while (true) {
 | 
			
		||||
        if (i >= path.length) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const parentNoteId = path[i++];
 | 
			
		||||
 | 
			
		||||
        if (childNoteId !== null) {
 | 
			
		||||
            const child = await froca.getNote(childNoteId, !logErrors);
 | 
			
		||||
@@ -56,7 +65,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!parents.some(p => p.noteId === parentNoteId) || (i === path.length - 1 && parentNoteId !== 'root')) {
 | 
			
		||||
            if (!parents.some((p) => p.noteId === parentNoteId)) {
 | 
			
		||||
                if (logErrors) {
 | 
			
		||||
                    const parent = froca.getNoteFromCache(parentNoteId);
 | 
			
		||||
 | 
			
		||||
@@ -68,8 +77,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const activeNotePath = appContext.tabManager.getActiveContextNotePath();
 | 
			
		||||
                const bestNotePath = child.getBestNotePath(hoistedNoteId, activeNotePath);
 | 
			
		||||
                const bestNotePath = child.getBestNotePath(hoistedNoteId);
 | 
			
		||||
 | 
			
		||||
                if (bestNotePath) {
 | 
			
		||||
                    const pathToRoot = bestNotePath.reverse().slice(1);
 | 
			
		||||
@@ -100,9 +108,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
 | 
			
		||||
        if (!note) {
 | 
			
		||||
            throw new Error(`Unable to find note: ${notePath}.`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const activeNotePath = appContext.tabManager.getActiveContextNotePath();
 | 
			
		||||
        const bestNotePath = note.getBestNotePath(hoistedNoteId, activeNotePath);
 | 
			
		||||
        const bestNotePath = note.getBestNotePath(hoistedNoteId);
 | 
			
		||||
 | 
			
		||||
        if (!bestNotePath) {
 | 
			
		||||
            throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,11 +11,7 @@ export function reloadFrontendApp(reason?: string) {
 | 
			
		||||
        logInfo(`Frontend app reload: ${reason}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow()?.reload();
 | 
			
		||||
    } else {
 | 
			
		||||
        window.location.reload();
 | 
			
		||||
    }
 | 
			
		||||
    window.location.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function restartDesktopApp() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								apps/client/src/share.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
import "normalize.css";
 | 
			
		||||
import "boxicons/css/boxicons.min.css";
 | 
			
		||||
import "@triliumnext/ckeditor5/src/theme/ck-content.css";
 | 
			
		||||
import "@triliumnext/share-theme/styles/index.css";
 | 
			
		||||
import "@triliumnext/share-theme/scripts/index.js";
 | 
			
		||||
 | 
			
		||||
async function ensureJQuery() {
 | 
			
		||||
    const $ = (await import("jquery")).default;
 | 
			
		||||
    (window as any).$ = $;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function formatCodeBlocks() {
 | 
			
		||||
    const anyCodeBlock = document.querySelector("#content pre");
 | 
			
		||||
    if (!anyCodeBlock) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await ensureJQuery();
 | 
			
		||||
    const { formatCodeBlocks } = await import("./services/syntax_highlight.js");
 | 
			
		||||
    await formatCodeBlocks($("#content"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function setupTextNote() {
 | 
			
		||||
    formatCodeBlocks();
 | 
			
		||||
}
 | 
			
		||||
@@ -716,6 +716,7 @@
 | 
			
		||||
    "backup_database_now": "نسخ اختياطي لقاعدة البيانات الان"
 | 
			
		||||
  },
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "wiki": "ويكي",
 | 
			
		||||
    "created": "تم الأنشاء",
 | 
			
		||||
    "actions": "أجراءات",
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@
 | 
			
		||||
    "bulk_actions_executed": "批量操作已成功执行。",
 | 
			
		||||
    "none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。",
 | 
			
		||||
    "labels": "标签",
 | 
			
		||||
    "relations": "关系",
 | 
			
		||||
    "relations": "关联关系",
 | 
			
		||||
    "notes": "笔记",
 | 
			
		||||
    "other": "其它"
 | 
			
		||||
  },
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_status": "导出状态",
 | 
			
		||||
    "export_in_progress": "导出进行中:{{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "导出成功完成。",
 | 
			
		||||
    "format_pdf": "PDF - 用于打印或共享目的。",
 | 
			
		||||
    "share-format": "HTML 网页发布——采用与共享笔记相同的主题,但可发布为静态网站。"
 | 
			
		||||
    "format_pdf": "PDF - 用于打印或共享目的。"
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "noteNavigation": "笔记导航",
 | 
			
		||||
@@ -185,8 +184,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "import-status": "导入状态",
 | 
			
		||||
    "in-progress": "导入进行中:{{progress}}",
 | 
			
		||||
    "successful": "导入成功完成。",
 | 
			
		||||
    "importZipRecommendation": "导入 ZIP 文件时,笔记层级将反映压缩文件内的子目录结构。"
 | 
			
		||||
    "successful": "导入成功完成。"
 | 
			
		||||
  },
 | 
			
		||||
  "include_note": {
 | 
			
		||||
    "dialog_title": "包含笔记",
 | 
			
		||||
@@ -261,6 +259,7 @@
 | 
			
		||||
    "delete_all_revisions": "删除此笔记的所有修订版本",
 | 
			
		||||
    "delete_all_button": "删除所有修订版本",
 | 
			
		||||
    "help_title": "关于笔记修订版本的帮助",
 | 
			
		||||
    "revision_last_edited": "此修订版本上次编辑于 {{date}}",
 | 
			
		||||
    "confirm_delete_all": "您是否要删除此笔记的所有修订版本?",
 | 
			
		||||
    "no_revisions": "此笔记暂无修订版本...",
 | 
			
		||||
    "restore_button": "恢复",
 | 
			
		||||
@@ -1289,6 +1288,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI 是一个 REST API,用于以编程方式访问 Trilium 实例,而无需 UI。",
 | 
			
		||||
    "see_more": "有关更多详细信息,请参见 {{- link_to_wiki}} 和 {{- link_to_openapi_spec}} 或 {{- link_to_swagger_ui}}。",
 | 
			
		||||
    "wiki": "维基",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI 规范",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "创建新的 ETAPI 令牌",
 | 
			
		||||
    "existing_tokens": "现有令牌",
 | 
			
		||||
    "no_tokens_yet": "目前还没有令牌。点击上面的按钮创建一个。",
 | 
			
		||||
@@ -1555,9 +1558,7 @@
 | 
			
		||||
    "window-on-top": "保持此窗口置顶"
 | 
			
		||||
  },
 | 
			
		||||
  "note_detail": {
 | 
			
		||||
    "could_not_find_typewidget": "找不到类型为 '{{type}}' 的 typeWidget",
 | 
			
		||||
    "printing": "正在打印…",
 | 
			
		||||
    "printing_pdf": "正在导出为PDF…"
 | 
			
		||||
    "could_not_find_typewidget": "找不到类型为 '{{type}}' 的 typeWidget"
 | 
			
		||||
  },
 | 
			
		||||
  "note_title": {
 | 
			
		||||
    "placeholder": "请输入笔记标题..."
 | 
			
		||||
 
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_status": "Exportstatus",
 | 
			
		||||
    "export_in_progress": "Export läuft: {{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "Der Export wurde erfolgreich abgeschlossen.",
 | 
			
		||||
    "format_pdf": "PDF - für Ausdrucke oder Teilen.",
 | 
			
		||||
    "share-format": "HTML für die Web-Veröffentlichung – verwendet dasselbe Theme wie bei freigegebenen Notizen, kann jedoch als statische Website veröffentlicht werden."
 | 
			
		||||
    "format_pdf": "PDF - für Ausdrucke oder Teilen."
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "noteNavigation": "Notiz Navigation",
 | 
			
		||||
@@ -261,6 +260,7 @@
 | 
			
		||||
    "delete_all_revisions": "Lösche alle Revisionen dieser Notiz",
 | 
			
		||||
    "delete_all_button": "Alle Revisionen löschen",
 | 
			
		||||
    "help_title": "Hilfe zu Notizrevisionen",
 | 
			
		||||
    "revision_last_edited": "Diese Revision wurde zuletzt am {{date}} bearbeitet",
 | 
			
		||||
    "confirm_delete_all": "Möchtest du alle Revisionen dieser Notiz löschen?",
 | 
			
		||||
    "no_revisions": "Für diese Notiz gibt es noch keine Revisionen...",
 | 
			
		||||
    "confirm_restore": "Möchtest du diese Revision wiederherstellen? Dadurch werden der aktuelle Titel und Inhalt der Notiz mit dieser Revision überschrieben.",
 | 
			
		||||
@@ -1286,6 +1286,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI ist eine REST-API, die für den programmgesteuerten Zugriff auf die Trilium-Instanz ohne Benutzeroberfläche verwendet wird.",
 | 
			
		||||
    "see_more": "Weitere Details können im {{- link_to_wiki}} und in der {{- link_to_openapi_spec}} oder der {{- link_to_swagger_ui }} gefunden werden.",
 | 
			
		||||
    "wiki": "Wiki",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI-Spezifikation",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Erstelle ein neues ETAPI-Token",
 | 
			
		||||
    "existing_tokens": "Vorhandene Token",
 | 
			
		||||
    "no_tokens_yet": "Es sind noch keine Token vorhanden. Klicke auf die Schaltfläche oben, um eine zu erstellen.",
 | 
			
		||||
 
 | 
			
		||||
@@ -261,6 +261,7 @@
 | 
			
		||||
    "delete_all_revisions": "Delete all revisions of this note",
 | 
			
		||||
    "delete_all_button": "Delete all revisions",
 | 
			
		||||
    "help_title": "Help on Note Revisions",
 | 
			
		||||
    "revision_last_edited": "This revision was last edited on {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Do you want to delete all revisions of this note?",
 | 
			
		||||
    "no_revisions": "No revisions for this note yet...",
 | 
			
		||||
    "restore_button": "Restore",
 | 
			
		||||
@@ -1453,6 +1454,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI is a REST API used to access Trilium instance programmatically, without UI.",
 | 
			
		||||
    "see_more": "See more details in the {{- link_to_wiki}} and the {{- link_to_openapi_spec}} or the {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI spec",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Create new ETAPI token",
 | 
			
		||||
    "existing_tokens": "Existing tokens",
 | 
			
		||||
    "no_tokens_yet": "There are no tokens yet. Click on the button above to create one.",
 | 
			
		||||
 
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_status": "Estado de exportación",
 | 
			
		||||
    "export_in_progress": "Exportación en curso: {{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "La exportación finalizó exitosamente.",
 | 
			
		||||
    "format_pdf": "PDF - para propósitos de impresión o compartición.",
 | 
			
		||||
    "share-format": "HTML para publicación web: utiliza el mismo tema que se utiliza en las notas compartidas, pero se puede publicar como un sitio web estático."
 | 
			
		||||
    "format_pdf": "PDF - para propósitos de impresión o compartición."
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "noteNavigation": "Navegación de notas",
 | 
			
		||||
@@ -185,8 +184,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "import-status": "Estado de importación",
 | 
			
		||||
    "in-progress": "Importación en progreso: {{progress}}",
 | 
			
		||||
    "successful": "Importación finalizada exitosamente.",
 | 
			
		||||
    "importZipRecommendation": "Al importar un archivo ZIP, la jerarquía de notas reflejará la estructura de subdirectorios dentro del archivo comprimido."
 | 
			
		||||
    "successful": "Importación finalizada exitosamente."
 | 
			
		||||
  },
 | 
			
		||||
  "include_note": {
 | 
			
		||||
    "dialog_title": "Incluir nota",
 | 
			
		||||
@@ -261,6 +259,7 @@
 | 
			
		||||
    "delete_all_revisions": "Eliminar todas las revisiones de esta nota",
 | 
			
		||||
    "delete_all_button": "Eliminar todas las revisiones",
 | 
			
		||||
    "help_title": "Ayuda sobre revisiones de notas",
 | 
			
		||||
    "revision_last_edited": "Esta revisión se editó por última vez en {{date}}",
 | 
			
		||||
    "confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
 | 
			
		||||
    "no_revisions": "Aún no hay revisiones para esta nota...",
 | 
			
		||||
    "restore_button": "Restaurar",
 | 
			
		||||
@@ -1446,6 +1445,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI es una REST API que se utiliza para acceder a la instancia de Trilium mediante programación, sin interfaz de usuario.",
 | 
			
		||||
    "see_more": "Véa más detalles en el {{- link_to_wiki}} y el {{- link_to_openapi_spec}} o el {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Especificación ETAPI OpenAPI",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Crear nuevo token ETAPI",
 | 
			
		||||
    "existing_tokens": "Tokens existentes",
 | 
			
		||||
    "no_tokens_yet": "Aún no hay tokens. Dé clic en el botón de arriba para crear uno.",
 | 
			
		||||
@@ -1712,9 +1715,7 @@
 | 
			
		||||
    "window-on-top": "Mantener esta ventana en la parte superior"
 | 
			
		||||
  },
 | 
			
		||||
  "note_detail": {
 | 
			
		||||
    "could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
 | 
			
		||||
    "printing": "Impresión en curso...",
 | 
			
		||||
    "printing_pdf": "Exportando a PDF en curso.."
 | 
			
		||||
    "could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'"
 | 
			
		||||
  },
 | 
			
		||||
  "note_title": {
 | 
			
		||||
    "placeholder": "escriba el título de la nota aquí..."
 | 
			
		||||
 
 | 
			
		||||
@@ -260,6 +260,7 @@
 | 
			
		||||
    "delete_all_revisions": "Supprimer toutes les versions de cette note",
 | 
			
		||||
    "delete_all_button": "Supprimer toutes les versions",
 | 
			
		||||
    "help_title": "Aide sur les versions de notes",
 | 
			
		||||
    "revision_last_edited": "Cette version a été modifiée pour la dernière fois le {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Voulez-vous supprimer toutes les versions de cette note ?",
 | 
			
		||||
    "no_revisions": "Aucune version pour cette note pour l'instant...",
 | 
			
		||||
    "confirm_restore": "Voulez-vous restaurer cette version ? Le titre et le contenu actuels de la note seront écrasés par cette version.",
 | 
			
		||||
@@ -1288,6 +1289,8 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI est une API REST utilisée pour accéder à l'instance Trilium par programme, sans interface utilisateur.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Spec ETAPI OpenAPI",
 | 
			
		||||
    "create_token": "Créer un nouveau jeton ETAPI",
 | 
			
		||||
    "existing_tokens": "Jetons existants",
 | 
			
		||||
    "no_tokens_yet": "Il n'y a pas encore de jetons. Cliquez sur le bouton ci-dessus pour en créer un.",
 | 
			
		||||
@@ -1304,7 +1307,9 @@
 | 
			
		||||
    "delete_token": "Supprimer/désactiver ce token",
 | 
			
		||||
    "rename_token_title": "Renommer le jeton",
 | 
			
		||||
    "rename_token_message": "Veuillez saisir le nom du nouveau jeton",
 | 
			
		||||
    "delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?"
 | 
			
		||||
    "delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?",
 | 
			
		||||
    "see_more": "Voir plus de détails dans le {{- link_to_wiki}} et le {{- link_to_openapi_spec}} ou le {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "swagger_ui": "Interface utilisateur ETAPI Swagger"
 | 
			
		||||
  },
 | 
			
		||||
  "options_widget": {
 | 
			
		||||
    "options_status": "Statut des options",
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,10 @@
 | 
			
		||||
    "new_token_message": "Inserisci il nome del nuovo token",
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI è un'API REST utilizzata per accedere alle istanze di Trilium in modo programmatico, senza interfaccia utente.",
 | 
			
		||||
    "see_more": "Per maggiori dettagli consulta {{- link_to_wiki}} e {{- link_to_openapi_spec}} o {{- link_to_swagger_ui}}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Specifiche ETAPI OpenAPI",
 | 
			
		||||
    "swagger_ui": "Interfaccia utente ETAPI Swagger",
 | 
			
		||||
    "create_token": "Crea un nuovo token ETAPI",
 | 
			
		||||
    "existing_tokens": "Token esistenti",
 | 
			
		||||
    "no_tokens_yet": "Non ci sono ancora token. Clicca sul pulsante qui sopra per crearne uno.",
 | 
			
		||||
@@ -863,6 +867,7 @@
 | 
			
		||||
    "delete_all_revisions": "Elimina tutte le revisioni di questa nota",
 | 
			
		||||
    "delete_all_button": "Elimina tutte le revisioni",
 | 
			
		||||
    "help_title": "Aiuto sulle revisioni delle note",
 | 
			
		||||
    "revision_last_edited": "Questa revisione è stata modificata l'ultima volta il {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Vuoi eliminare tutte le revisioni di questa nota?",
 | 
			
		||||
    "no_revisions": "Ancora nessuna revisione per questa nota...",
 | 
			
		||||
    "restore_button": "Ripristina",
 | 
			
		||||
 
 | 
			
		||||
@@ -254,8 +254,7 @@
 | 
			
		||||
    "export_status": "エクスポート状況",
 | 
			
		||||
    "export_in_progress": "エクスポート処理中: {{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "エクスポートが正常に完了しました。",
 | 
			
		||||
    "format_pdf": "PDF - 印刷または共有目的に。",
 | 
			
		||||
    "share-format": "Web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 Web サイトとして公開できます。"
 | 
			
		||||
    "format_pdf": "PDF - 印刷または共有目的に。"
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "title": "チートシート",
 | 
			
		||||
@@ -611,6 +610,7 @@
 | 
			
		||||
    "delete_all_revisions": "このノートの変更履歴をすべて削除",
 | 
			
		||||
    "delete_all_button": "変更履歴をすべて削除",
 | 
			
		||||
    "help_title": "変更履歴のヘルプ",
 | 
			
		||||
    "revision_last_edited": "この変更は{{date}}に行われました",
 | 
			
		||||
    "confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
 | 
			
		||||
    "no_revisions": "このノートに変更履歴はまだありません...",
 | 
			
		||||
    "restore_button": "復元",
 | 
			
		||||
@@ -657,6 +657,10 @@
 | 
			
		||||
    "created": "作成日時",
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI は、Trilium インスタンスに UI なしでプログラム的にアクセスするための REST API です。",
 | 
			
		||||
    "see_more": "詳細は{{- link_to_wiki}}と{{- link_to_openapi_spec}}または{{- link_to_swagger_ui }}を参照してください。",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPIの仕様",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "新しくETAPIトークンを作成",
 | 
			
		||||
    "existing_tokens": "既存のトークン",
 | 
			
		||||
    "no_tokens_yet": "トークンはまだありません。上のボタンをクリックして作成してください。",
 | 
			
		||||
 
 | 
			
		||||
@@ -912,6 +912,7 @@
 | 
			
		||||
    "delete_all_revisions": "Usuń wszystkie wersje tej notatki",
 | 
			
		||||
    "delete_all_button": "Usuń wszystkie wersje",
 | 
			
		||||
    "help_title": "Pomoc dotycząca wersji notatki",
 | 
			
		||||
    "revision_last_edited": "Ta wersja była ostatnio edytowana {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Czy chcesz usunąć wszystkie wersje tej notatki?",
 | 
			
		||||
    "no_revisions": "Brak wersji dla tej notatki...",
 | 
			
		||||
    "restore_button": "Przywróć",
 | 
			
		||||
@@ -1663,6 +1664,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI to interfejs API REST używany do programowego dostępu do instancji Trilium, bez interfejsu użytkownika.",
 | 
			
		||||
    "see_more": "Zobacz więcej szczegółów w {{- link_to_wiki}} oraz w {{- link_to_openapi_spec}} lub {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "specyfikacja ETAPI OpenAPI",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Utwórz nowy token ETAPI",
 | 
			
		||||
    "existing_tokens": "Istniejące tokeny",
 | 
			
		||||
    "no_tokens_yet": "Nie ma jeszcze żadnych tokenów. Kliknij przycisk powyżej, aby utworzyć jeden.",
 | 
			
		||||
 
 | 
			
		||||
@@ -259,6 +259,7 @@
 | 
			
		||||
    "delete_all_revisions": "Apagar todas as versões desta nota",
 | 
			
		||||
    "delete_all_button": "Apagar todas as versões",
 | 
			
		||||
    "help_title": "Ajuda sobre as versões da nota",
 | 
			
		||||
    "revision_last_edited": "Esta versão foi editada pela última vez em {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Quer apagar todas as versões desta nota?",
 | 
			
		||||
    "no_revisions": "Ainda não há versões para esta nota...",
 | 
			
		||||
    "restore_button": "Recuperar",
 | 
			
		||||
@@ -1422,6 +1423,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI é uma API REST usada para aceder a instância do Trilium programaticamente, sem interface gráfica.",
 | 
			
		||||
    "see_more": "Veja mais pormenores no {{- link_to_wiki}}, na {{- link_to_openapi_spec}} ou na {{- link_to_swagger_ui}}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Especificação OpenAPI do ETAPI",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Criar token ETAPI",
 | 
			
		||||
    "existing_tokens": "Tokens existentes",
 | 
			
		||||
    "no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",
 | 
			
		||||
 
 | 
			
		||||
@@ -415,6 +415,7 @@
 | 
			
		||||
    "delete_all_revisions": "Excluir todas as versões desta nota",
 | 
			
		||||
    "delete_all_button": "Excluir todas as versões",
 | 
			
		||||
    "help_title": "Ajuda sobre as versões da nota",
 | 
			
		||||
    "revision_last_edited": "Esta versão foi editada pela última vez em {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Você quer excluir todas as versões desta nota?",
 | 
			
		||||
    "no_revisions": "Ainda não há versões para esta nota...",
 | 
			
		||||
    "restore_button": "Recuperar",
 | 
			
		||||
@@ -1932,6 +1933,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI é uma API REST usada para acessar a instância do Trilium programaticamente, sem interface gráfica.",
 | 
			
		||||
    "see_more": "Veja mais detalhes no {{- link_to_wiki}}, na {{- link_to_openapi_spec}} ou na {{- link_to_swagger_ui}}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Especificação OpenAPI do ETAPI",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Criar novo token ETAPI",
 | 
			
		||||
    "existing_tokens": "Tokens existentes",
 | 
			
		||||
    "no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",
 | 
			
		||||
 
 | 
			
		||||
@@ -507,13 +507,17 @@
 | 
			
		||||
    "new_token_message": "Introduceți denumirea noului token",
 | 
			
		||||
    "new_token_title": "Token ETAPI nou",
 | 
			
		||||
    "no_tokens_yet": "Nu există încă token-uri. Clic pe butonul de deasupra pentru a crea una.",
 | 
			
		||||
    "openapi_spec": "Specificația OpenAPI pentru ETAPI",
 | 
			
		||||
    "swagger_ui": "UI-ul Swagger pentru ETAPI",
 | 
			
		||||
    "rename_token": "Redenumește token-ul",
 | 
			
		||||
    "rename_token_message": "Introduceți denumirea noului token",
 | 
			
		||||
    "rename_token_title": "Redenumire token",
 | 
			
		||||
    "see_more": "Vedeți mai multe detalii în {{- link_to_wiki}} și în {{- link_to_openapi_spec}} sau în {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "token_created_message": "Copiați token-ul creat în clipboard. Trilium stochează token-ul ca hash așadar această valoare poate fi văzută doar acum.",
 | 
			
		||||
    "token_created_title": "Token ETAPI creat",
 | 
			
		||||
    "token_name": "Denumire token"
 | 
			
		||||
    "token_name": "Denumire token",
 | 
			
		||||
    "wiki": "wiki"
 | 
			
		||||
  },
 | 
			
		||||
  "execute_script": {
 | 
			
		||||
    "example_1": "De exemplu, pentru a adăuga un șir de caractere la titlul unei notițe, se poate folosi acest mic script:",
 | 
			
		||||
@@ -1086,6 +1090,7 @@
 | 
			
		||||
    "preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
 | 
			
		||||
    "restore_button": "Restaurează",
 | 
			
		||||
    "revision_deleted": "Revizia notiței a fost ștearsă.",
 | 
			
		||||
    "revision_last_edited": "Revizia a fost ultima oară modificată pe {{date}}",
 | 
			
		||||
    "revision_restored": "Revizia notiței a fost restaurată.",
 | 
			
		||||
    "revisions_deleted": "Notița reviziei a fost ștearsă.",
 | 
			
		||||
    "maximum_revisions": "Numărul maxim de revizii pentru notița curentă: {{number}}.",
 | 
			
		||||
 
 | 
			
		||||
@@ -366,6 +366,7 @@
 | 
			
		||||
    "delete_all_button": "Удалить все версии",
 | 
			
		||||
    "help_title": "Помощь по версиям заметок",
 | 
			
		||||
    "confirm_delete_all": "Вы хотите удалить все версии этой заметки?",
 | 
			
		||||
    "revision_last_edited": "Эта версия последний раз редактировалась {{date}}",
 | 
			
		||||
    "confirm_restore": "Хотите восстановить эту версию? Текущее название и содержание заметки будут перезаписаны этой версией.",
 | 
			
		||||
    "confirm_delete": "Вы хотите удалить эту версию?",
 | 
			
		||||
    "revisions_deleted": "Версии заметки были удалены.",
 | 
			
		||||
@@ -1440,6 +1441,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "wiki": "вики",
 | 
			
		||||
    "created": "Создано",
 | 
			
		||||
    "actions": "Действия",
 | 
			
		||||
    "existing_tokens": "Существующие токены",
 | 
			
		||||
@@ -1447,7 +1449,10 @@
 | 
			
		||||
    "default_token_name": "новый токен",
 | 
			
		||||
    "rename_token_title": "Переименовать токен",
 | 
			
		||||
    "description": "ETAPI — это REST API, используемый для программного доступа к экземпляру Trilium без пользовательского интерфейса.",
 | 
			
		||||
    "see_more": "Более подробную информацию смотрите в {{- link_to_wiki}} и {{- link_to_openapi_spec}} или {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "create_token": "Создать новый токен ETAPI",
 | 
			
		||||
    "openapi_spec": "Спецификация ETAPI OpenAPI",
 | 
			
		||||
    "swagger_ui": "Пользовательский интерфейс ETAPI Swagger",
 | 
			
		||||
    "new_token_title": "Новый токен ETAPI",
 | 
			
		||||
    "token_created_title": "Создан токен ETAPI",
 | 
			
		||||
    "rename_token": "Переименовать этот токен",
 | 
			
		||||
 
 | 
			
		||||
@@ -256,6 +256,7 @@
 | 
			
		||||
        "delete_all_revisions": "Obriši sve revizije ove beleške",
 | 
			
		||||
        "delete_all_button": "Obriši sve revizije",
 | 
			
		||||
        "help_title": "Pomoć za Revizije beleški",
 | 
			
		||||
        "revision_last_edited": "Ova revizija je poslednji put izmenjena {{date}}",
 | 
			
		||||
        "confirm_delete_all": "Da li želite da obrišete sve revizije ove beleške?",
 | 
			
		||||
        "no_revisions": "Još uvek nema revizija za ovu belešku...",
 | 
			
		||||
        "restore_button": "Vrati",
 | 
			
		||||
 
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_in_progress": "正在匯出:{{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "成功匯出。",
 | 
			
		||||
    "format_html": "HTML - 推薦,因為它保留了所有格式",
 | 
			
		||||
    "format_pdf": "PDF - 用於列印或與他人分享。",
 | 
			
		||||
    "share-format": "HTML 網頁發佈——使用與共享筆記相同的佈景主題,但可發佈為靜態網站。"
 | 
			
		||||
    "format_pdf": "PDF - 用於列印或與他人分享。"
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "noteNavigation": "筆記導航",
 | 
			
		||||
@@ -261,6 +260,7 @@
 | 
			
		||||
    "delete_all_revisions": "刪除此筆記的所有歷史版本",
 | 
			
		||||
    "delete_all_button": "刪除所有歷史版本",
 | 
			
		||||
    "help_title": "關於筆記歷史版本的說明",
 | 
			
		||||
    "revision_last_edited": "此歷史版本上次於 {{date}} 編輯",
 | 
			
		||||
    "confirm_delete_all": "您是否要刪除此筆記的所有歷史版本?",
 | 
			
		||||
    "no_revisions": "此筆記暫無歷史版本…",
 | 
			
		||||
    "confirm_restore": "您是否要還原此歷史版本?這將使用此歷史版本覆寫筆記的目前標題和內容。",
 | 
			
		||||
@@ -1281,6 +1281,8 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI 是一個 REST API,用於以編程方式訪問 Trilium 實例,而無需 UI。",
 | 
			
		||||
    "wiki": "維基",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI 規範",
 | 
			
		||||
    "create_token": "新增 ETAPI 令牌",
 | 
			
		||||
    "existing_tokens": "現有令牌",
 | 
			
		||||
    "no_tokens_yet": "目前還沒有令牌。點擊上面的按鈕新增一個。",
 | 
			
		||||
@@ -1297,7 +1299,9 @@
 | 
			
		||||
    "delete_token": "刪除 / 停用此令牌",
 | 
			
		||||
    "rename_token_title": "重新命名令牌",
 | 
			
		||||
    "rename_token_message": "請輸入新的令牌名稱",
 | 
			
		||||
    "delete_token_confirmation": "您確定要刪除 ETAPI 令牌 \"{{name}}\" 嗎?"
 | 
			
		||||
    "delete_token_confirmation": "您確定要刪除 ETAPI 令牌 \"{{name}}\" 嗎?",
 | 
			
		||||
    "see_more": "有關更多詳細資訊,請參閱 {{- link_to_wiki}} 和 {{- link_to_openapi_spec}} 或 {{- link_to_swagger_ui}}。",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI"
 | 
			
		||||
  },
 | 
			
		||||
  "options_widget": {
 | 
			
		||||
    "options_status": "選項狀態",
 | 
			
		||||
 
 | 
			
		||||
@@ -309,6 +309,7 @@
 | 
			
		||||
    "delete_all_revisions": "Видалити всі версії цієї нотатки",
 | 
			
		||||
    "delete_all_button": "Видалити всі версії",
 | 
			
		||||
    "help_title": "Довідка щодо Версій нотаток",
 | 
			
		||||
    "revision_last_edited": "Цю версію востаннє редагували {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Ви хочете видалити всі версії цієї нотатки?",
 | 
			
		||||
    "no_revisions": "Поки що немає версій цієї нотатки...",
 | 
			
		||||
    "restore_button": "Відновити",
 | 
			
		||||
@@ -1402,6 +1403,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI — це REST API, який використовується для програмного доступу до екземпляра Trilium без інтерфейсу користувача.",
 | 
			
		||||
    "see_more": "Див. докладнішу інформацію у {{- link_to_wiki}} та {{- link_to_openapi_spec}} або {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "wiki": "вікі",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI spec",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Створити новий токен ETAPI",
 | 
			
		||||
    "existing_tokens": "Існуючі токени",
 | 
			
		||||
    "no_tokens_yet": "Токенів поки що немає. Натисніть кнопку вище, щоб створити його.",
 | 
			
		||||
 
 | 
			
		||||
@@ -79,8 +79,8 @@ export default function ExportDialog() {
 | 
			
		||||
                        values={[
 | 
			
		||||
                            { value: "html", label: t("export.format_html_zip") },
 | 
			
		||||
                            { value: "markdown", label: t("export.format_markdown") },
 | 
			
		||||
                            { value: "share", label: t("export.share-format") },
 | 
			
		||||
                            { value: "opml", label: t("export.format_opml") }
 | 
			
		||||
                            { value: "opml", label: t("export.format_opml") },
 | 
			
		||||
                            { value: "share", label: t("export.share-format") }
 | 
			
		||||
                        ]}
 | 
			
		||||
                    />
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -140,10 +140,11 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
 | 
			
		||||
        <FormList onSelect={onSelect} fullHeight>
 | 
			
		||||
            {revisions.map((item) =>
 | 
			
		||||
                <FormListItem
 | 
			
		||||
                    title={t("revisions.revision_last_edited", { date: item.dateLastEdited })}
 | 
			
		||||
                    value={item.revisionId}
 | 
			
		||||
                    active={currentRevision && item.revisionId === currentRevision.revisionId}
 | 
			
		||||
                >
 | 
			
		||||
                    {item.dateCreated && item.dateCreated.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
 | 
			
		||||
                    {item.dateLastEdited && item.dateLastEdited.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
 | 
			
		||||
                </FormListItem>
 | 
			
		||||
            )}
 | 
			
		||||
        </FormList>);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import type { ComponentChildren } from "preact";
 | 
			
		||||
import { CSSProperties } from "preact/compat";
 | 
			
		||||
 | 
			
		||||
interface OptionsSectionProps {
 | 
			
		||||
    title?: ComponentChildren;
 | 
			
		||||
    title?: string;
 | 
			
		||||
    children: ComponentChildren;
 | 
			
		||||
    noCard?: boolean;
 | 
			
		||||
    style?: CSSProperties;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ import dialog from "../../../services/dialog";
 | 
			
		||||
import { formatDateTime } from "../../../utils/formatters";
 | 
			
		||||
import ActionButton from "../../react/ActionButton";
 | 
			
		||||
import { useTriliumEvent } from "../../react/hooks";
 | 
			
		||||
import HelpButton from "../../react/HelpButton";
 | 
			
		||||
 | 
			
		||||
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
 | 
			
		||||
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;
 | 
			
		||||
@@ -49,13 +48,19 @@ export default function EtapiSettings() {
 | 
			
		||||
            message: t("etapi.token_created_message"),
 | 
			
		||||
            defaultValue: authToken
 | 
			
		||||
        });
 | 
			
		||||
    }, []);
 | 
			
		||||
    }, []);    
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <OptionsSection title={t("etapi.title")}>
 | 
			
		||||
            <FormText>
 | 
			
		||||
                {t("etapi.description")}
 | 
			
		||||
                <HelpButton helpPage="pgxEVkzLl1OP" />
 | 
			
		||||
                {t("etapi.description")}<br />
 | 
			
		||||
                <RawHtml
 | 
			
		||||
                    html={t("etapi.see_more", {
 | 
			
		||||
                        link_to_wiki: `<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a>`,
 | 
			
		||||
                        // TODO: We use window.open src/public/app/services/link.ts -> prevents regular click behavior on "a" element here because it's a relative path
 | 
			
		||||
                        link_to_openapi_spec: `<a class="tn-link" onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>`,
 | 
			
		||||
                        link_to_swagger_ui: `<a class="tn-link" href="#_help_f3xpgx6H01PW">${t("etapi.swagger_ui")}</a>`
 | 
			
		||||
                    })} />                    
 | 
			
		||||
            </FormText>
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
@@ -63,7 +68,6 @@ export default function EtapiSettings() {
 | 
			
		||||
                text={t("etapi.create_token")}
 | 
			
		||||
                onClick={createTokenCallback}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <hr />
 | 
			
		||||
 | 
			
		||||
            <h5>{t("etapi.existing_tokens")}</h5>
 | 
			
		||||
@@ -119,7 +123,7 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
 | 
			
		||||
                                                text={t("etapi.rename_token")}
 | 
			
		||||
                                                onClick={() => renameCallback(etapiTokenId, name)}
 | 
			
		||||
                                            />
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
                                            <ActionButton
 | 
			
		||||
                                                icon="bx bx-trash"
 | 
			
		||||
                                                text={t("etapi.delete_token")}
 | 
			
		||||
 
 | 
			
		||||
@@ -74,6 +74,7 @@ export default defineConfig(() => ({
 | 
			
		||||
                mobile: join(__dirname, "src", "mobile.ts"),
 | 
			
		||||
                login: join(__dirname, "src", "login.ts"),
 | 
			
		||||
                setup: join(__dirname, "src", "setup.ts"),
 | 
			
		||||
                share: join(__dirname, "src", "share.ts"),
 | 
			
		||||
                set_password: join(__dirname, "src", "set_password.ts"),
 | 
			
		||||
                runtime: join(__dirname, "src", "runtime.ts"),
 | 
			
		||||
                print: join(__dirname, "src", "print.tsx")
 | 
			
		||||
@@ -83,8 +84,7 @@ export default defineConfig(() => ({
 | 
			
		||||
                chunkFileNames: "src/[name].js",
 | 
			
		||||
                assetFileNames: "src/[name].[ext]",
 | 
			
		||||
                manualChunks: {
 | 
			
		||||
                    "ckeditor5": [ "@triliumnext/ckeditor5" ],
 | 
			
		||||
                    "boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
 | 
			
		||||
                    "ckeditor5": [ "@triliumnext/ckeditor5" ]
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            onwarn(warning, rollupWarn) {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@
 | 
			
		||||
    "@triliumnext/commons": "workspace:*",
 | 
			
		||||
    "@triliumnext/server": "workspace:*",
 | 
			
		||||
    "copy-webpack-plugin": "13.0.1",
 | 
			
		||||
    "electron": "38.5.0",
 | 
			
		||||
    "electron": "38.4.0",
 | 
			
		||||
    "@electron-forge/cli": "7.10.2",
 | 
			
		||||
    "@electron-forge/maker-deb": "7.10.2",
 | 
			
		||||
    "@electron-forge/maker-dmg": "7.10.2",
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
    "@triliumnext/desktop": "workspace:*",
 | 
			
		||||
    "@types/fs-extra": "11.0.4",
 | 
			
		||||
    "copy-webpack-plugin": "13.0.1",
 | 
			
		||||
    "electron": "38.5.0",
 | 
			
		||||
    "electron": "38.4.0",
 | 
			
		||||
    "fs-extra": "11.3.2"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,6 @@ if (!DOCS_ROOT || !USER_GUIDE_ROOT) {
 | 
			
		||||
    throw new Error("Missing DOCS_ROOT or USER_GUIDE_ROOT environment variable.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const BASE_URL = "https://docs.triliumnotes.org";
 | 
			
		||||
 | 
			
		||||
const NOTE_MAPPINGS: NoteMapping[] = [
 | 
			
		||||
    {
 | 
			
		||||
        rootNoteId: "pOsGYCXsbNQG",
 | 
			
		||||
@@ -160,14 +158,6 @@ async function cleanUpMeta(outputPath: string, minify: boolean) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        el.isExpanded = false;
 | 
			
		||||
 | 
			
		||||
        // Rewrite web view URLs that point to root.
 | 
			
		||||
        if (el.type === "webView" && minify) {
 | 
			
		||||
            const srcAttr = el.attributes.find(attr => attr.name === "webViewSrc");
 | 
			
		||||
            if (srcAttr.value.startsWith("/")) {
 | 
			
		||||
                srcAttr.value = BASE_URL + srcAttr.value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (minify) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										502
									
								
								apps/server-e2e/src/exact_search.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,502 @@
 | 
			
		||||
import { test, expect } from "@playwright/test";
 | 
			
		||||
import App from "./support/app";
 | 
			
		||||
 | 
			
		||||
const BASE_URL = "http://127.0.0.1:8082";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * E2E tests for exact search functionality using the leading "=" operator.
 | 
			
		||||
 *
 | 
			
		||||
 * These tests validate the GitHub issue:
 | 
			
		||||
 * - Searching for "pagio" returns many false positives (e.g., "page", "pages")
 | 
			
		||||
 * - Searching for "=pagio" should return ONLY exact matches for "pagio"
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
test.describe("Exact Search with Leading = Operator", () => {
 | 
			
		||||
    let csrfToken: string;
 | 
			
		||||
    let createdNoteIds: string[] = [];
 | 
			
		||||
 | 
			
		||||
    test.beforeEach(async ({ page, context }) => {
 | 
			
		||||
        const app = new App(page, context);
 | 
			
		||||
        await app.goto();
 | 
			
		||||
 | 
			
		||||
        // Get CSRF token
 | 
			
		||||
        csrfToken = await page.evaluate(() => {
 | 
			
		||||
            return (window as any).glob.csrfToken;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(csrfToken).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        // Create test notes with specific content patterns
 | 
			
		||||
        // Note 1: Contains exactly "pagio" in title
 | 
			
		||||
        const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Test Note with pagio",
 | 
			
		||||
                content: "This note contains the word pagio in the content.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note1.ok()).toBeTruthy();
 | 
			
		||||
        const note1Data = await note1.json();
 | 
			
		||||
        createdNoteIds.push(note1Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        // Note 2: Contains "page" (not exact match)
 | 
			
		||||
        const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Test Note with page",
 | 
			
		||||
                content: "This note contains the word page in the content.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note2.ok()).toBeTruthy();
 | 
			
		||||
        const note2Data = await note2.json();
 | 
			
		||||
        createdNoteIds.push(note2Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        // Note 3: Contains "pages" (plural, not exact match)
 | 
			
		||||
        const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Test Note with pages",
 | 
			
		||||
                content: "This note contains the word pages in the content.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note3.ok()).toBeTruthy();
 | 
			
		||||
        const note3Data = await note3.json();
 | 
			
		||||
        createdNoteIds.push(note3Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        // Note 4: Contains "homepage" (contains "page", not exact match)
 | 
			
		||||
        const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Homepage Note",
 | 
			
		||||
                content: "This note is about homepage content.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note4.ok()).toBeTruthy();
 | 
			
		||||
        const note4Data = await note4.json();
 | 
			
		||||
        createdNoteIds.push(note4Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        // Note 5: Another note with exact "pagio" in content
 | 
			
		||||
        const note5 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Another pagio Note",
 | 
			
		||||
                content: "This is another note with pagio content for testing exact matches.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note5.ok()).toBeTruthy();
 | 
			
		||||
        const note5Data = await note5.json();
 | 
			
		||||
        createdNoteIds.push(note5Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        // Note 6: Contains "pagio" in title only
 | 
			
		||||
        const note6 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "pagio",
 | 
			
		||||
                content: "This note has pagio as the title.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note6.ok()).toBeTruthy();
 | 
			
		||||
        const note6Data = await note6.json();
 | 
			
		||||
        createdNoteIds.push(note6Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        // Wait a bit for indexing
 | 
			
		||||
        await page.waitForTimeout(500);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test.afterEach(async ({ page }) => {
 | 
			
		||||
        // Clean up created notes
 | 
			
		||||
        for (const noteId of createdNoteIds) {
 | 
			
		||||
            try {
 | 
			
		||||
                const taskId = `cleanup-${Math.random().toString(36).substr(2, 9)}`;
 | 
			
		||||
                await page.request.delete(`${BASE_URL}/api/notes/${noteId}?taskId=${taskId}&last=true`, {
 | 
			
		||||
                    headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
                });
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error(`Failed to delete note ${noteId}:`, e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        createdNoteIds = [];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Quick search without = operator returns all partial matches", async ({ page }) => {
 | 
			
		||||
        // Test the /quick-search endpoint without the = operator
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/quick-search/pag`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        // Should return multiple notes including "page", "pages", "homepage"
 | 
			
		||||
        expect(data.searchResultNoteIds).toBeDefined();
 | 
			
		||||
        expect(data.searchResults).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        // Filter to only our test notes
 | 
			
		||||
        const testResults = data.searchResults.filter((result: any) =>
 | 
			
		||||
            result.noteTitle.includes("page") ||
 | 
			
		||||
            result.noteTitle.includes("pagio") ||
 | 
			
		||||
            result.noteTitle.includes("Homepage")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Should find at least "page", "pages", "homepage", and "pagio" notes
 | 
			
		||||
        expect(testResults.length).toBeGreaterThanOrEqual(4);
 | 
			
		||||
 | 
			
		||||
        console.log("Quick search 'pag' found:", testResults.length, "matching notes");
 | 
			
		||||
        console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Quick search with = operator returns only exact matches", async ({ page }) => {
 | 
			
		||||
        // Test the /quick-search endpoint WITH the = operator
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/quick-search/=pagio`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        // Should return only notes with exact "pagio" match
 | 
			
		||||
        expect(data.searchResultNoteIds).toBeDefined();
 | 
			
		||||
        expect(data.searchResults).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        // Filter to only our test notes
 | 
			
		||||
        const testResults = data.searchResults.filter((result: any) =>
 | 
			
		||||
            createdNoteIds.includes(result.notePath.split("/").pop() || "")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        console.log("Quick search '=pagio' found:", testResults.length, "matching notes");
 | 
			
		||||
        console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
 | 
			
		||||
 | 
			
		||||
        // Should find exactly 3 notes: "Test Note with pagio", "Another pagio Note", "pagio"
 | 
			
		||||
        expect(testResults.length).toBe(3);
 | 
			
		||||
 | 
			
		||||
        // Verify that none of the results contain "page" or "pages" (only "pagio")
 | 
			
		||||
        for (const result of testResults) {
 | 
			
		||||
            const title = result.noteTitle.toLowerCase();
 | 
			
		||||
            const hasPageNotPagio = (title.includes("page") && !title.includes("pagio"));
 | 
			
		||||
            expect(hasPageNotPagio).toBe(false);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Full search API without = operator returns partial matches", async ({ page }) => {
 | 
			
		||||
        // Test the /search endpoint without the = operator
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/search/pag`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        // Should return an array of note IDs
 | 
			
		||||
        expect(Array.isArray(data)).toBe(true);
 | 
			
		||||
 | 
			
		||||
        // Filter to only our test notes
 | 
			
		||||
        const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
 | 
			
		||||
 | 
			
		||||
        console.log("Full search 'pag' found:", testNoteIds.length, "matching notes from our test set");
 | 
			
		||||
 | 
			
		||||
        // Should find at least 4 notes
 | 
			
		||||
        expect(testNoteIds.length).toBeGreaterThanOrEqual(4);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Full search API with = operator returns only exact matches", async ({ page }) => {
 | 
			
		||||
        // Test the /search endpoint WITH the = operator
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/search/=pagio`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        // Should return an array of note IDs
 | 
			
		||||
        expect(Array.isArray(data)).toBe(true);
 | 
			
		||||
 | 
			
		||||
        // Filter to only our test notes
 | 
			
		||||
        const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
 | 
			
		||||
 | 
			
		||||
        console.log("Full search '=pagio' found:", testNoteIds.length, "matching notes from our test set");
 | 
			
		||||
 | 
			
		||||
        // Should find exactly 3 notes with exact "pagio" match
 | 
			
		||||
        expect(testNoteIds.length).toBe(3);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Exact search operator works with content search", async ({ page }) => {
 | 
			
		||||
        // Create a note with "test" in title but different content
 | 
			
		||||
        const noteWithTest = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Testing Content",
 | 
			
		||||
                content: "This note contains the exact word test in content.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(noteWithTest.ok()).toBeTruthy();
 | 
			
		||||
        const noteWithTestData = await noteWithTest.json();
 | 
			
		||||
        const testNoteId = noteWithTestData.note.noteId;
 | 
			
		||||
        createdNoteIds.push(testNoteId);
 | 
			
		||||
 | 
			
		||||
        // Create a note with "testing" (not exact match)
 | 
			
		||||
        const noteWithTesting = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Testing More",
 | 
			
		||||
                content: "This note has testing in the content.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(noteWithTesting.ok()).toBeTruthy();
 | 
			
		||||
        const noteWithTestingData = await noteWithTesting.json();
 | 
			
		||||
        createdNoteIds.push(noteWithTestingData.note.noteId);
 | 
			
		||||
 | 
			
		||||
        await page.waitForTimeout(500);
 | 
			
		||||
 | 
			
		||||
        // Search with exact operator
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/quick-search/=test`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        const ourTestNotes = data.searchResults.filter((result: any) => {
 | 
			
		||||
            const noteId = result.notePath.split("/").pop();
 | 
			
		||||
            return noteId === testNoteId || noteId === noteWithTestingData.note.noteId;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        console.log("Exact search '=test' found our test notes:", ourTestNotes.length);
 | 
			
		||||
        console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
 | 
			
		||||
 | 
			
		||||
        // Should find the note with exact "test" match, but not "testing"
 | 
			
		||||
        // Note: This test may fail if the implementation doesn't properly handle exact matching in content
 | 
			
		||||
        expect(ourTestNotes.length).toBeGreaterThan(0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Exact search is case-insensitive", async ({ page }) => {
 | 
			
		||||
        // Create notes with different case variations
 | 
			
		||||
        const noteUpper = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "EXACT MATCH",
 | 
			
		||||
                content: "This note has EXACT in uppercase.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(noteUpper.ok()).toBeTruthy();
 | 
			
		||||
        const noteUpperData = await noteUpper.json();
 | 
			
		||||
        createdNoteIds.push(noteUpperData.note.noteId);
 | 
			
		||||
 | 
			
		||||
        const noteLower = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "exact match",
 | 
			
		||||
                content: "This note has exact in lowercase.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(noteLower.ok()).toBeTruthy();
 | 
			
		||||
        const noteLowerData = await noteLower.json();
 | 
			
		||||
        createdNoteIds.push(noteLowerData.note.noteId);
 | 
			
		||||
 | 
			
		||||
        await page.waitForTimeout(500);
 | 
			
		||||
 | 
			
		||||
        // Search with exact operator in lowercase
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/quick-search/=exact`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        const ourTestNotes = data.searchResults.filter((result: any) => {
 | 
			
		||||
            const noteId = result.notePath.split("/").pop();
 | 
			
		||||
            return noteId === noteUpperData.note.noteId || noteId === noteLowerData.note.noteId;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        console.log("Case-insensitive exact search found:", ourTestNotes.length, "notes");
 | 
			
		||||
 | 
			
		||||
        // Should find both uppercase and lowercase versions
 | 
			
		||||
        expect(ourTestNotes.length).toBe(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Exact phrase matching with multi-word searches", async ({ page }) => {
 | 
			
		||||
        // Create notes with various phrase patterns
 | 
			
		||||
        const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "exact phrase",
 | 
			
		||||
                content: "This note contains the exact phrase.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note1.ok()).toBeTruthy();
 | 
			
		||||
        const note1Data = await note1.json();
 | 
			
		||||
        createdNoteIds.push(note1Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "exact phrase match",
 | 
			
		||||
                content: "This note has exact phrase followed by more words.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note2.ok()).toBeTruthy();
 | 
			
		||||
        const note2Data = await note2.json();
 | 
			
		||||
        createdNoteIds.push(note2Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "phrase exact",
 | 
			
		||||
                content: "This note has the words in reverse order.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note3.ok()).toBeTruthy();
 | 
			
		||||
        const note3Data = await note3.json();
 | 
			
		||||
        createdNoteIds.push(note3Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "this exact and that phrase",
 | 
			
		||||
                content: "Words are separated but both present.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(note4.ok()).toBeTruthy();
 | 
			
		||||
        const note4Data = await note4.json();
 | 
			
		||||
        createdNoteIds.push(note4Data.note.noteId);
 | 
			
		||||
 | 
			
		||||
        await page.waitForTimeout(500);
 | 
			
		||||
 | 
			
		||||
        // Search for exact phrase "exact phrase"
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/quick-search/='exact phrase'`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        const ourTestNotes = data.searchResults.filter((result: any) => {
 | 
			
		||||
            const noteId = result.notePath.split("/").pop();
 | 
			
		||||
            return [note1Data.note.noteId, note2Data.note.noteId, note3Data.note.noteId, note4Data.note.noteId].includes(noteId || "");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        console.log("Exact phrase search '=\"exact phrase\"' found:", ourTestNotes.length, "notes");
 | 
			
		||||
        console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
 | 
			
		||||
 | 
			
		||||
        // Should find only notes 1 and 2 (consecutive "exact phrase")
 | 
			
		||||
        // Should NOT find note 3 (reversed order) or note 4 (words separated)
 | 
			
		||||
        expect(ourTestNotes.length).toBe(2);
 | 
			
		||||
 | 
			
		||||
        const foundTitles = ourTestNotes.map((r: any) => r.noteTitle);
 | 
			
		||||
        expect(foundTitles).toContain("exact phrase");
 | 
			
		||||
        expect(foundTitles).toContain("exact phrase match");
 | 
			
		||||
        expect(foundTitles).not.toContain("phrase exact");
 | 
			
		||||
        expect(foundTitles).not.toContain("this exact and that phrase");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Exact phrase matching respects word order", async ({ page }) => {
 | 
			
		||||
        // Create notes to test word order sensitivity
 | 
			
		||||
        const noteForward = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Testing Order",
 | 
			
		||||
                content: "This is a test sentence for verification.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(noteForward.ok()).toBeTruthy();
 | 
			
		||||
        const noteForwardData = await noteForward.json();
 | 
			
		||||
        createdNoteIds.push(noteForwardData.note.noteId);
 | 
			
		||||
 | 
			
		||||
        const noteReverse = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Order Testing",
 | 
			
		||||
                content: "A sentence test is this for verification.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(noteReverse.ok()).toBeTruthy();
 | 
			
		||||
        const noteReverseData = await noteReverse.json();
 | 
			
		||||
        createdNoteIds.push(noteReverseData.note.noteId);
 | 
			
		||||
 | 
			
		||||
        await page.waitForTimeout(500);
 | 
			
		||||
 | 
			
		||||
        // Search for exact phrase "test sentence"
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/quick-search/='test sentence'`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        const ourTestNotes = data.searchResults.filter((result: any) => {
 | 
			
		||||
            const noteId = result.notePath.split("/").pop();
 | 
			
		||||
            return noteId === noteForwardData.note.noteId || noteId === noteReverseData.note.noteId;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        console.log("Exact phrase search '=\"test sentence\"' found:", ourTestNotes.length, "notes");
 | 
			
		||||
        console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
 | 
			
		||||
 | 
			
		||||
        // Should find only the forward order note
 | 
			
		||||
        expect(ourTestNotes.length).toBe(1);
 | 
			
		||||
        expect(ourTestNotes[0].noteTitle).toBe("Testing Order");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("Multi-word exact search without quotes", async ({ page }) => {
 | 
			
		||||
        // Test that multi-word search with = but without quotes also does exact phrase matching
 | 
			
		||||
        const notePhrase = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Quick Test Note",
 | 
			
		||||
                content: "A simple note for multi word testing.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(notePhrase.ok()).toBeTruthy();
 | 
			
		||||
        const notePhraseData = await notePhrase.json();
 | 
			
		||||
        createdNoteIds.push(notePhraseData.note.noteId);
 | 
			
		||||
 | 
			
		||||
        const noteScattered = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken },
 | 
			
		||||
            data: {
 | 
			
		||||
                title: "Word Multi Testing",
 | 
			
		||||
                content: "Words are multi scattered in this testing example.",
 | 
			
		||||
                type: "text"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        expect(noteScattered.ok()).toBeTruthy();
 | 
			
		||||
        const noteScatteredData = await noteScattered.json();
 | 
			
		||||
        createdNoteIds.push(noteScatteredData.note.noteId);
 | 
			
		||||
 | 
			
		||||
        await page.waitForTimeout(500);
 | 
			
		||||
 | 
			
		||||
        // Search for "=multi word" without quotes (parser tokenizes as two words)
 | 
			
		||||
        const response = await page.request.get(`${BASE_URL}/api/quick-search/=multi word`, {
 | 
			
		||||
            headers: { "x-csrf-token": csrfToken }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.ok()).toBeTruthy();
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        const ourTestNotes = data.searchResults.filter((result: any) => {
 | 
			
		||||
            const noteId = result.notePath.split("/").pop();
 | 
			
		||||
            return noteId === notePhraseData.note.noteId || noteId === noteScatteredData.note.noteId;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        console.log("Multi-word exact search '=multi word' found:", ourTestNotes.length, "notes");
 | 
			
		||||
        console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
 | 
			
		||||
 | 
			
		||||
        // Should find only the note with consecutive "multi word" phrase
 | 
			
		||||
        expect(ourTestNotes.length).toBe(1);
 | 
			
		||||
        expect(ourTestNotes[0].noteTitle).toBe("Quick Test Note");
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:24.11.0-bullseye-slim AS builder
 | 
			
		||||
FROM node:24.10.0-bullseye-slim AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:24.11.0-bullseye-slim
 | 
			
		||||
FROM node:24.10.0-bullseye-slim
 | 
			
		||||
# Install only runtime dependencies
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
    apt-get install -y --no-install-recommends \
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:24.11.0-alpine AS builder
 | 
			
		||||
FROM node:24.10.0-alpine AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:24.11.0-alpine
 | 
			
		||||
FROM node:24.10.0-alpine
 | 
			
		||||
# Install runtime dependencies
 | 
			
		||||
RUN apk add --no-cache su-exec shadow
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:24.11.0-alpine AS builder
 | 
			
		||||
FROM node:24.10.0-alpine AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:24.11.0-alpine
 | 
			
		||||
FROM node:24.10.0-alpine
 | 
			
		||||
# Create a non-root user with configurable UID/GID
 | 
			
		||||
ARG USER=trilium
 | 
			
		||||
ARG UID=1001
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
FROM node:22.21.0-bullseye-slim AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
WORKDIR /usr/src/app/build
 | 
			
		||||
COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:22.21.0-bullseye-slim
 | 
			
		||||
# Install only runtime dependencies
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
    apt-get install -y --no-install-recommends \
 | 
			
		||||
    gosu && \
 | 
			
		||||
    rm -rf \
 | 
			
		||||
    /var/lib/apt/lists/* \
 | 
			
		||||
    /var/cache/apt/*
 | 
			
		||||
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
COPY ./dist /usr/src/app
 | 
			
		||||
RUN rm -rf /usr/src/app/node_modules/better-sqlite3
 | 
			
		||||
COPY --from=builder /usr/src/app/node_modules/better-sqlite3 /usr/src/app/node_modules/better-sqlite3
 | 
			
		||||
COPY ./start-docker.sh /usr/src/app
 | 
			
		||||
 | 
			
		||||
# Configure container
 | 
			
		||||
EXPOSE 8080
 | 
			
		||||
CMD [ "sh", "./start-docker.sh" ]
 | 
			
		||||
HEALTHCHECK --start-period=10s CMD exec gosu node node /usr/src/app/docker_healthcheck.cjs
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:24.11.0-bullseye-slim AS builder
 | 
			
		||||
FROM node:24.10.0-bullseye-slim AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:24.11.0-bullseye-slim
 | 
			
		||||
FROM node:24.10.0-bullseye-slim
 | 
			
		||||
# Create a non-root user with configurable UID/GID
 | 
			
		||||
ARG USER=trilium
 | 
			
		||||
ARG UID=1001
 | 
			
		||||
 
 | 
			
		||||
@@ -26,18 +26,17 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "better-sqlite3": "12.4.1",
 | 
			
		||||
    "html-to-text": "9.0.5",
 | 
			
		||||
    "node-html-parser": "7.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@anthropic-ai/sdk": "0.68.0",
 | 
			
		||||
    "@anthropic-ai/sdk": "0.67.0",
 | 
			
		||||
    "@braintree/sanitize-url": "7.1.1",
 | 
			
		||||
    "@electron/remote": "2.1.3",
 | 
			
		||||
    "@preact/preset-vite": "2.10.2",
 | 
			
		||||
    "@triliumnext/commons": "workspace:*",
 | 
			
		||||
    "@triliumnext/express-partial-content": "workspace:*",
 | 
			
		||||
    "@triliumnext/highlightjs": "workspace:*",
 | 
			
		||||
    "@triliumnext/turndown-plugin-gfm": "workspace:*",
 | 
			
		||||
    "@triliumnext/highlightjs": "workspace:*",
 | 
			
		||||
    "@types/archiver": "7.0.0",
 | 
			
		||||
    "@types/better-sqlite3": "7.6.13",
 | 
			
		||||
    "@types/cls-hooked": "4.3.9",
 | 
			
		||||
@@ -61,34 +60,35 @@
 | 
			
		||||
    "@types/serve-static": "2.2.0",
 | 
			
		||||
    "@types/stream-throttle": "0.1.4",
 | 
			
		||||
    "@types/supertest": "6.0.3",
 | 
			
		||||
    "@types/swagger-ui-express": "4.1.8",
 | 
			
		||||
    "@types/tmp": "0.2.6",
 | 
			
		||||
    "@types/turndown": "5.0.6",
 | 
			
		||||
    "@types/ws": "8.18.1",
 | 
			
		||||
    "@types/xml2js": "0.4.14",
 | 
			
		||||
    "archiver": "7.0.1",
 | 
			
		||||
    "async-mutex": "0.5.0",
 | 
			
		||||
    "axios": "1.13.1",
 | 
			
		||||
    "axios": "1.13.0",
 | 
			
		||||
    "bindings": "1.5.0",
 | 
			
		||||
    "bootstrap": "5.3.8",
 | 
			
		||||
    "chardet": "2.1.1",
 | 
			
		||||
    "chardet": "2.1.0",
 | 
			
		||||
    "cheerio": "1.1.2",
 | 
			
		||||
    "chokidar": "4.0.3",
 | 
			
		||||
    "cls-hooked": "4.2.2",
 | 
			
		||||
    "compression": "1.8.1",
 | 
			
		||||
    "cookie-parser": "1.4.7",
 | 
			
		||||
    "csrf-csrf": "3.2.2",
 | 
			
		||||
    "dayjs": "1.11.19",
 | 
			
		||||
    "dayjs": "1.11.18",
 | 
			
		||||
    "debounce": "2.2.0",
 | 
			
		||||
    "debug": "4.4.3",
 | 
			
		||||
    "ejs": "3.1.10",
 | 
			
		||||
    "electron": "38.5.0",
 | 
			
		||||
    "electron": "38.4.0",
 | 
			
		||||
    "electron-debug": "4.1.0",
 | 
			
		||||
    "electron-window-state": "5.0.3",
 | 
			
		||||
    "escape-html": "1.0.3",
 | 
			
		||||
    "express": "5.1.0",
 | 
			
		||||
    "express-http-proxy": "2.1.2",
 | 
			
		||||
    "express-openid-connect": "2.19.2",
 | 
			
		||||
    "express-rate-limit": "8.2.1",
 | 
			
		||||
    "express-rate-limit": "8.1.0",
 | 
			
		||||
    "express-session": "1.18.2",
 | 
			
		||||
    "file-uri-to-path": "2.0.0",
 | 
			
		||||
    "fs-extra": "11.3.2",
 | 
			
		||||
@@ -109,7 +109,7 @@
 | 
			
		||||
    "mime-types": "3.0.1",
 | 
			
		||||
    "multer": "2.0.2",
 | 
			
		||||
    "normalize-strings": "1.1.1",
 | 
			
		||||
    "ollama": "0.6.2",
 | 
			
		||||
    "ollama": "0.6.0",
 | 
			
		||||
    "openai": "6.7.0",
 | 
			
		||||
    "rand-token": "1.0.1",
 | 
			
		||||
    "safe-compare": "1.1.4",
 | 
			
		||||
@@ -122,6 +122,7 @@
 | 
			
		||||
    "striptags": "3.2.0",
 | 
			
		||||
    "supertest": "7.1.4",
 | 
			
		||||
    "swagger-jsdoc": "6.2.8",
 | 
			
		||||
    "swagger-ui-express": "5.0.1",
 | 
			
		||||
    "time2fa": "1.4.2",
 | 
			
		||||
    "tmp": "0.2.5",
 | 
			
		||||
    "turndown": "7.2.2",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
openapi: 3.1.0
 | 
			
		||||
info:
 | 
			
		||||
  title: Internal Trilium API
 | 
			
		||||
  version: 0.99.3
 | 
			
		||||
  title: Trilium Notes Internal API
 | 
			
		||||
  version: 0.98.0
 | 
			
		||||
  description: |
 | 
			
		||||
    This is the internal API used by the Trilium Notes client application.
 | 
			
		||||
    
 | 
			
		||||
@@ -24,12 +24,11 @@ info:
 | 
			
		||||
    State-changing operations require CSRF tokens when using session authentication.
 | 
			
		||||
    
 | 
			
		||||
  contact:
 | 
			
		||||
        name: Trilium Notes Team
 | 
			
		||||
        email: contact@eliandoran.me
 | 
			
		||||
        url: https://triliumnotes.org
 | 
			
		||||
    name: TriliumNext Issue Tracker
 | 
			
		||||
    url: https://github.com/TriliumNext/Trilium/issues
 | 
			
		||||
  license:
 | 
			
		||||
      name: GNU Affero General Public License v3.0 only
 | 
			
		||||
      url: https://www.gnu.org/licenses/agpl-3.0.en.html
 | 
			
		||||
    name: GNU Affero General Public License v3.0
 | 
			
		||||
    url: https://www.gnu.org/licenses/agpl-3.0.html
 | 
			
		||||
    
 | 
			
		||||
servers:
 | 
			
		||||
  - url: http://localhost:8080
 | 
			
		||||
							
								
								
									
										2
									
								
								apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB  | 
| 
		 Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB  | 
| 
		 Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB  | 
| 
		 Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB  | 
| 
		 Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB  | 
| 
		 Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 237 KiB  | 
| 
		 Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB  | 
| 
		 Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB  | 
| 
		 Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB  | 
| 
		 Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 191 KiB  | 
@@ -11,12 +11,12 @@
 | 
			
		||||
<p>To set your preferred chat model, you'll want to enter the provider's
 | 
			
		||||
  name here:</p>
 | 
			
		||||
<figure class="image image_resized" style="width:88.38%;">
 | 
			
		||||
  <img style="aspect-ratio:1884/1267;" src="Providers_image.png"
 | 
			
		||||
  <img style="aspect-ratio:1884/1267;" src="AI Provider Information_im.png"
 | 
			
		||||
  width="1884" height="1267">
 | 
			
		||||
</figure>
 | 
			
		||||
<p>And to set your preferred embedding provider:</p>
 | 
			
		||||
<figure class="image image_resized"
 | 
			
		||||
style="width:93.47%;">
 | 
			
		||||
  <img style="aspect-ratio:1907/1002;" src="1_Providers_image.png"
 | 
			
		||||
  <img style="aspect-ratio:1907/1002;" src="1_AI Provider Information_im.png"
 | 
			
		||||
  width="1907" height="1002">
 | 
			
		||||
</figure>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB  | 
| 
		 Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 270 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB  | 
| 
		 Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB  | 
| 
		 Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB  | 
@@ -9,8 +9,8 @@ class="image image_resized" style="width:50.49%;">
 | 
			
		||||
  width="785" height="498">
 | 
			
		||||
  </figure>
 | 
			
		||||
  <figure class="image image_resized" style="width:40.54%;">
 | 
			
		||||
    <img style="aspect-ratio:467/100;" src="Installing Ollama_image.png"
 | 
			
		||||
    width="467" height="100">
 | 
			
		||||
    <img style="aspect-ratio:467/100;" src="Installing Ollama_image.png" width="467"
 | 
			
		||||
    height="100">
 | 
			
		||||
  </figure>
 | 
			
		||||
  <figure class="image image_resized" style="width:55.73%;">
 | 
			
		||||
    <img style="aspect-ratio:1296/1011;" src="1_Installing Ollama_image.png"
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB  | 
| 
		 Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 198 KiB  | 
@@ -1,6 +1,6 @@
 | 
			
		||||
<figure class="image image_resized" style="width:63.68%;">
 | 
			
		||||
  <img style="aspect-ratio:1363/1364;" src="AI_image.png"
 | 
			
		||||
  width="1363" height="1364">
 | 
			
		||||
  <img style="aspect-ratio:1363/1364;" src="Introduction_image.png" width="1363"
 | 
			
		||||
  height="1364">
 | 
			
		||||
  <figcaption>An example chat with an LLM</figcaption>
 | 
			
		||||
</figure>
 | 
			
		||||
<p>The AI / LLM features within Trilium Notes are designed to allow you to
 | 
			
		||||
@@ -11,13 +11,13 @@
 | 
			
		||||
<p>The quickest way to get started is to navigate to the “AI/LLM” settings:</p>
 | 
			
		||||
<figure
 | 
			
		||||
class="image image_resized" style="width:74.04%;">
 | 
			
		||||
  <img style="aspect-ratio:1916/1906;" src="5_AI_image.png"
 | 
			
		||||
  width="1916" height="1906">
 | 
			
		||||
  <img style="aspect-ratio:1916/1906;" src="5_Introduction_image.png" width="1916"
 | 
			
		||||
  height="1906">
 | 
			
		||||
  </figure>
 | 
			
		||||
  <p>Enable the feature:</p>
 | 
			
		||||
  <figure class="image image_resized" style="width:82.82%;">
 | 
			
		||||
    <img style="aspect-ratio:1911/997;" src="1_AI_image.png"
 | 
			
		||||
    width="1911" height="997">
 | 
			
		||||
    <img style="aspect-ratio:1911/997;" src="1_Introduction_image.png" width="1911"
 | 
			
		||||
    height="997">
 | 
			
		||||
  </figure>
 | 
			
		||||
  
 | 
			
		||||
<h2>Embeddings</h2>
 | 
			
		||||
@@ -43,30 +43,30 @@ class="image image_resized" style="width:74.04%;">
 | 
			
		||||
    We will then hit the “refresh” button to have it fetch our models:</p>
 | 
			
		||||
  <figure
 | 
			
		||||
  class="image image_resized" style="width:82.28%;">
 | 
			
		||||
    <img style="aspect-ratio:1912/1075;" src="4_AI_image.png"
 | 
			
		||||
    width="1912" height="1075">
 | 
			
		||||
    <img style="aspect-ratio:1912/1075;" src="4_Introduction_image.png" width="1912"
 | 
			
		||||
    height="1075">
 | 
			
		||||
    </figure>
 | 
			
		||||
    <p>When selecting the dropdown for the “Embedding Model”, embedding models
 | 
			
		||||
      should be at the top of the list, separated by regular chat models with
 | 
			
		||||
      a horizontal line, as seen below:</p>
 | 
			
		||||
    <figure class="image image_resized"
 | 
			
		||||
    style="width:61.73%;">
 | 
			
		||||
      <img style="aspect-ratio:1232/959;" src="8_AI_image.png"
 | 
			
		||||
      width="1232" height="959">
 | 
			
		||||
      <img style="aspect-ratio:1232/959;" src="8_Introduction_image.png" width="1232"
 | 
			
		||||
      height="959">
 | 
			
		||||
    </figure>
 | 
			
		||||
    <p>After selecting an embedding model, embeddings should automatically begin
 | 
			
		||||
      to be generated by checking the embedding statistics at the top of the
 | 
			
		||||
      “AI/LLM” settings panel:</p>
 | 
			
		||||
    <figure class="image image_resized" style="width:67.06%;">
 | 
			
		||||
      <img style="aspect-ratio:1333/499;" src="7_AI_image.png"
 | 
			
		||||
      width="1333" height="499">
 | 
			
		||||
      <img style="aspect-ratio:1333/499;" src="7_Introduction_image.png" width="1333"
 | 
			
		||||
      height="499">
 | 
			
		||||
    </figure>
 | 
			
		||||
    <p>If you don't see any embeddings being created, you will want to scroll
 | 
			
		||||
      to the bottom of the settings, and hit “Recreate All Embeddings”:</p>
 | 
			
		||||
    <figure
 | 
			
		||||
    class="image image_resized" style="width:65.69%;">
 | 
			
		||||
      <img style="aspect-ratio:1337/1490;" src="3_AI_image.png"
 | 
			
		||||
      width="1337" height="1490">
 | 
			
		||||
      <img style="aspect-ratio:1337/1490;" src="3_Introduction_image.png" width="1337"
 | 
			
		||||
      height="1490">
 | 
			
		||||
      </figure>
 | 
			
		||||
      <p>Creating the embeddings will take some time, and will be regenerated when
 | 
			
		||||
        a Note is created, updated, or deleted (removed).</p>
 | 
			
		||||
@@ -139,8 +139,8 @@ class="image image_resized" style="width:74.04%;">
 | 
			
		||||
      <p>When Tools are executed within your Chat, you'll see output like the following:</p>
 | 
			
		||||
      <figure
 | 
			
		||||
      class="image image_resized" style="width:66.88%;">
 | 
			
		||||
        <img style="aspect-ratio:1372/1591;" src="6_AI_image.png"
 | 
			
		||||
        width="1372" height="1591">
 | 
			
		||||
        <img style="aspect-ratio:1372/1591;" src="6_Introduction_image.png" width="1372"
 | 
			
		||||
        height="1591">
 | 
			
		||||
        </figure>
 | 
			
		||||
        <p>You don't need to tell the LLM to execute a certain tool, it should “smartly”
 | 
			
		||||
          call tools and automatically execute them as needed.</p>
 | 
			
		||||
@@ -149,13 +149,13 @@ class="image image_resized" style="width:74.04%;">
 | 
			
		||||
          use the “Chat with Notes” button, where you can go ahead and start chatting!:</p>
 | 
			
		||||
        <figure
 | 
			
		||||
        class="image image_resized" style="width:60.77%;">
 | 
			
		||||
          <img style="aspect-ratio:1378/539;" src="2_AI_image.png"
 | 
			
		||||
          width="1378" height="539">
 | 
			
		||||
          <img style="aspect-ratio:1378/539;" src="2_Introduction_image.png" width="1378"
 | 
			
		||||
          height="539">
 | 
			
		||||
          </figure>
 | 
			
		||||
          <p>If you don't see the “Chat with Notes” button on your side launchbar,
 | 
			
		||||
            you might need to move it from the “Available Launchers” section to the
 | 
			
		||||
            “Visible Launchers” section:</p>
 | 
			
		||||
          <figure class="image image_resized" style="width:69.81%;">
 | 
			
		||||
            <img style="aspect-ratio:1765/1287;" src="9_AI_image.png"
 | 
			
		||||
            width="1765" height="1287">
 | 
			
		||||
            <img style="aspect-ratio:1765/1287;" src="9_Introduction_image.png" width="1765"
 | 
			
		||||
            height="1287">
 | 
			
		||||
          </figure>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB  | 
@@ -10,9 +10,9 @@
 | 
			
		||||
  and arbitrary tags - whenever you change tag attribute in the task note,
 | 
			
		||||
  this task is then automatically moved to appropriate location.</p>
 | 
			
		||||
<p>Task Manager also integrates with <a href="#root/_help_l0tKav7yLHGF">day notes</a> -
 | 
			
		||||
  notes are <a href="#root/_help_IakOLONlIfGI">cloned</a> into day note to both todoDate
 | 
			
		||||
  note and doneDate note (with <a href="#root/_help_kBrnXNG3Hplm">prefix</a> of either
 | 
			
		||||
  "TODO" or "DONE").</p>
 | 
			
		||||
  notes are <a href="#root/_help_IakOLONlIfGI">cloned</a> into day note to
 | 
			
		||||
  both todoDate note and doneDate note (with <a href="#root/_help_kBrnXNG3Hplm">prefix</a> of
 | 
			
		||||
  either "TODO" or "DONE").</p>
 | 
			
		||||
<h2>Implementation</h2>
 | 
			
		||||
<p>New tasks are created in the TODO note which has <code>~child:template</code> 
 | 
			
		||||
  <a
 | 
			
		||||
@@ -22,9 +22,9 @@
 | 
			
		||||
<p>Task template defines several <a href="#root/_help_OFXdgB2nNk1F">promoted attributes</a> -
 | 
			
		||||
  todoDate, doneDate, tags, location. Importantly it also defines <code>~runOnAttributeChange</code> relation
 | 
			
		||||
  - <a href="#root/_help_GPERMystNGTB">event</a> handler which is run on attribute
 | 
			
		||||
  change. This <a href="#root/_help_CdNpE2pqjmI6">script</a> handles when e.g. we
 | 
			
		||||
  fill out the doneDate attribute - meaning the task is done and should be
 | 
			
		||||
  moved to "Done" note and removed from TODO, locations and tags.</p>
 | 
			
		||||
  change. This <a href="#root/_help_CdNpE2pqjmI6">script</a> handles when e.g.
 | 
			
		||||
  we fill out the doneDate attribute - meaning the task is done and should
 | 
			
		||||
  be moved to "Done" note and removed from TODO, locations and tags.</p>
 | 
			
		||||
<h3>New task button</h3>
 | 
			
		||||
<p>There's also "button" note which contains simple script which adds a button
 | 
			
		||||
  to create new note (task) in the TODO note.</p><pre><code class="language-text-x-trilium-auto">api.addButtonToToolbar({
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<figure class="image">
 | 
			
		||||
  <img style="aspect-ratio:1071/146;" src="Attributes_image.png"
 | 
			
		||||
  width="1071" height="146">
 | 
			
		||||
  <img style="aspect-ratio:1071/146;" src="Attributes_image.png" width="1071"
 | 
			
		||||
  height="146">
 | 
			
		||||
</figure>
 | 
			
		||||
<p>In Trilium, attributes are key-value pairs assigned to notes, providing
 | 
			
		||||
  additional metadata or functionality. There are two primary types of attributes:</p>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<p>Inheritance refers to the process of having a <a href="#root/_help_HI6GBBIduIgv">label</a> or
 | 
			
		||||
  a <a href="#root/_help_Cq5X6iKQop6R">relation</a> shared across multiple notes,
 | 
			
		||||
  generally in parent-child relations (or anywhere if using templates).</p>
 | 
			
		||||
  a <a href="#root/_help_Cq5X6iKQop6R">relation</a> shared across multiple
 | 
			
		||||
  notes, generally in parent-child relations (or anywhere if using templates).</p>
 | 
			
		||||
<h2>Standard Inheritance</h2>
 | 
			
		||||
<p>In Trilium, attributes can be automatically inherited by child notes if
 | 
			
		||||
  they have the <code>isInheritable</code> flag set to <code>true</code>. This
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<p>A label is an <a href="#root/_help_zEY4DaJG4YT5">attribute</a> of a note which
 | 
			
		||||
  has a name and optionally a value.</p>
 | 
			
		||||
<p>A label is an <a href="#root/_help_zEY4DaJG4YT5">attribute</a> of a note
 | 
			
		||||
  which has a name and optionally a value.</p>
 | 
			
		||||
<h2>Common use cases</h2>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li><strong>Metadata for personal use</strong>: Assign labels with optional
 | 
			
		||||
@@ -81,7 +81,8 @@
 | 
			
		||||
      <td><code>calendarRoot</code>
 | 
			
		||||
      </td>
 | 
			
		||||
      <td>Marks the note which should be used as root for <a class="reference-link"
 | 
			
		||||
        href="#root/_help_l0tKav7yLHGF">Day Notes</a>. Only one should be marked as such.</td>
 | 
			
		||||
        href="#root/_help_l0tKav7yLHGF">Day Notes</a>. Only one should be marked
 | 
			
		||||
        as such.</td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td><code>archived</code>
 | 
			
		||||
@@ -153,8 +154,8 @@
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td><code>autoReadOnlyDisabled</code>
 | 
			
		||||
      </td>
 | 
			
		||||
      <td>Disables automatic <a href="#root/_help_CoFPLs3dRlXc">read-only mode</a> for the
 | 
			
		||||
        given note.</td>
 | 
			
		||||
      <td>Disables automatic <a href="#root/_help_CoFPLs3dRlXc">read-only mode</a> for
 | 
			
		||||
        the given note.</td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td><code>appCss</code>
 | 
			
		||||
@@ -176,7 +177,8 @@
 | 
			
		||||
      <td>Set to <code>next</code>, <code>next-light</code>, or <code>next-dark</code> to
 | 
			
		||||
        use the corresponding TriliumNext theme (auto, light or dark) as the base
 | 
			
		||||
        for a custom theme, instead of the legacy one. See <a class="reference-link"
 | 
			
		||||
        href="#root/_help_WFGzWeUK6arS">Customize the Next theme</a> for more information.</td>
 | 
			
		||||
        href="#root/_help_WFGzWeUK6arS">Customize the Next theme</a> for more
 | 
			
		||||
        information.</td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td><code>cssClass</code>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<p>A relation is similar to a <a href="#root/_help_HI6GBBIduIgv">label</a>, but
 | 
			
		||||
  instead of having a text value it refers to another note.</p>
 | 
			
		||||
<p>A relation is similar to a <a href="#root/_help_HI6GBBIduIgv">label</a>,
 | 
			
		||||
  but instead of having a text value it refers to another note.</p>
 | 
			
		||||
<h2>Common use cases</h2>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li><strong>Metadata Relationships for personal use</strong>: For example,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<figure class="image">
 | 
			
		||||
  <img style="aspect-ratio:1425/654;" src="Bulk Actions_image.png"
 | 
			
		||||
  width="1425" height="654">
 | 
			
		||||
  <img style="aspect-ratio:1425/654;" src="Bulk Actions_image.png" width="1425"
 | 
			
		||||
  height="654">
 | 
			
		||||
</figure>
 | 
			
		||||
<p>The <em>Bulk Actions</em> dialog makes it easy to apply changes to multiple
 | 
			
		||||
  notes at once, ranging from simple actions such as adding or removing a
 | 
			
		||||
@@ -8,8 +8,8 @@
 | 
			
		||||
<h2>Interaction</h2>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>The first step is to select the notes in the <a class="reference-link"
 | 
			
		||||
    href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>. It's possible to apply bulk actions
 | 
			
		||||
    to:
 | 
			
		||||
    href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>. It's possible to apply bulk
 | 
			
		||||
    actions to:
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>A single note (and potentially its child notes) simply by clicking on
 | 
			
		||||
        it (with a left click or a right click).</li>
 | 
			
		||||
@@ -53,17 +53,17 @@
 | 
			
		||||
  </li>
 | 
			
		||||
  <li><strong>Update label value</strong>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>For each note, if it has a <a href="#root/_help_HI6GBBIduIgv">label</a> of the
 | 
			
		||||
        given name, it will change its value to the specified one. Leave <em>New value</em> field
 | 
			
		||||
      <li>For each note, if it has a <a href="#root/_help_HI6GBBIduIgv">label</a> of
 | 
			
		||||
        the given name, it will change its value to the specified one. Leave <em>New value</em> field
 | 
			
		||||
        empty to create a label without a value.</li>
 | 
			
		||||
      <li>Notes without the label will not be affected.</li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </li>
 | 
			
		||||
  <li><em><strong>Rename label</strong></em>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>For each note, if it has a <a href="#root/_help_HI6GBBIduIgv">label</a> of the
 | 
			
		||||
        given name, it will be renamed/replaced with a label of the new name. The
 | 
			
		||||
        value of the label (if present) will be kept intact.</li>
 | 
			
		||||
      <li>For each note, if it has a <a href="#root/_help_HI6GBBIduIgv">label</a> of
 | 
			
		||||
        the given name, it will be renamed/replaced with a label of the new name.
 | 
			
		||||
        The value of the label (if present) will be kept intact.</li>
 | 
			
		||||
      <li>Notes without the label will not be affected.</li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </li>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<p>A Trilium instance represents a server. If <a class="reference-link"
 | 
			
		||||
  href="#root/_help_cbkrhQjrkKrh">Synchronization</a> is set up, since multiple
 | 
			
		||||
  servers are involved (the one from the desktop client and the one the synchronisation
 | 
			
		||||
  is set up with), sometimes it can be useful to distinguish the instance
 | 
			
		||||
  you are running on.</p>
 | 
			
		||||
  href="#root/_help_cbkrhQjrkKrh">Synchronization</a> is set up, since
 | 
			
		||||
  multiple servers are involved (the one from the desktop client and the
 | 
			
		||||
  one the synchronisation is set up with), sometimes it can be useful to
 | 
			
		||||
  distinguish the instance you are running on.</p>
 | 
			
		||||
<h2>Setting the instance name</h2>
 | 
			
		||||
<p>To set up a name for the instance, modify the <code>config.ini</code>:</p><pre><code class="language-text-x-trilium-auto">[General]
 | 
			
		||||
instanceName=Hello</code></pre>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,5 +23,5 @@
 | 
			
		||||
<p>If you do not need to preserve any configurations that might be stored
 | 
			
		||||
  in the <code>config.ini</code> file, you can just delete all of the <a href="#root/_help_tAassRL4RSQL">data directory's</a> contents
 | 
			
		||||
  to fully restore the application to its original state. You can also review
 | 
			
		||||
  the <a href="#root/_help_Gzjqa934BdH4">configuration</a> file to provide all <code>config.ini</code> values
 | 
			
		||||
  as environment variables instead.</p>
 | 
			
		||||
  the <a href="#root/_help_Gzjqa934BdH4">configuration</a> file to provide
 | 
			
		||||
  all <code>config.ini</code> values as environment variables instead.</p>
 | 
			
		||||
@@ -1,13 +1,11 @@
 | 
			
		||||
<aside class="admonition tip">
 | 
			
		||||
  <p>For a quick start, consult the <a class="reference-link" href="#root/pgxEVkzLl1OP/_help_9qPsTWBorUhQ">API Reference</a>.</p>
 | 
			
		||||
</aside>
 | 
			
		||||
<p>ETAPI is Trilium's public/external REST API. It is available since Trilium
 | 
			
		||||
  v0.50.</p>
 | 
			
		||||
<p>The documentation is in OpenAPI format, available <a href="https://github.com/TriliumNext/Trilium/blob/master/src/etapi/etapi.openapi.yaml">here</a>.</p>
 | 
			
		||||
<h2>API clients</h2>
 | 
			
		||||
<p>As an alternative to calling the API directly, there are client libraries
 | 
			
		||||
  to simplify this</p>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li data-list-item-id="e3342ddfa108f6c8c6c47d7d3da8b02fa"><a href="https://github.com/Nriver/trilium-py">trilium-py</a>, you can
 | 
			
		||||
  <li><a href="https://github.com/Nriver/trilium-py">trilium-py</a>, you can
 | 
			
		||||
    use Python to communicate with Trilium.</li>
 | 
			
		||||
</ul>
 | 
			
		||||
<h2>Obtaining a token</h2>
 | 
			
		||||
@@ -25,10 +23,10 @@ Authorization: ETAPITOKEN</code></pre>
 | 
			
		||||
<p>Since v0.56 you can also use basic auth format:</p><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
 | 
			
		||||
Authorization: Basic BATOKEN</code></pre>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li data-list-item-id="ec59ac570a3d2a846da38378a5f2428ed">Where <code>BATOKEN = BASE64(username + ':' + password)</code> - this is
 | 
			
		||||
  <li>Where <code>BATOKEN = BASE64(username + ':' + password)</code> - this is
 | 
			
		||||
    a standard Basic Auth serialization</li>
 | 
			
		||||
  <li data-list-item-id="e18e2e73ebecc949dd4a51cd9f8bb0b91">Where <code>username</code> is "etapi"</li>
 | 
			
		||||
  <li data-list-item-id="ee892223f95cef4a53caec5477ab31edb">And <code>password</code> is the generated ETAPI token described above.</li>
 | 
			
		||||
  <li>Where <code>username</code> is "etapi"</li>
 | 
			
		||||
  <li>And <code>password</code> is the generated ETAPI token described above.</li>
 | 
			
		||||
</ul>
 | 
			
		||||
<p>Basic Auth is meant to be used with tools which support only basic auth.</p>
 | 
			
		||||
<h2>Interaction using Bash scripts</h2>
 | 
			
		||||
@@ -44,10 +42,10 @@ NOTE_ID="i6ra4ZshJhgN"
 | 
			
		||||
curl "$SERVER/etapi/notes/$NOTE_ID/content" -H "Authorization: $TOKEN" </code></pre>
 | 
			
		||||
<p>Make sure to replace the values of:</p>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li data-list-item-id="e68020f83acc951e180bb405d149a64a5"><code>TOKEN</code> with your ETAPI token.</li>
 | 
			
		||||
  <li data-list-item-id="ef4c31df5f6d18811e7de0ee8ff95f3a7"><code>SERVER</code> with the correct protocol, host name and port to your
 | 
			
		||||
  <li><code>TOKEN</code> with your ETAPI token.</li>
 | 
			
		||||
  <li><code>SERVER</code> with the correct protocol, host name and port to your
 | 
			
		||||
    Trilium instance.</li>
 | 
			
		||||
  <li data-list-item-id="e25086bb4c54d32259f987f9366e22204"><code>NOTE_ID</code> with an existing note ID to download.</li>
 | 
			
		||||
  <li><code>NOTE_ID</code> with an existing note ID to download.</li>
 | 
			
		||||
</ul>
 | 
			
		||||
<p>As another example, to obtain a .zip export of a note and place it in
 | 
			
		||||
  a directory called <code>out</code>, simply replace the last statement in
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<figure class="image image-style-align-right">
 | 
			
		||||
  <img style="aspect-ratio:263/445;" src="Hidden Notes_image.png"
 | 
			
		||||
  width="263" height="445">
 | 
			
		||||
  <img style="aspect-ratio:263/445;" src="Hidden Notes_image.png" width="263"
 | 
			
		||||
  height="445">
 | 
			
		||||
</figure>
 | 
			
		||||
<p>For easy extensibility, a lot of features in Trilium make use of actual
 | 
			
		||||
  notes to store information as opposed to having them stored in a separate
 | 
			
		||||
@@ -8,13 +8,14 @@
 | 
			
		||||
  href="#root/_help_zEY4DaJG4YT5">Attributes</a>, <a class="reference-link"
 | 
			
		||||
  href="#root/_help_Cq5X6iKQop6R">Relations</a> or even <a class="reference-link"
 | 
			
		||||
  href="#root/_help_eIg8jdvaoNNd">Search</a> and <a class="reference-link"
 | 
			
		||||
  href="#root/_help_QEAPj01N5f7w">Links</a> to be able to operate on them.</p>
 | 
			
		||||
  href="#root/_help_QEAPj01N5f7w">Links</a> to be able to operate on
 | 
			
		||||
  them.</p>
 | 
			
		||||
<p>As the name suggests, these notes are hidden to the user by default to
 | 
			
		||||
  prevent cluttering the note tree and to prevent them from being accidentally
 | 
			
		||||
  deleted.</p>
 | 
			
		||||
<p>The hidden notes are stored in the user's <a class="reference-link"
 | 
			
		||||
  href="#root/_help_wX4HbRucYSDD">Database</a> just like normal notes, but
 | 
			
		||||
  they have a unique <a class="reference-link" href="#root/_help_m1lbrzyKDaRB">Note ID</a> which
 | 
			
		||||
  href="#root/_help_wX4HbRucYSDD">Database</a> just like normal notes,
 | 
			
		||||
  but they have a unique <a class="reference-link" href="#root/_help_m1lbrzyKDaRB">Note ID</a> which
 | 
			
		||||
  allows them to be distinguished from the normal ones.</p>
 | 
			
		||||
<h2>Accessing the hidden note tree</h2>
 | 
			
		||||
<p>From the <a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a>,
 | 
			
		||||
@@ -50,8 +51,8 @@ class="ck-table-resized">
 | 
			
		||||
      </td>
 | 
			
		||||
      <td>
 | 
			
		||||
        <p>When SQL queries or commands are executed in the <a class="reference-link"
 | 
			
		||||
          href="#root/_help_YKWqdJhzi2VY">SQL Console</a>, they are stored here, grouped
 | 
			
		||||
          by month. Only the query is stored and not the results.</p>
 | 
			
		||||
          href="#root/_help_YKWqdJhzi2VY">SQL Console</a>, they are stored here,
 | 
			
		||||
          grouped by month. Only the query is stored and not the results.</p>
 | 
			
		||||
        <p>This section can be accessed without going to the hidden tree by simply
 | 
			
		||||
          going to the <a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a> and
 | 
			
		||||
          selecting Advanced → Open SQL Console History.</p>
 | 
			
		||||
@@ -64,8 +65,9 @@ class="ck-table-resized">
 | 
			
		||||
      </td>
 | 
			
		||||
      <td>
 | 
			
		||||
        <p>Whenever a search is executed from the full <a class="reference-link"
 | 
			
		||||
          href="#root/_help_eIg8jdvaoNNd">Search</a>, the query will be stored here, grouped
 | 
			
		||||
          by month. Only the search parameters are stored and not the results themselves.</p>
 | 
			
		||||
          href="#root/_help_eIg8jdvaoNNd">Search</a>, the query will be stored here,
 | 
			
		||||
          grouped by month. Only the search parameters are stored and not the results
 | 
			
		||||
          themselves.</p>
 | 
			
		||||
        <p>This section can be accessed without going to the hidden tree by simply
 | 
			
		||||
          going to the <a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a> and
 | 
			
		||||
          selecting Advanced → Open Search History.</p>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics.html
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						@@ -82,18 +82,18 @@ trilium_notes_total 1234 1701432000
 | 
			
		||||
</ul>
 | 
			
		||||
<h2><strong>Grafana Dashboard</strong></h2>
 | 
			
		||||
<figure class="image">
 | 
			
		||||
  <img style="aspect-ratio:2594/1568;" src="1_Metrics_image.png"
 | 
			
		||||
  width="2594" height="1568">
 | 
			
		||||
  <img style="aspect-ratio:2594/1568;" src="1_Metrics_image.png" width="2594"
 | 
			
		||||
  height="1568">
 | 
			
		||||
</figure>
 | 
			
		||||
<p>You can also use the Grafana Dashboard that has been created for TriliumNext
 | 
			
		||||
  - just take the JSON from <a class="reference-link" href="#root/_help_bOP3TB56fL1V">grafana-dashboard.json</a> and
 | 
			
		||||
  then import the dashboard, following these screenshots:</p>
 | 
			
		||||
<figure class="image">
 | 
			
		||||
  <img style="aspect-ratio:1881/282;" src="2_Metrics_image.png"
 | 
			
		||||
  width="1881" height="282">
 | 
			
		||||
  <img style="aspect-ratio:1881/282;" src="2_Metrics_image.png" width="1881"
 | 
			
		||||
  height="282">
 | 
			
		||||
</figure>
 | 
			
		||||
<p>Then paste the JSON, and hit load:</p>
 | 
			
		||||
<figure class="image">
 | 
			
		||||
  <img style="aspect-ratio:1055/830;" src="Metrics_image.png"
 | 
			
		||||
  width="1055" height="830">
 | 
			
		||||
  <img style="aspect-ratio:1055/830;" src="Metrics_image.png" width="1055"
 | 
			
		||||
  height="830">
 | 
			
		||||
</figure>
 | 
			
		||||
@@ -6,16 +6,7 @@
 | 
			
		||||
  <li>Note Map, which shows the hierarchical tree structure.</li>
 | 
			
		||||
</ul>
 | 
			
		||||
<h2>Link Map</h2>
 | 
			
		||||
<p>The Link map is a visualization of links and <a class="reference-link"
 | 
			
		||||
  href="#root/_help_Cq5X6iKQop6R">Relations</a> incoming to and outgoing from
 | 
			
		||||
  a particular note.</p>
 | 
			
		||||
<p>The map indicates the following types of relations:</p>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li><a class="reference-link" href="#root/_help_hrZ1D00cLbal">Internal (reference) links</a> between
 | 
			
		||||
    notes.</li>
 | 
			
		||||
  <li><a class="reference-link" href="#root/_help_Cq5X6iKQop6R">Relations</a>
 | 
			
		||||
  </li>
 | 
			
		||||
</ul>
 | 
			
		||||
<p>Shows <a href="#root/_help_zEY4DaJG4YT5">relations</a> between notes:</p>
 | 
			
		||||
<p>
 | 
			
		||||
  <img src="1_Note Map (Link map, Tree m.png">
 | 
			
		||||
</p>
 | 
			
		||||
@@ -30,8 +21,8 @@
 | 
			
		||||
  in full screen. See <a href="#root/_help_bdUJEHsAPYQR">Note Map</a> for
 | 
			
		||||
  more information.</p>
 | 
			
		||||
<h2>See also</h2>
 | 
			
		||||
<p><a href="#root/_help_iRwzGnHPzonm">Relation map</a> is a similar concept, with
 | 
			
		||||
  some differences:</p>
 | 
			
		||||
<p><a href="#root/_help_iRwzGnHPzonm">Relation map</a> is a similar concept,
 | 
			
		||||
  with some differences:</p>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>note map is automatically generated while relation map must be created
 | 
			
		||||
    manually</li>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,9 @@
 | 
			
		||||
<ul>
 | 
			
		||||
  <li><a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a> notes are
 | 
			
		||||
    represented internally as HTML, using the <a class="reference-link"
 | 
			
		||||
    href="#root/_help_MI26XDLSAlCD">CKEditor</a> representation. Note that due
 | 
			
		||||
    to the custom plugins, some HTML elements are specific to Trilium only,
 | 
			
		||||
    for example the admonitions.</li>
 | 
			
		||||
    href="#root/_help_MI26XDLSAlCD">CKEditor</a> representation. Note
 | 
			
		||||
    that due to the custom plugins, some HTML elements are specific to Trilium
 | 
			
		||||
    only, for example the admonitions.</li>
 | 
			
		||||
  <li><a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a> notes are
 | 
			
		||||
    plain text and are represented internally as-is.</li>
 | 
			
		||||
  <li><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a> notes
 | 
			
		||||
@@ -22,10 +22,10 @@
 | 
			
		||||
</ul>
 | 
			
		||||
<p>Note that some information is also stored as <a class="reference-link"
 | 
			
		||||
  href="#root/_help_0vhv7lsOLy82">Attachments</a>. For example <a class="reference-link"
 | 
			
		||||
  href="#root/_help_grjYqerjn243">Canvas</a> notes use the attachments feature
 | 
			
		||||
  to store the custom libraries, and alongside with <a class="reference-link"
 | 
			
		||||
  href="#root/_help_gBbsAeiuUxI5">Mind Map</a> and other similar note types
 | 
			
		||||
  it stores an SVG representation of the content for use in other features
 | 
			
		||||
  href="#root/_help_grjYqerjn243">Canvas</a> notes use the attachments
 | 
			
		||||
  feature to store the custom libraries, and alongside with <a class="reference-link"
 | 
			
		||||
  href="#root/_help_gBbsAeiuUxI5">Mind Map</a> and other similar note
 | 
			
		||||
  types it stores an SVG representation of the content for use in other features
 | 
			
		||||
  such as including in other notes, shared notes, etc.</p>
 | 
			
		||||
<p>Here's part of the HTML representation of this note, as it's stored in
 | 
			
		||||
  the database (but prettified).</p><pre><code class="language-text-x-trilium-auto"><h2>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										144
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						@@ -3,8 +3,8 @@
 | 
			
		||||
  from your Trilium notes, making it accessible to others online.</p>
 | 
			
		||||
<figure
 | 
			
		||||
class="image">
 | 
			
		||||
  <img style="aspect-ratio:1144/660;" src="Sharing_image.png"
 | 
			
		||||
  width="1144" height="660">
 | 
			
		||||
  <img style="aspect-ratio:1144/660;" src="Sharing_image.png" width="1144"
 | 
			
		||||
  height="660">
 | 
			
		||||
  </figure>
 | 
			
		||||
  
 | 
			
		||||
<h2>Features, interaction and limitations</h2>
 | 
			
		||||
@@ -149,7 +149,7 @@ class="image">
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a>
 | 
			
		||||
        <th><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map View</a>
 | 
			
		||||
        </th>
 | 
			
		||||
        <td>Not supported.</td>
 | 
			
		||||
        <td> </td>
 | 
			
		||||
@@ -177,8 +177,8 @@ class="image">
 | 
			
		||||
  <p>Some of these limitations may be addressed in future updates.</p>
 | 
			
		||||
  <h2>Prerequisites</h2>
 | 
			
		||||
  <p>To use the sharing feature, you must have a <a class="reference-link"
 | 
			
		||||
    href="#root/_help_WOcw2SLH6tbX">Server Installation</a> of Trilium. This
 | 
			
		||||
    is necessary because the notes will be hosted from the server.</p>
 | 
			
		||||
    href="#root/_help_WOcw2SLH6tbX">Server Installation</a> of Trilium.
 | 
			
		||||
    This is necessary because the notes will be hosted from the server.</p>
 | 
			
		||||
  <h2>Sharing a note</h2>
 | 
			
		||||
  <ol>
 | 
			
		||||
    <li>
 | 
			
		||||
@@ -186,8 +186,7 @@ class="image">
 | 
			
		||||
        within the note's interface. Once sharing is enabled, an URL will appear,
 | 
			
		||||
        which you can click to access the shared note.</p>
 | 
			
		||||
      <p>
 | 
			
		||||
        <img src="Sharing_share-single-note.png"
 | 
			
		||||
        alt="Share Note">
 | 
			
		||||
        <img src="Sharing_share-single-note.png" alt="Share Note">
 | 
			
		||||
      </p>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li>
 | 
			
		||||
@@ -208,19 +207,9 @@ class="image">
 | 
			
		||||
    This allows you to manage and navigate through all the notes you have made
 | 
			
		||||
    public.</p>
 | 
			
		||||
  <h2>Security considerations</h2>
 | 
			
		||||
  <ul>
 | 
			
		||||
    <li>Shared notes are published on the open internet and can be accessed by
 | 
			
		||||
      anyone with the URL unless the notes are password-protected.</li>
 | 
			
		||||
    <li>The URL's randomness does not provide security, so it is crucial not to
 | 
			
		||||
      share sensitive information through this feature.</li>
 | 
			
		||||
    <li>Trilium takes precautions to protect your publicly shared instance from
 | 
			
		||||
      leaking information for non-shared notes, including opening a separate
 | 
			
		||||
      read-only connection to the <a class="reference-link" href="#root/_help_wX4HbRucYSDD">Database</a>.
 | 
			
		||||
      Depending on your threat model, it might make more sense to use 
 | 
			
		||||
      <a
 | 
			
		||||
      class="reference-link" href="#root/_help_ycBFjKrrwE9p">Exporting HTML for web publishing</a> and use battle-tested web servers
 | 
			
		||||
        such as Nginx or Apache to serve static content.</li>
 | 
			
		||||
  </ul>
 | 
			
		||||
  <p>Shared notes are published on the open internet and can be accessed by
 | 
			
		||||
    anyone with the URL. The URL's randomness does not provide security, so
 | 
			
		||||
    it is crucial not to share sensitive information through this feature.</p>
 | 
			
		||||
  <h3>Password protection</h3>
 | 
			
		||||
  <p>To protect shared notes with a username and password, you can use the <code>#shareCredentials</code> attribute.
 | 
			
		||||
    Add this label to the note with the format <code>#shareCredentials="username:password"</code>.
 | 
			
		||||
@@ -231,10 +220,10 @@ class="image">
 | 
			
		||||
    it using your own CSS:</p>
 | 
			
		||||
  <ul>
 | 
			
		||||
    <li><strong>Custom CSS</strong>: Link a CSS <a class="reference-link"
 | 
			
		||||
      href="#root/_help_6f9hih2hXXZk">Code</a> note to the shared page by adding
 | 
			
		||||
      a <code>~shareCss</code> relation to the note. If you want this style to
 | 
			
		||||
      apply to the entire subtree, make the label inheritable. You can hide the
 | 
			
		||||
      CSS code note from the tree navigation by adding the <code>#shareHiddenFromTree</code> label.</li>
 | 
			
		||||
      href="#root/_help_6f9hih2hXXZk">Code</a> note to the shared page by
 | 
			
		||||
      adding a <code>~shareCss</code> relation to the note. If you want this style
 | 
			
		||||
      to apply to the entire subtree, make the label inheritable. You can hide
 | 
			
		||||
      the CSS code note from the tree navigation by adding the <code>#shareHiddenFromTree</code> label.</li>
 | 
			
		||||
    <li><strong>Omitting Default CSS</strong>: For extensive styling changes,
 | 
			
		||||
      use the <code>#shareOmitDefaultCss</code> label to avoid conflicts with Trilium's
 | 
			
		||||
      <a
 | 
			
		||||
@@ -290,15 +279,6 @@ for (const attr of parentNote.attributes) {
 | 
			
		||||
    <li>Using slashes (<code>/</code>) within aliases to create subpaths is not
 | 
			
		||||
      supported.</li>
 | 
			
		||||
  </ol>
 | 
			
		||||
  <aside class="admonition tip">
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>To easily identify pages that don't have a share alias, run a 
 | 
			
		||||
        <a
 | 
			
		||||
        class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a> with <code>#!shareAlias</code>.</li>
 | 
			
		||||
      <li>To be able to enter the share alias faster, consider using <a class="reference-link"
 | 
			
		||||
        href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> (for example <code>#label:shareAlias(inheritable)="promoted,alias=Slug,single,text"</code>).</li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </aside>
 | 
			
		||||
  <h3>Setting a custom favicon</h3>
 | 
			
		||||
  <p>To customize the favicon for your shared pages, create a relation <code>~shareFavicon</code> pointing
 | 
			
		||||
    to a file note containing the favicon (e.g., in <code>.ico</code> format).</p>
 | 
			
		||||
@@ -319,11 +299,7 @@ for (const attr of parentNote.attributes) {
 | 
			
		||||
    When viewed, the list of shared roots will be displayed at the bottom of
 | 
			
		||||
    the note.</p>
 | 
			
		||||
  <h2>Attribute reference</h2>
 | 
			
		||||
  <table class="ck-table-resized">
 | 
			
		||||
    <colgroup>
 | 
			
		||||
      <col style="width:18.38%;">
 | 
			
		||||
        <col style="width:81.62%;">
 | 
			
		||||
    </colgroup>
 | 
			
		||||
  <table>
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Attribute</th>
 | 
			
		||||
@@ -332,40 +308,40 @@ for (const attr of parentNote.attributes) {
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareHiddenFromTree</code>
 | 
			
		||||
        <td><code>shareHiddenFromTree</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>this note is hidden from left navigation tree, but still accessible with
 | 
			
		||||
          its URL</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareExternalLink</code>
 | 
			
		||||
        <td><code>shareExternalLink</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>note will act as a link to an external website in the share tree</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareAlias</code>
 | 
			
		||||
        <td><code>shareAlias</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareOmitDefaultCss</code>
 | 
			
		||||
        <td><code>shareOmitDefaultCss</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>default share page CSS will be omitted. Use when you make extensive styling
 | 
			
		||||
          changes.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareRoot</code>
 | 
			
		||||
        <td><code>shareRoot</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>marks note which is served on /share root.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareDescription</code>
 | 
			
		||||
        <td><code>shareDescription</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>define text to be added to the HTML meta tag for description</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareRaw</code>
 | 
			
		||||
        <td><code>shareRaw</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>Note will be served in its raw format, without HTML wrapper. See also 
 | 
			
		||||
          <a
 | 
			
		||||
@@ -373,7 +349,7 @@ for (const attr of parentNote.attributes) {
 | 
			
		||||
            without setting an attribute.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareDisallowRobotIndexing</code>
 | 
			
		||||
        <td><code>shareDisallowRobotIndexing</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>
 | 
			
		||||
          <p>Indicates to web crawlers that the page should not be indexed of this
 | 
			
		||||
@@ -385,19 +361,19 @@ for (const attr of parentNote.attributes) {
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareCredentials</code>
 | 
			
		||||
        <td><code>shareCredentials</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>require credentials to access this shared note. Value is expected to be
 | 
			
		||||
          in format <code>username:password</code>. Don't forget to make this inheritable
 | 
			
		||||
          to apply to child-notes/images.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareIndex</code>
 | 
			
		||||
        <td><code>shareIndex</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>Note with this label will list all roots of shared notes.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareHtmlLocation</code>
 | 
			
		||||
        <td><code>shareHtmlLocation</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>defines where custom HTML injected via <code>~shareHtml</code> relation
 | 
			
		||||
          should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where
 | 
			
		||||
@@ -407,76 +383,6 @@ for (const attr of parentNote.attributes) {
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
  
 | 
			
		||||
<h3>Customizing logo</h3>
 | 
			
		||||
  <p>It's possible to adjust the logo which is displayed on the top-left of
 | 
			
		||||
    the left pane.</p>
 | 
			
		||||
  <table>
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Attribute</th>
 | 
			
		||||
        <th>Description</th>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>~shareLogo</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>Relation set to an image to use as logo. The image must be part of the
 | 
			
		||||
          share tree (it can be hidden if needed).</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareLogoWidth</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>The width (in pixels, without unit) to set for the logo. Default is <code>53</code>.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareLogoHeight</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>The height (in pixels, without unit) to set for the logo. Default is <code>40</code>.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareRootLink</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>URL to navigate to when the logo is pressed.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
  
 | 
			
		||||
<h3>Customizing OpenGraph</h3>
 | 
			
		||||
  <table>
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Attribute</th>
 | 
			
		||||
        <th>Description</th>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareOpenGraphColor</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>This adjusts the <code>theme-color</code> meta-property.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareOpenGraphURL</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>This adjusts the <code>og:url</code> and <code>twitter:url</code> meta-properties.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareOpenGraphDomain</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>Adjusts the <code>twitter:domain</code> meta-property.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td><code>#shareOpenGraphImage</code> 
 | 
			
		||||
          <br><code>~shareOpenGraphImage</code>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>Can be either a label, case in which the value is passed on as-is, or
 | 
			
		||||
          it can be a relation to an image <a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a>.
 | 
			
		||||
          This controls the <code>og:image</code> meta-property.</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
  
 | 
			
		||||
<h2>Credits</h2>
 | 
			
		||||
  <p>Since v0.95.0, a new theme was introduced (and enabled by default) which
 | 
			
		||||
    greatly improves the visual aspect of the Share feature, as well as its
 | 
			
		||||
 
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
<p>As described in <a class="reference-link" href="#root/_help_R9pX4DGra2Vt">Sharing</a>,
 | 
			
		||||
  Trilium can act as a public server in which the shared notes are displayed
 | 
			
		||||
  in read-only mode. While this can work in most cases, it's generally not
 | 
			
		||||
  meant for high-traffic websites and since it's running on a Node.js server
 | 
			
		||||
  it can be potentially exploited.</p>
 | 
			
		||||
<p>Another alternative is to generate static HTML files (just like other
 | 
			
		||||
  static site generators such as <a href="https://www.mkdocs.org/">MkDocs</a>).
 | 
			
		||||
  Since the normal HTML ZIP export does not contain any styling or additional
 | 
			
		||||
  functionality, Trilium provides a way to export the same layout and style
 | 
			
		||||
  as the <a class="reference-link" href="#root/_help_R9pX4DGra2Vt">Sharing</a> function
 | 
			
		||||
  into static HTML files.</p>
 | 
			
		||||
<p>Apart from the enhanced security, these HTML files are also easy to deploy
 | 
			
		||||
  on “serverless” deployments such as GitHub Pages or CloudFlare Pages and
 | 
			
		||||
  cache very easily.</p>
 | 
			
		||||
<aside class="admonition tip">
 | 
			
		||||
  <p>Trilium's documentation, available at <a href="https://docs.triliumnotes.org/">docs.triliumnotes.org</a> is
 | 
			
		||||
    built using this function of exporting to static HTML files which are then
 | 
			
		||||
    deployed automatically to CloudFlare Pages.</p>
 | 
			
		||||
  <p>The process is <a href="https://github.com/TriliumNext/Trilium/blob/main/apps/edit-docs/src/build-docs.ts">automated</a> by
 | 
			
		||||
    importing the Markdown documentation and exporting it via a script to the
 | 
			
		||||
    static web format.</p>
 | 
			
		||||
</aside>
 | 
			
		||||
<h2>Differences from normal sharing</h2>
 | 
			
		||||
<p>Apart from normal <a class="reference-link" href="#root/_help_R9pX4DGra2Vt">Sharing</a>,
 | 
			
		||||
  exporting to static HTML files comes with a few subtle differences:</p>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>The URL structure is different. Where in normal sharing it's something
 | 
			
		||||
    along the way of <code>example.com/share/noteid</code>, the notes follow
 | 
			
		||||
    an hierarchical structure, such as <code>docs.triliumnotes.org/user-guide/concepts/navigation/tree-concepts</code>.</li>
 | 
			
		||||
  <li>The <code>favicon.ico</code> is not handled automatically, it needs to be
 | 
			
		||||
    manually added on the server after the export is generated.</li>
 | 
			
		||||
  <li>The “Last updated” for notes is not available.</li>
 | 
			
		||||
  <li>The search functionality works slightly different since the normal one
 | 
			
		||||
    requires an active API to work. In the static export, search still works
 | 
			
		||||
    but uses a different mechanism so results might be different.</li>
 | 
			
		||||
</ul>
 | 
			
		||||
<h2>Differences from normal .zip export</h2>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>The name of the files/URLs will prefer <code>shareAlias</code> to allow
 | 
			
		||||
    for clean URLs.</li>
 | 
			
		||||
  <li>The export requires a functional web server as the pages will not render
 | 
			
		||||
    properly if accessed locally via a web browser due to the use of module
 | 
			
		||||
    scripts.</li>
 | 
			
		||||
</ul>
 | 
			
		||||
<h2>Testing locally</h2>
 | 
			
		||||
<p>As mentioned previously, the exported static pages require a website to
 | 
			
		||||
  function. In order to test locally, a web server needs to be used.</p>
 | 
			
		||||
<p>One example is to use the Node.js-based <a href="https://www.npmjs.com/package/http-server"><code>http-server</code></a> which
 | 
			
		||||
  can be installed via:</p><pre><code class="language-text-x-trilium-auto">npm i -g http-server</code></pre>
 | 
			
		||||
<p>Once installed simply:</p>
 | 
			
		||||
<ol>
 | 
			
		||||
  <li>Extract the exported .zip file.</li>
 | 
			
		||||
  <li>Inside the extracted directory, run <code>http-server</code>.</li>
 | 
			
		||||
  <li>Access the indicated address (e.g. <a href="http://localhost:8080">http://localhost:8080</a>).</li>
 | 
			
		||||
</ol>
 | 
			
		||||
<h2>Automation</h2>
 | 
			
		||||
<p><a class="reference-link" href="#root/_help_pgxEVkzLl1OP">ETAPI (REST API)</a> could
 | 
			
		||||
  potentially be used to automate an export on a scheduled task.</p>
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
<p>It might be desirable to only expose the share functionality of Trilium
 | 
			
		||||
  to the Internet, and keep the application accessible only within a local
 | 
			
		||||
  network or via VPN.</p>
 | 
			
		||||
<p>To do so, a reverse proxy is required.</p>
 | 
			
		||||
<h2>Caddy</h2><pre><code class="language-text-x-trilium-auto">http://domain.com {
 | 
			
		||||
  reverse_proxy /share http://localhost:8080/share
 | 
			
		||||
}</code></pre>
 | 
			
		||||
<p>This is for newer versions where the share functionality is isolated,
 | 
			
		||||
  for older versions it's required to also include <code>/assets</code>.<sup><a href="#fn2b8mg20aol8">[1]</a></sup>
 | 
			
		||||
</p>
 | 
			
		||||
<ol>
 | 
			
		||||
  <li>
 | 
			
		||||
    <p><sup><strong><a href="#fnref2b8mg20aol8">^</a></strong></sup>
 | 
			
		||||
    </p>
 | 
			
		||||
    <p><a href="https://github.com/orgs/TriliumNext/discussions/7341#discussioncomment-14679897">https://github.com/orgs/TriliumNext/discussions/7341#discussioncomment-14679897</a>
 | 
			
		||||
    </p>
 | 
			
		||||
  </li>
 | 
			
		||||
</ol>
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
  </li>
 | 
			
		||||
  <li><a href="#root/_help_QEAPj01N5f7w">Reference links</a>
 | 
			
		||||
  </li>
 | 
			
		||||
  <li><a href="#root/_help_NwBbFdNZ9h7O">Admonitions</a>, we ended up creating our
 | 
			
		||||
    own plugin but <a href="https://github.com/aarkue/ckeditor5-admonition">aarkue/ckeditor5-admonition</a> was
 | 
			
		||||
  <li><a href="#root/_help_NwBbFdNZ9h7O">Admonitions</a>, we ended up creating
 | 
			
		||||
    our own plugin but <a href="https://github.com/aarkue/ckeditor5-admonition">aarkue/ckeditor5-admonition</a> was
 | 
			
		||||
    a good inspiration (including the toolbar icon).</li>
 | 
			
		||||
</ul>
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<p><a href="https://excalidraw.com/">Excalidraw</a> is the technology behind
 | 
			
		||||
  the <a href="#root/_help_grjYqerjn243">Canvas</a> notes. The source
 | 
			
		||||
  code of the library is available on <a href="https://github.com/excalidraw/excalidraw">GitHub</a>.</p>
 | 
			
		||||
  the <a href="#root/_help_grjYqerjn243">Canvas</a> notes. The
 | 
			
		||||
  source code of the library is available on <a href="https://github.com/excalidraw/excalidraw">GitHub</a>.</p>
 | 
			
		||||
<p>We are using an unmodified version of it, so it shares the same <a href="https://github.com/excalidraw/excalidraw/issues">issues</a> as
 | 
			
		||||
  the original.</p>
 | 
			
		||||