mirror of
https://github.com/zadam/trilium.git
synced 2025-10-30 09:56:36 +01:00
Compare commits
2 Commits
v0.98.1
...
feat/add-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5332f015ef | ||
|
|
a3d77421fd |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.98.1",
|
||||
"version": "0.98.0",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -36,7 +36,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.4.2",
|
||||
"i18next": "25.4.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -62,7 +62,7 @@
|
||||
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/jquery": "3.5.32",
|
||||
"@types/leaflet": "1.9.20",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
|
||||
223
apps/client/src/services/ckeditor_plugin_config.ts
Normal file
223
apps/client/src/services/ckeditor_plugin_config.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* @module CKEditor Plugin Configuration Service
|
||||
*
|
||||
* This service manages the dynamic configuration of CKEditor plugins based on user preferences.
|
||||
* It handles plugin enablement, dependency resolution, and toolbar configuration.
|
||||
*/
|
||||
|
||||
import server from "./server.js";
|
||||
import type {
|
||||
PluginConfiguration,
|
||||
PluginMetadata,
|
||||
PluginRegistry,
|
||||
PluginValidationResult
|
||||
} from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* Cache for plugin registry and user configuration
|
||||
*/
|
||||
let pluginRegistryCache: PluginRegistry | null = null;
|
||||
let userConfigCache: PluginConfiguration[] | null = null;
|
||||
let cacheTimestamp = 0;
|
||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Get the plugin registry from server
|
||||
*/
|
||||
export async function getPluginRegistry(): Promise<PluginRegistry> {
|
||||
const now = Date.now();
|
||||
if (pluginRegistryCache && (now - cacheTimestamp) < CACHE_DURATION) {
|
||||
return pluginRegistryCache;
|
||||
}
|
||||
|
||||
try {
|
||||
pluginRegistryCache = await server.get<PluginRegistry>('ckeditor-plugins/registry');
|
||||
cacheTimestamp = now;
|
||||
return pluginRegistryCache;
|
||||
} catch (error) {
|
||||
console.error('Failed to load CKEditor plugin registry:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's plugin configuration from server
|
||||
*/
|
||||
export async function getUserPluginConfig(): Promise<PluginConfiguration[]> {
|
||||
const now = Date.now();
|
||||
if (userConfigCache && (now - cacheTimestamp) < CACHE_DURATION) {
|
||||
return userConfigCache;
|
||||
}
|
||||
|
||||
try {
|
||||
userConfigCache = await server.get<PluginConfiguration[]>('ckeditor-plugins/config');
|
||||
cacheTimestamp = now;
|
||||
return userConfigCache;
|
||||
} catch (error) {
|
||||
console.error('Failed to load user plugin configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache (call when configuration is updated)
|
||||
*/
|
||||
export function clearCache(): void {
|
||||
pluginRegistryCache = null;
|
||||
userConfigCache = null;
|
||||
cacheTimestamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enabled plugins for the current user
|
||||
*/
|
||||
export async function getEnabledPlugins(): Promise<Set<string>> {
|
||||
const userConfig = await getUserPluginConfig();
|
||||
const enabledPlugins = new Set<string>();
|
||||
|
||||
// Add all enabled user plugins
|
||||
userConfig.forEach(config => {
|
||||
if (config.enabled) {
|
||||
enabledPlugins.add(config.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Always include core plugins
|
||||
const registry = await getPluginRegistry();
|
||||
Object.values(registry.plugins).forEach(plugin => {
|
||||
if (plugin.isCore) {
|
||||
enabledPlugins.add(plugin.id);
|
||||
}
|
||||
});
|
||||
|
||||
return enabledPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disabled plugin names for CKEditor config
|
||||
*/
|
||||
export async function getDisabledPlugins(): Promise<string[]> {
|
||||
try {
|
||||
const registry = await getPluginRegistry();
|
||||
const enabledPlugins = await getEnabledPlugins();
|
||||
const disabledPlugins: string[] = [];
|
||||
|
||||
// Find plugins that are disabled
|
||||
Object.values(registry.plugins).forEach(plugin => {
|
||||
if (!plugin.isCore && !enabledPlugins.has(plugin.id)) {
|
||||
// Map plugin ID to actual CKEditor plugin names if needed
|
||||
const pluginNames = getPluginNames(plugin.id);
|
||||
disabledPlugins.push(...pluginNames);
|
||||
}
|
||||
});
|
||||
|
||||
return disabledPlugins;
|
||||
} catch (error) {
|
||||
console.warn("Failed to get disabled plugins, returning empty list:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map plugin ID to actual CKEditor plugin names
|
||||
* Some plugins might have multiple names or different names than their ID
|
||||
*/
|
||||
function getPluginNames(pluginId: string): string[] {
|
||||
const nameMap: Record<string, string[]> = {
|
||||
"emoji": ["EmojiMention", "EmojiPicker"],
|
||||
"math": ["Math", "AutoformatMath"],
|
||||
"image": ["Image", "ImageCaption", "ImageInline", "ImageResize", "ImageStyle", "ImageToolbar", "ImageUpload"],
|
||||
"table": ["Table", "TableToolbar", "TableProperties", "TableCellProperties", "TableSelection", "TableCaption", "TableColumnResize"],
|
||||
"font": ["Font", "FontColor", "FontBackgroundColor"],
|
||||
"list": ["List", "ListProperties"],
|
||||
"specialcharacters": ["SpecialCharacters", "SpecialCharactersEssentials"],
|
||||
"findandreplace": ["FindAndReplace"],
|
||||
"horizontalline": ["HorizontalLine"],
|
||||
"pagebreak": ["PageBreak"],
|
||||
"removeformat": ["RemoveFormat"],
|
||||
"alignment": ["Alignment"],
|
||||
"indent": ["Indent", "IndentBlock"],
|
||||
"codeblock": ["CodeBlock"],
|
||||
"blockquote": ["BlockQuote"],
|
||||
"todolist": ["TodoList"],
|
||||
"heading": ["Heading", "HeadingButtonsUI"],
|
||||
"paragraph": ["ParagraphButtonUI"],
|
||||
// Add more mappings as needed
|
||||
};
|
||||
|
||||
return nameMap[pluginId] || [pluginId.charAt(0).toUpperCase() + pluginId.slice(1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the current plugin configuration
|
||||
*/
|
||||
export async function validatePluginConfiguration(): Promise<PluginValidationResult> {
|
||||
try {
|
||||
const userConfig = await getUserPluginConfig();
|
||||
return await server.post<PluginValidationResult>('ckeditor-plugins/validate', {
|
||||
plugins: userConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to validate plugin configuration:', error);
|
||||
return {
|
||||
valid: false,
|
||||
errors: [{
|
||||
type: "missing_dependency",
|
||||
pluginId: "unknown",
|
||||
message: `Validation failed: ${error}`
|
||||
}],
|
||||
warnings: [],
|
||||
resolvedPlugins: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get toolbar items that should be hidden based on disabled plugins
|
||||
*/
|
||||
export async function getHiddenToolbarItems(): Promise<string[]> {
|
||||
const registry = await getPluginRegistry();
|
||||
const enabledPlugins = await getEnabledPlugins();
|
||||
const hiddenItems: string[] = [];
|
||||
|
||||
Object.values(registry.plugins).forEach(plugin => {
|
||||
if (!enabledPlugins.has(plugin.id) && plugin.toolbarItems) {
|
||||
hiddenItems.push(...plugin.toolbarItems);
|
||||
}
|
||||
});
|
||||
|
||||
return hiddenItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user plugin configuration
|
||||
*/
|
||||
export async function updatePluginConfiguration(plugins: PluginConfiguration[]): Promise<void> {
|
||||
try {
|
||||
const response = await server.put('ckeditor-plugins/config', {
|
||||
plugins,
|
||||
validate: true
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.errors?.join(", ") || "Update failed");
|
||||
}
|
||||
|
||||
// Clear cache so next requests fetch fresh data
|
||||
clearCache();
|
||||
} catch (error) {
|
||||
console.error('Failed to update plugin configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getPluginRegistry,
|
||||
getUserPluginConfig,
|
||||
getEnabledPlugins,
|
||||
getDisabledPlugins,
|
||||
getHiddenToolbarItems,
|
||||
validatePluginConfiguration,
|
||||
updatePluginConfiguration,
|
||||
clearCache
|
||||
};
|
||||
@@ -36,8 +36,6 @@ export interface Suggestion {
|
||||
commandId?: string;
|
||||
commandDescription?: string;
|
||||
commandShortcut?: string;
|
||||
attributeSnippet?: string;
|
||||
highlightedAttributeSnippet?: string;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
@@ -325,33 +323,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
// Add special class for search-notes action
|
||||
const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : "";
|
||||
|
||||
// Choose appropriate icon based on action
|
||||
let iconClass = suggestion.icon ?? "bx bx-note";
|
||||
if (suggestion.action === "search-notes") {
|
||||
iconClass = "bx bx-search";
|
||||
} else if (suggestion.action === "create-note") {
|
||||
iconClass = "bx bx-plus";
|
||||
} else if (suggestion.action === "external-link") {
|
||||
iconClass = "bx bx-link-external";
|
||||
}
|
||||
|
||||
// Simplified HTML structure without nested divs
|
||||
let html = `<div class="note-suggestion ${actionClass}">`;
|
||||
html += `<span class="icon ${iconClass}"></span>`;
|
||||
html += `<span class="text">`;
|
||||
html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`;
|
||||
|
||||
// Add attribute snippet inline if available
|
||||
if (suggestion.highlightedAttributeSnippet) {
|
||||
html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`;
|
||||
}
|
||||
|
||||
html += `</span>`;
|
||||
html += `</div>`;
|
||||
return html;
|
||||
return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`;
|
||||
}
|
||||
},
|
||||
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
||||
|
||||
@@ -862,34 +862,10 @@ table.promoted-attributes-in-tooltip th {
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion {
|
||||
cursor: pointer;
|
||||
padding: 6px 16px;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .icon {
|
||||
display: inline-block;
|
||||
line-height: inherit;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .text {
|
||||
display: inline-block;
|
||||
width: calc(100% - 20px);
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .search-result-title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .search-result-attributes {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: var(--muted-text-color);
|
||||
opacity: 0.6;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -1819,42 +1795,20 @@ textarea {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-dialog {
|
||||
max-width: 900px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .modal-body {
|
||||
padding: 0;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-dropdown-menu {
|
||||
max-height: calc(80vh - 200px);
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.jump-to-note-results {
|
||||
width: 100%;
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestions {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-dropdown-menu .aa-suggestion:hover,
|
||||
.jump-to-note-results .aa-dropdown-menu .aa-cursor {
|
||||
background-color: var(--hover-item-background-color, #f8f9fa);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Command palette styling */
|
||||
@@ -1872,24 +1826,8 @@ textarea {
|
||||
|
||||
.jump-to-note-dialog .aa-cursor .command-suggestion,
|
||||
.jump-to-note-dialog .aa-suggestion:hover .command-suggestion {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .show-in-full-search,
|
||||
.jump-to-note-results .show-in-full-search {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding-top: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestion .search-notes-action {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-suggestion:has(.search-notes-action)::after {
|
||||
display: none;
|
||||
border-left-color: var(--link-color);
|
||||
background-color: var(--hover-background-color);
|
||||
}
|
||||
|
||||
.jump-to-note-dialog .command-icon {
|
||||
@@ -2346,8 +2284,7 @@ footer.webview-footer button {
|
||||
|
||||
/* Search result highlighting */
|
||||
.search-result-title b,
|
||||
.search-result-content b,
|
||||
.search-result-attributes b {
|
||||
.search-result-content b {
|
||||
font-weight: 900;
|
||||
color: var(--admonition-warning-accent-color);
|
||||
}
|
||||
|
||||
@@ -536,9 +536,10 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
|
||||
}
|
||||
|
||||
/* List item */
|
||||
.jump-to-note-dialog .aa-suggestion,
|
||||
.note-detail-empty .aa-suggestion {
|
||||
.jump-to-note-dialog .aa-suggestions div,
|
||||
.note-detail-empty .aa-suggestions div {
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
color: var(--menu-text-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -202,8 +202,7 @@
|
||||
"okButton": "OK"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Suche im Volltext",
|
||||
"search_placeholder": "Suche nach Notiz anhand ihres Titels oder gib > ein für Kommandos..."
|
||||
"search_button": "Suche im Volltext"
|
||||
},
|
||||
"markdown_import": {
|
||||
"dialog_title": "Markdown-Import",
|
||||
@@ -218,26 +217,21 @@
|
||||
"search_placeholder": "Suche nach einer Notiz anhand ihres Namens",
|
||||
"move_button": "Zur ausgewählten Notiz wechseln",
|
||||
"error_no_path": "Kein Weg, auf den man sich bewegen kann.",
|
||||
"move_success_message": "Ausgewählte Notizen wurden verschoben in "
|
||||
"move_success_message": "Ausgewählte Notizen wurden verschoben"
|
||||
},
|
||||
"note_type_chooser": {
|
||||
"modal_title": "Wähle den Notiztyp aus",
|
||||
"modal_body": "Wähle den Notiztyp / die Vorlage der neuen Notiz:",
|
||||
"templates": "Vorlagen",
|
||||
"change_path_prompt": "Ändern wo die neue Notiz erzeugt wird:",
|
||||
"search_placeholder": "Durchsuche Pfad nach Namen (Standard falls leer)",
|
||||
"builtin_templates": "Eingebaute Vorlage"
|
||||
"templates": "Vorlagen"
|
||||
},
|
||||
"password_not_set": {
|
||||
"title": "Das Passwort ist nicht festgelegt",
|
||||
"body1": "Geschützte Notizen werden mit einem Benutzerpasswort verschlüsselt, es wurde jedoch noch kein Passwort festgelegt.",
|
||||
"body2": "Um Notizen zu schützen, klicke den unteren Button um den Optionsdialog zu öffnen und dein Passwort festzulegen.",
|
||||
"go_to_password_options": "Gehe zu Passwortoptionen"
|
||||
"body1": "Geschützte Notizen werden mit einem Benutzerpasswort verschlüsselt, es wurde jedoch noch kein Passwort festgelegt."
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Eingabeaufforderung",
|
||||
"title": "Prompt",
|
||||
"ok": "OK",
|
||||
"defaultTitle": "Eingabeaufforderung"
|
||||
"defaultTitle": "Prompt"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"modal_title": "Geschützte Sitzung",
|
||||
@@ -271,12 +265,10 @@
|
||||
"maximum_revisions": "Maximale Revisionen für aktuelle Notiz: {{number}}.",
|
||||
"settings": "Einstellungen für Notizrevisionen",
|
||||
"download_button": "Herunterladen",
|
||||
"mime": "MIME: ",
|
||||
"mime": "MIME:",
|
||||
"file_size": "Dateigröße:",
|
||||
"preview": "Vorschau:",
|
||||
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar.",
|
||||
"restore_button": "Wiederherstellen",
|
||||
"delete_button": "Löschen"
|
||||
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"sort_children_by": "Unternotizen sortieren nach...",
|
||||
@@ -382,7 +374,7 @@
|
||||
"bookmark_folder": "Notizen mit dieser Bezeichnung werden in den Lesezeichen als Ordner angezeigt (und ermöglichen den Zugriff auf ihre untergeordneten Ordner).",
|
||||
"share_hidden_from_tree": "Diese Notiz ist im linken Navigationsbaum ausgeblendet, kann aber weiterhin über ihre URL aufgerufen werden",
|
||||
"share_external_link": "Die Notiz dient als Link zu einer externen Website im Freigabebaum",
|
||||
"share_alias": "Lege einen Alias fest, mit dem die Notiz unter https://your_trilium_host/share/[dein_alias] verfügbar sein wird.",
|
||||
"share_alias": "Lege einen Alias fest, unter dem die Notiz unter https://your_trilium_host/share/[dein_alias] verfügbar sein wird.",
|
||||
"share_omit_default_css": "Das Standard-CSS für die Freigabeseite wird weggelassen. Verwende es, wenn du umfangreiche Stylingänderungen vornimmst.",
|
||||
"share_root": "Markiert eine Notiz, die im /share-Root bereitgestellt wird.",
|
||||
"share_description": "Definiere Text, der dem HTML-Meta-Tag zur Beschreibung hinzugefügt werden soll",
|
||||
@@ -398,7 +390,7 @@
|
||||
"color": "Definiert die Farbe der Notiz im Notizbaum, in Links usw. Verwende einen beliebigen gültigen CSS-Farbwert wie „rot“ oder #a13d5f",
|
||||
"keyboard_shortcut": "Definiert eine Tastenkombination, die sofort zu dieser Notiz springt. Beispiel: „Strg+Alt+E“. Erfordert ein Neuladen des Frontends, damit die Änderung wirksam wird.",
|
||||
"keep_current_hoisting": "Das Öffnen dieses Links ändert das Hochziehen nicht, selbst wenn die Notiz im aktuell hochgezogenen Unterbaum nicht angezeigt werden kann.",
|
||||
"execute_button": "Titel der Schaltfläche, welche die aktuelle Codenotiz ausführt",
|
||||
"execute_button": "Titel der Schaltfläche, die die aktuelle Codenotiz ausführt",
|
||||
"execute_description": "Längere Beschreibung der aktuellen Codenotiz, die zusammen mit der Schaltfläche „Ausführen“ angezeigt wird",
|
||||
"exclude_from_note_map": "Notizen mit dieser Bezeichnung werden in der Notizenkarte ausgeblendet",
|
||||
"new_notes_on_top": "Neue Notizen werden oben in der übergeordneten Notiz erstellt, nicht unten.",
|
||||
@@ -413,7 +405,7 @@
|
||||
"run_on_branch_change": "wird ausgeführt, wenn ein Zweig aktualisiert wird.",
|
||||
"run_on_branch_deletion": "wird ausgeführt, wenn ein Zweig gelöscht wird. Der Zweig ist eine Verknüpfung zwischen der übergeordneten Notiz und der untergeordneten Notiz und wird z. B. gelöscht. beim Verschieben der Notiz (alter Zweig/Link wird gelöscht).",
|
||||
"run_on_attribute_creation": "wird ausgeführt, wenn für die Notiz ein neues Attribut erstellt wird, das diese Beziehung definiert",
|
||||
"run_on_attribute_change": " wird ausgeführt, wenn das Attribut einer Notiz geändert wird, die diese Beziehung definiert. Dies wird auch ausgelöst, wenn das Attribut gelöscht wird",
|
||||
"run_on_attribute_change": "wird ausgeführt, wenn das Attribut einer Notiz geändert wird, die diese Beziehung definiert. Dies wird auch ausgelöst, wenn das Attribut gelöscht wird",
|
||||
"relation_template": "Die Attribute der Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Der Inhalt und der Unterbaum der Notiz werden den Instanznotizen hinzugefügt, wenn sie leer sind. Einzelheiten findest du in der Dokumentation.",
|
||||
"inherit": "Die Attribute einer Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Ein ähnliches Konzept findest du unter Vorlagenbeziehung. Siehe Attributvererbung in der Dokumentation.",
|
||||
"render_note": "Notizen vom Typ \"HTML-Notiz rendern\" werden mit einer Code-Notiz (HTML oder Skript) gerendert, und es ist notwendig, über diese Beziehung anzugeben, welche Notiz gerendert werden soll.",
|
||||
@@ -426,8 +418,7 @@
|
||||
"other_notes_with_name": "Other notes with {{attributeType}} name \"{{attributeName}}\"",
|
||||
"and_more": "... und {{count}} mehr.",
|
||||
"print_landscape": "Beim Export als PDF, wird die Seitenausrichtung Querformat anstatt Hochformat verwendet.",
|
||||
"print_page_size": "Beim Export als PDF, wird die Größe der Seite angepasst. Unterstützte Größen: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
|
||||
"color_type": "Farbe"
|
||||
"print_page_size": "Beim Export als PDF, wird die Größe der Seite angepasst. Unterstützte Größen: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>."
|
||||
},
|
||||
"attribute_editor": {
|
||||
"help_text_body1": "Um ein Label hinzuzufügen, gebe einfach z.B. ein. <code>#rock</code> oder wenn du auch einen Wert hinzufügen möchten, dann z.B. <code>#year = 2024</code>",
|
||||
@@ -501,7 +492,7 @@
|
||||
"on_all_matched_notes": "Auf allen übereinstimmenden Notizen",
|
||||
"move_note_new_parent": "Verschiebe die Notiz in die neue übergeordnete Notiz, wenn die Notiz nur eine übergeordnete Notiz hat (d. h. der alte Zweig wird entfernt und ein neuer Zweig in die neue übergeordnete Notiz erstellt).",
|
||||
"clone_note_new_parent": "Notiz auf die neue übergeordnete Notiz klonen, wenn die Notiz mehrere Klone/Zweige hat (es ist nicht klar, welcher Zweig entfernt werden soll)",
|
||||
"nothing_will_happen": "Es passiert nichts, wenn die Notiz nicht zur Zielnotiz verschoben werden kann (z.B. wenn dies einen Kreislauf in der Baumstruktur erzeugen würde)."
|
||||
"nothing_will_happen": "Es passiert nichts, wenn die Notiz nicht zur Zielnotiz verschoben werden kann (d. h. dies würde einen Baumzyklus erzeugen)."
|
||||
},
|
||||
"rename_note": {
|
||||
"rename_note": "Notiz umbenennen",
|
||||
@@ -512,7 +503,7 @@
|
||||
"example_note": "<code>Notiz</code> – alle übereinstimmenden Notizen werden in „Notiz“ umbenannt.",
|
||||
"example_new_title": "<code>NEU: ${note.title}</code> – Übereinstimmende Notiztitel erhalten das Präfix „NEU:“",
|
||||
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> – übereinstimmende Notizen werden mit dem Erstellungsmonat und -datum der Notiz vorangestellt",
|
||||
"api_docs": "Siehe API-Dokumente für <a href='https://zadam.github.io/trilium/backend_api/Note.html'>Notiz</a> und seinen <a href='https://day.js.org/ docs/en/display/format'>dateCreatedObj / utcDateCreatedObj properties</a> für Details."
|
||||
"api_docs": "Siehe API-Dokumente für <a hrefu003d'https://zadam.github.io/trilium/backend_api/Note.html'>Notiz</a> und seinen <a hrefu003d'https://day.js.org/ docs/en/display/format'>dateCreatedObj / utcDateCreatedObj-Eigenschaften</a> für Details."
|
||||
},
|
||||
"add_relation": {
|
||||
"add_relation": "Beziehung hinzufügen",
|
||||
@@ -586,8 +577,7 @@
|
||||
"september": "September",
|
||||
"october": "Oktober",
|
||||
"november": "November",
|
||||
"december": "Dezember",
|
||||
"cannot_find_week_note": "Wochennotiz kann nicht gefunden werden"
|
||||
"december": "Dezember"
|
||||
},
|
||||
"close_pane_button": {
|
||||
"close_this_pane": "Schließe diesen Bereich"
|
||||
@@ -709,8 +699,8 @@
|
||||
"zoom_out_title": "Herauszoomen"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Rückverlinkung",
|
||||
"backlinks": "{{count}} Rückverlinkungen",
|
||||
"backlink": "{{count}} Backlink",
|
||||
"backlinks": "{{count}} Backlinks",
|
||||
"relation": "Beziehung"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
@@ -728,8 +718,7 @@
|
||||
"basic_properties": {
|
||||
"note_type": "Notiztyp",
|
||||
"editable": "Bearbeitbar",
|
||||
"basic_properties": "Grundlegende Eigenschaften",
|
||||
"language": "Sprache"
|
||||
"basic_properties": "Grundlegende Eigenschaften"
|
||||
},
|
||||
"book_properties": {
|
||||
"view_type": "Ansichtstyp",
|
||||
@@ -740,11 +729,7 @@
|
||||
"collapse": "Einklappen",
|
||||
"expand": "Ausklappen",
|
||||
"invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“",
|
||||
"calendar": "Kalender",
|
||||
"book_properties": "Sammlungseigenschaften",
|
||||
"table": "Tabelle",
|
||||
"geo-map": "Weltkarte",
|
||||
"board": "Tafel"
|
||||
"calendar": "Kalender"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
|
||||
@@ -820,9 +805,7 @@
|
||||
"unknown_label_type": "Unbekannter Labeltyp „{{type}}“",
|
||||
"unknown_attribute_type": "Unbekannter Attributtyp „{{type}}“",
|
||||
"add_new_attribute": "Neues Attribut hinzufügen",
|
||||
"remove_this_attribute": "Entferne dieses Attribut",
|
||||
"unset-field-placeholder": "nicht gesetzt",
|
||||
"remove_color": "Entferne Farblabel"
|
||||
"remove_this_attribute": "Entferne dieses Attribut"
|
||||
},
|
||||
"script_executor": {
|
||||
"query": "Abfrage",
|
||||
@@ -934,7 +917,7 @@
|
||||
"attachment_detail": {
|
||||
"open_help_page": "Hilfeseite zu Anhängen öffnen",
|
||||
"owning_note": "Eigentümernotiz: ",
|
||||
"you_can_also_open": ", Du kannst auch das öffnen ",
|
||||
"you_can_also_open": ", Du kannst auch das öffnen",
|
||||
"list_of_all_attachments": "Liste aller Anhänge",
|
||||
"attachment_deleted": "Dieser Anhang wurde gelöscht."
|
||||
},
|
||||
@@ -959,8 +942,7 @@
|
||||
"enter_workspace": "Betrete den Arbeitsbereich {{title}}"
|
||||
},
|
||||
"file": {
|
||||
"file_preview_not_available": "Für dieses Dateiformat ist keine Dateivorschau verfügbar.",
|
||||
"too_big": "Die Vorschau zeigt aus Effizienzgründen nur die ersten {{maxNumChars}} Zeichen der Datei an. Lade die Datei herunter und öffne sie extern um den gesamten Inhalt zu sehen."
|
||||
"file_preview_not_available": "Für dieses Dateiformat ist keine Dateivorschau verfügbar."
|
||||
},
|
||||
"protected_session": {
|
||||
"enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
|
||||
@@ -999,7 +981,7 @@
|
||||
"web_view": {
|
||||
"web_view": "Webansicht",
|
||||
"embed_websites": "Notiz vom Typ Web View ermöglicht das Einbetten von Websites in Trilium.",
|
||||
"create_label": "Um zu beginnen, erstelle bitte ein Label mit einer URL-Adresse, die eingebettet werden soll, z. B. #webViewSrc=\"https://www.google.com\""
|
||||
"create_label": "To start, please create a label with a URL address you want to embed, e.g. #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Aktualisieren"
|
||||
@@ -1025,7 +1007,7 @@
|
||||
"error_creating_anonymized_database": "Die anonymisierte Datenbank konnte nicht erstellt werden. Überprüfe die Backend-Protokolle auf Details",
|
||||
"successfully_created_fully_anonymized_database": "Vollständig anonymisierte Datenbank in {{anonymizedFilePath}} erstellt",
|
||||
"successfully_created_lightly_anonymized_database": "Leicht anonymisierte Datenbank in {{anonymizedFilePath}} erstellt",
|
||||
"no_anonymized_database_yet": "Noch keine anonymisierte Datenbank."
|
||||
"no_anonymized_database_yet": "Noch keine anonymisierte Datenbank"
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"title": "Datenbankintegritätsprüfung",
|
||||
@@ -1081,8 +1063,7 @@
|
||||
"max_width_label": "Maximale Inhaltsbreite in Pixel",
|
||||
"apply_changes_description": "Um Änderungen an der Inhaltsbreite anzuwenden, klicke auf",
|
||||
"reload_button": "Frontend neu laden",
|
||||
"reload_description": "Änderungen an den Darstellungsoptionen",
|
||||
"max_width_unit": "Pixel"
|
||||
"reload_description": "Änderungen an den Darstellungsoptionen"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Native Titelleiste (App-Neustart erforderlich)",
|
||||
@@ -1105,10 +1086,7 @@
|
||||
"layout-vertical-title": "Vertikal",
|
||||
"layout-horizontal-title": "Horizontal",
|
||||
"layout-vertical-description": "Startleiste ist auf der linken Seite (standard)",
|
||||
"layout-horizontal-description": "Startleiste ist unter der Tableiste. Die Tableiste wird dadurch auf die ganze Breite erweitert.",
|
||||
"auto_theme": "Alt (Folge dem Farbschema des Systems)",
|
||||
"light_theme": "Alt (Hell)",
|
||||
"dark_theme": "Alt (Dunkel)"
|
||||
"layout-horizontal-description": "Startleiste ist unter der Tableiste. Die Tableiste wird dadurch auf die ganze Breite erweitert."
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Zoomfaktor (nur Desktop-Build)",
|
||||
@@ -1141,7 +1119,7 @@
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_erasure_timeout": "Zeitüberschreitung beim Löschen von Anhängen",
|
||||
"attachment_auto_deletion_description": "Anhänge werden automatisch gelöscht (und gelöscht), wenn sie nach einer definierten Zeitspanne nicht mehr in ihrer Notiz referenziert werden.",
|
||||
"erase_attachments_after": "Nicht verwendete Anhänge löschen nach:",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):",
|
||||
"erase_unused_attachments_now": "Lösche jetzt nicht verwendete Anhangnotizen",
|
||||
"unused_attachments_erased": "Nicht verwendete Anhänge wurden gelöscht."
|
||||
@@ -1152,7 +1130,7 @@
|
||||
},
|
||||
"note_erasure_timeout": {
|
||||
"note_erasure_timeout_title": "Beachte das Zeitlimit für die Löschung",
|
||||
"note_erasure_description": "Gelöschte Notizen (und Attribute, Notizrevisionen...) werden zunächst nur als gelöscht markiert und können über den Dialog „Zuletzt verwendete Notizen” wiederhergestellt werden. Nach einer bestimmten Zeit werden gelöschte Notizen „gelöscht”, was bedeutet, dass ihr Inhalt nicht mehr wiederhergestellt werden kann. Mit dieser Einstellung können Sie die Zeitspanne zwischen dem Löschen und dem endgültigen Löschen der Notiz festlegen.",
|
||||
"note_erasure_description": "Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them from Recent Notes dialog. After a period of time, deleted notes are \"erased\" which means their content is not recoverable anymore. This setting allows you to configure the length of the period between deleting and erasing the note.",
|
||||
"erase_notes_after": "Notizen löschen nach:",
|
||||
"manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):",
|
||||
"erase_deleted_notes_now": "Jetzt gelöschte Notizen löschen",
|
||||
@@ -1383,7 +1361,7 @@
|
||||
"duplicate": "Duplizieren",
|
||||
"export": "Exportieren",
|
||||
"import-into-note": "In Notiz importieren",
|
||||
"apply-bulk-actions": "Massenaktionen anwenden",
|
||||
"apply-bulk-actions": "Massenaktionen ausführen",
|
||||
"converted-to-attachments": "{{count}} Notizen wurden als Anhang konvertiert.",
|
||||
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest?"
|
||||
},
|
||||
@@ -1399,7 +1377,7 @@
|
||||
"relation-map": "Beziehungskarte",
|
||||
"note-map": "Notizkarte",
|
||||
"render-note": "Render Notiz",
|
||||
"mermaid-diagram": "Mermaid Diagramm",
|
||||
"mermaid-diagram": "Mermaid Diagram",
|
||||
"canvas": "Canvas",
|
||||
"web-view": "Webansicht",
|
||||
"mind-map": "Mind Map",
|
||||
@@ -1409,7 +1387,7 @@
|
||||
"doc": "Dokument",
|
||||
"widget": "Widget",
|
||||
"confirm-change": "Es is nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?",
|
||||
"geo-map": "Geo-Karte",
|
||||
"geo-map": "Geo Map",
|
||||
"beta-feature": "Beta"
|
||||
},
|
||||
"protect_note": {
|
||||
@@ -1583,11 +1561,11 @@
|
||||
"label": "Format Toolbar",
|
||||
"floating": {
|
||||
"title": "Schwebend",
|
||||
"description": "Werkzeuge erscheinen in Cursornähe;"
|
||||
"description": "Werkzeuge erscheinen in Cursornähe"
|
||||
},
|
||||
"fixed": {
|
||||
"title": "Fixiert",
|
||||
"description": "Werkzeuge erscheinen im \"Format\" Tab."
|
||||
"description": "Werkzeuge erscheinen im \"Format\" Tab"
|
||||
},
|
||||
"multiline-toolbar": "Toolbar wenn nötig in mehreren Zeilen darstellen."
|
||||
}
|
||||
@@ -1653,49 +1631,5 @@
|
||||
},
|
||||
"modal": {
|
||||
"close": "Schließen"
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued": "{{ count }} Notiz zur Indizierung vorgemerkt",
|
||||
"n_notes_queued_plural": "{{ count }} Notizen zur Indizierung vorgemerkt",
|
||||
"notes_indexed": "{{ count }} Notiz indiziert",
|
||||
"notes_indexed_plural": "{{ count }} Notizen indiziert",
|
||||
"not_started": "Nicht gestartet",
|
||||
"title": "KI Einstellungen",
|
||||
"processed_notes": "Verarbeitete Notizen",
|
||||
"total_notes": "Gesamt Notizen",
|
||||
"progress": "Fortschritt",
|
||||
"queued_notes": "Eingereihte Notizen",
|
||||
"failed_notes": "Fehlgeschlagenen Notizen",
|
||||
"last_processed": "Zuletzt verarbeitet",
|
||||
"refresh_stats": "Statistiken neu laden",
|
||||
"enable_ai_features": "Aktiviere KI/LLM Funktionen",
|
||||
"enable_ai_description": "Aktiviere KI-Funktionen wie Notizzusammenfassungen, Inhaltserzeugung und andere LLM-Funktionen",
|
||||
"openai_tab": "OpenAI",
|
||||
"anthropic_tab": "Anthropic",
|
||||
"voyage_tab": "Voyage AI",
|
||||
"ollama_tab": "Ollama",
|
||||
"enable_ai": "Aktiviere KI/LLM Funktionen",
|
||||
"enable_ai_desc": "Aktiviere KI-Funktionen wie Notizzusammenfassungen, Inhaltserzeugung und andere LLM-Funktionen",
|
||||
"provider_configuration": "KI-Anbieterkonfiguration",
|
||||
"provider_precedence": "Anbieter Priorität",
|
||||
"provider_precedence_description": "Komma-getrennte Liste von Anbieter in der Reihenfolge ihrer Priorität (z.B. 'openai, anthropic,ollama')",
|
||||
"temperature": "Temperatur",
|
||||
"temperature_description": "Regelt die Zufälligkeit in Antworten (0 = deterministisch, 2 = maximale Zufälligkeit)",
|
||||
"system_prompt": "Systemaufforderung",
|
||||
"system_prompt_description": "Standard Systemaufforderung für alle KI-Interaktionen",
|
||||
"openai_configuration": "OpenAI Konfiguration",
|
||||
"openai_settings": "OpenAI Einstellungen",
|
||||
"api_key": "API Schlüssel",
|
||||
"url": "Basis-URL",
|
||||
"model": "Modell"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Verlasse Zen Modus"
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Leistung",
|
||||
"enable-motion": "Aktiviere Übergänge und Animationen",
|
||||
"enable-shadows": "Aktiviere Schatten",
|
||||
"enable-backdrop-effects": "Aktiviere Hintergrundeffekte für Menüs, Pop-up Fenster und Panele"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1814,6 +1814,43 @@
|
||||
"multiline-toolbar": "Display the toolbar on multiple lines if it doesn't fit."
|
||||
}
|
||||
},
|
||||
"ckeditor_plugins": {
|
||||
"title": "Editor Plugins",
|
||||
"description": "Configure which CKEditor plugins are enabled. Changes take effect when the editor is reloaded.",
|
||||
"loading": "Loading plugin configuration...",
|
||||
"load_failed": "Failed to load plugin configuration.",
|
||||
"load_error": "Error loading plugins",
|
||||
"retry": "Retry",
|
||||
"category_formatting": "Text Formatting",
|
||||
"category_structure": "Document Structure",
|
||||
"category_media": "Media & Files",
|
||||
"category_tables": "Tables",
|
||||
"category_advanced": "Advanced Features",
|
||||
"category_trilium": "Trilium Features",
|
||||
"category_external": "External Plugins",
|
||||
"stats_enabled": "Enabled",
|
||||
"stats_total": "Total",
|
||||
"stats_core": "Core",
|
||||
"stats_premium": "Premium",
|
||||
"no_license": "no license",
|
||||
"premium": "Premium",
|
||||
"premium_required": "Requires premium CKEditor license",
|
||||
"has_dependencies": "Dependencies",
|
||||
"depends_on": "Depends on",
|
||||
"toolbar_items": "Toolbar items",
|
||||
"validate": "Validate",
|
||||
"validation_error": "Validation failed",
|
||||
"validation_errors": "Configuration Errors:",
|
||||
"validation_warnings": "Configuration Warnings:",
|
||||
"save": "Save Changes",
|
||||
"save_success": "Plugin configuration saved successfully",
|
||||
"save_error": "Failed to save configuration",
|
||||
"reload_editor_notice": "Please reload any open text notes to apply changes",
|
||||
"reset_defaults": "Reset to Defaults",
|
||||
"reset_confirm": "Are you sure you want to reset all plugin settings to their default values?",
|
||||
"reset_success": "Plugin configuration reset to defaults",
|
||||
"reset_error": "Failed to reset configuration"
|
||||
},
|
||||
"electron_context_menu": {
|
||||
"add-term-to-dictionary": "Add \"{{term}}\" to dictionary",
|
||||
"cut": "Cut",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Over Trilium Notes",
|
||||
"homepage": "Homepagina:",
|
||||
"app_version": "App versie:",
|
||||
"db_version": "DB Versie:",
|
||||
"sync_version": "Sync Versie:"
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
"add_relation": {
|
||||
"add_relation": "Adaugă relație",
|
||||
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
|
||||
"create_relation_on_all_matched_notes": "Creează relația pentru toate notițele găsite.",
|
||||
"create_relation_on_all_matched_notes": "Crează relația pentru toate notițele găsite",
|
||||
"relation_name": "denumirea relației",
|
||||
"target_note": "notița destinație",
|
||||
"to": "către"
|
||||
@@ -76,9 +76,9 @@
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_auto_deletion_description": "Atașamentele se șterg automat (permanent) dacă nu sunt referențiate de către notița lor părinte după un timp prestabilit de timp.",
|
||||
"attachment_erasure_timeout": "Perioadă de ștergere a atașamentelor",
|
||||
"erase_attachments_after": "Șterge atașamentele neutilizate după:",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"erase_unused_attachments_now": "Elimină atașamentele șterse acum",
|
||||
"manual_erasing_description": "Puteți șterge atașamentele nefolosite manual (fără a lua în considerare timpul de mai sus):",
|
||||
"manual_erasing_description": "Șterge acum toate atașamentele nefolosite din notițe",
|
||||
"unused_attachments_erased": "Atașamentele nefolosite au fost șterse."
|
||||
},
|
||||
"attachment_list": {
|
||||
@@ -141,7 +141,7 @@
|
||||
"hide_promoted_attributes": "Ascunde lista atributelor promovate pentru această notiță",
|
||||
"hide_relations": "lista denumirilor relațiilor ce trebuie ascunse, delimitate prin virgulă. Toate celelalte vor fi afișate.",
|
||||
"icon_class": "valoarea acestei etichete este adăugată ca o clasă CSS la iconița notiței din ierarhia notițelor, fapt ce poate ajuta la identificarea vizuală mai rapidă a notițelor. Un exemplu ar fi „bx bx-home” pentru iconițe preluate din boxicons. Poate fi folosită în notițe de tip șablon.",
|
||||
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței marcată cu eticheta <code>#inbox</code>.",
|
||||
"inbox": "locația implicită în care vor apărea noile notițe atunci când se crează o noitiță utilizând butonul „Crează notiță” din bara laterală, notițele vor fi create în interiorul notiței cu această etichetă.",
|
||||
"inherit": "atributele acestei notițe vor fi moștenite chiar dacă nu există o relație părinte-copil între notițe. A se vedea relația de tip șablon pentru un concept similar. De asemenea, a se vedea moștenirea atributelor în documentație.",
|
||||
"inheritable": "Moștenibilă",
|
||||
"inheritable_title": "Atributele moștenibile vor fi moștenite de către toți descendenții acestei notițe.",
|
||||
@@ -177,7 +177,7 @@
|
||||
"render_note": "relație ce definește notița (de tip notiță de cod HTML sau script) ce trebuie randată pentru notițele de tip „Randare notiță HTML”",
|
||||
"run": "definește evenimentele la care să ruleze scriptul. Valori acceptate:\n<ul>\n<li>frontendStartup - când pornește interfața Trilium (sau este reîncărcată), dar nu pe mobil.</li>\n<li>mobileStartup - când pornește interfața Trilium (sau este reîncărcată), doar pe mobil.</li>\n<li>backendStartup - când pornește serverul Trilium</li>\n<li>hourly - o dată pe oră. Se poate utiliza adițional eticheta <code>runAtHour</code> pentru a specifica ora.</li>\n<li>daily - o dată pe zi</li>\n</ul>",
|
||||
"run_at_hour": "La ce oră ar trebui să ruleze. Trebuie folosit împreună cu <code>#run=hourly</code>. Poate fi definit de mai multe ori pentru a rula de mai multe ori în cadrul aceleași zile.",
|
||||
"run_on_attribute_change": " se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
|
||||
"run_on_attribute_change": "se execută atunci când atributele unei notițe care definește această relație se schimbă. Se apelează și atunci când un atribut este șters",
|
||||
"run_on_attribute_creation": "se execută atunci când un nou atribut este creat pentru notița care definește această relație",
|
||||
"run_on_branch_change": "se execută atunci când o ramură este actualizată.",
|
||||
"run_on_branch_creation": "se execută când o ramură este creată. O ramură este o legătură dintre o notiță părinte și o notiță copil și este creată, spre exemplu, la clonarea sau mutarea unei notițe.",
|
||||
@@ -198,7 +198,7 @@
|
||||
"share_disallow_robot_indexing": "împiedică indexarea conținutului de către roboți utilizând antetul <code>X-Robots-Tag: noindex</code>",
|
||||
"share_external_link": "notița va funcționa drept o legătură către un site web extern în ierarhia de partajare",
|
||||
"share_favicon": "Notiță ce conține pictograma favicon pentru a fi setată în paginile partajate. De obicei se poate seta în rădăcina ierarhiei de partajare și se poate face moștenibilă. Notița ce conține favicon-ul trebuie să fie și ea în ierarhia de partajare. Considerați și utilizarea „share_hidden_from_tree”.",
|
||||
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL",
|
||||
"share_hidden_from_tree": "notița este ascunsă din arborele de navigație din stânga, dar încă este accesibilă prin intermediul unui URL.",
|
||||
"share_index": "notițele cu această etichetă vor afișa lista tuturor rădăcilor notițelor partajate",
|
||||
"share_js": "Notiță JavaScript ce va fi injectată în pagina de partajare. Notița respectivă trebuie să fie și ea în ierarhia de partajare. Considerați utilizarea 'share_hidden_from_tree'.",
|
||||
"share_omit_default_css": "CSS-ul implicit pentru pagina de partajare va fi omis. Se poate folosi atunci când se fac schimbări majore de stil la pagină.",
|
||||
@@ -214,7 +214,7 @@
|
||||
"target_note_title": "Relația este o conexiune numită dintre o notiță sursă și o notiță țintă.",
|
||||
"template": "Șablon",
|
||||
"text": "Text",
|
||||
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelor <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații.",
|
||||
"title_template": "titlul implicit al notițelor create în interiorul acestei notițe. Valoarea este evaluată ca un șir de caractere JavaScript\n și poate fi astfel îmbogățită cu un conținut dinamic prin intermediul variabilelow <code>now</code> și <code>parentNote</code>. Exemple:\n \n <ul>\n <li><code>Lucrările lui ${parentNote.getLabelValue('autor')}</code></li>\n <li><code>Jurnal pentru ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n A se vedea <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki-ul pentru detalii</a>, documentația API pentru <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> și <a href=\"https://day.js.org/docs/en/display/format\">now</a> pentru mai multe informații",
|
||||
"toc": "<code>#toc</code> sau <code>#toc=show</code> forțează afișarea tabelei de conținut, <code>#toc=hide</code> forțează ascunderea ei. Dacă eticheta nu există, se utilizează setările globale",
|
||||
"top": "păstrează notița la începutul listei (se aplică doar pentru notițe sortate automat)",
|
||||
"url": "URL",
|
||||
@@ -369,7 +369,7 @@
|
||||
},
|
||||
"confirm": {
|
||||
"also_delete_note": "Șterge și notița",
|
||||
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații? ",
|
||||
"are_you_sure_remove_note": "Doriți ștergerea notiței „{{title}}” din harta de relații?",
|
||||
"cancel": "Anulează",
|
||||
"confirmation": "Confirm",
|
||||
"if_you_dont_check": "Dacă această opțiune nu este bifată, notița va fi ștearsă doar din harta de relații.",
|
||||
@@ -519,8 +519,8 @@
|
||||
"export_status": "Starea exportului",
|
||||
"export_type_single": "Doar această notiță fără descendenții ei",
|
||||
"export_type_subtree": "Această notiță și toți descendenții ei",
|
||||
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea.",
|
||||
"format_markdown": "Markdown - păstrează majoritatea formatării.",
|
||||
"format_html_zip": "HTML în arhivă ZIP - recomandat deoarece păstrează toată formatarea",
|
||||
"format_markdown": "Markdown - păstrează majoritatea formatării",
|
||||
"format_opml": "OPML - format de interschimbare pentru editoare cu structură ierarhică (outline). Formatarea, imaginile și fișierele nu vor fi incluse.",
|
||||
"opml_version_1": "OPML v1.0 - text simplu",
|
||||
"opml_version_2": "OPML v2.0 - permite și HTML",
|
||||
@@ -640,7 +640,7 @@
|
||||
"newTabNoteLink": "pe o legătură către o notiță va deschide notița într-un tab nou",
|
||||
"notSet": "nesetat",
|
||||
"noteNavigation": "Navigarea printre notițe",
|
||||
"numberedList": "<code>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
|
||||
"numberedList": "<kbd>1.</code> sau <code>1)</code> urmat de spațiu pentru o listă numerotată",
|
||||
"onlyInDesktop": "Doar pentru desktop (aplicația Electron)",
|
||||
"openEmptyTab": "deschide un tab nou",
|
||||
"other": "Altele",
|
||||
@@ -807,7 +807,7 @@
|
||||
"dialog_title": "Mută notițele în...",
|
||||
"error_no_path": "Nicio cale la care să poată fi mutate.",
|
||||
"move_button": "Mută la notița selectată",
|
||||
"move_success_message": "Notițele selectate au fost mutate în ",
|
||||
"move_success_message": "Notițele selectate au fost mutate în",
|
||||
"notes_to_move": "Notițe de mutat",
|
||||
"search_placeholder": "căutați notița după denumirea ei",
|
||||
"target_parent_note": "Notița părinte destinație"
|
||||
@@ -1058,7 +1058,7 @@
|
||||
"download_button": "Descarcă",
|
||||
"file_size": "Dimensiune fișier:",
|
||||
"help_title": "Informații despre reviziile notițelor",
|
||||
"mime": "MIME: ",
|
||||
"mime": "MIME:",
|
||||
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
|
||||
"note_revisions": "Revizii ale notiței",
|
||||
"preview": "Previzualizare:",
|
||||
@@ -1193,7 +1193,7 @@
|
||||
"enable": "Activează corectorul ortografic",
|
||||
"language_code_label": "Codurile de limbă",
|
||||
"language_code_placeholder": "de exemplu „en-US”, „de-AT”",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\". ",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\".",
|
||||
"title": "Corector ortografic",
|
||||
"restart-required": "Schimbările asupra setărilor corectorului ortografic vor fi aplicate după restartarea aplicației."
|
||||
},
|
||||
@@ -1286,7 +1286,7 @@
|
||||
"update_relation_target": {
|
||||
"allowed_characters": "Sunt permise doar caractere alfanumerice, underline și două puncte.",
|
||||
"change_target_note": "schimbă notița-țintă a unei relații existente",
|
||||
"on_all_matched_notes": "Pentru toate notițele găsite",
|
||||
"on_all_matched_notes": "Pentru toate notițele găsite:",
|
||||
"relation_name": "denumirea relației",
|
||||
"target_note": "notița destinație",
|
||||
"to": "la",
|
||||
@@ -1314,7 +1314,7 @@
|
||||
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
|
||||
},
|
||||
"web_view": {
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
|
||||
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
|
||||
"web_view": "Vizualizare web"
|
||||
},
|
||||
@@ -1863,16 +1863,11 @@
|
||||
},
|
||||
"create_new_ai_chat": "Crează o nouă discuție cu AI-ul",
|
||||
"configuration_warnings": "Sunt câteva probleme la configurația AI-ului. Verificați setările.",
|
||||
"experimental_warning": "Funcția LLM este experimentală.",
|
||||
"experimental_warning": "Funcția LLM este experimentală!",
|
||||
"selected_provider": "Furnizor selectat",
|
||||
"selected_provider_description": "Selectați furnizorul de AI pentru funcțiile de discuție și completare",
|
||||
"select_model": "Selectați modelul...",
|
||||
"select_provider": "Selectați furnizorul...",
|
||||
"ai_enabled": "Funcționalitățile AI au fost activate",
|
||||
"ai_disabled": "Funcționalitățile AI au fost dezactivate",
|
||||
"no_models_found_online": "Nu s-a găsit niciun model. Verificați cheia API și configurația.",
|
||||
"no_models_found_ollama": "Nu s-a găsit niciun model Ollama. Verificați dacă Ollama rulează.",
|
||||
"error_fetching": "Eroare la obținerea modelelor: {{error}}"
|
||||
"select_provider": "Selectați furnizorul..."
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Format dată/timp personalizat",
|
||||
@@ -2003,26 +1998,6 @@
|
||||
"call_to_action": {
|
||||
"background_effects_title": "Efectele de fundal sunt acum stabile",
|
||||
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
|
||||
"background_effects_button": "Activează efectele de fundal",
|
||||
"next_theme_title": "Încercați noua temă Trilium",
|
||||
"next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?",
|
||||
"next_theme_button": "Testează noua temă",
|
||||
"dismiss": "Treci peste"
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Setări de performanță",
|
||||
"enable-motion": "Activează tranzițiile și animațiile",
|
||||
"enable-shadows": "Activează umbrirea elementelor",
|
||||
"enable-backdrop-effects": "Activează efectele de fundal pentru meniuri, popup-uri și panouri"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Setări similare"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Tema de culori pentru blocuri de cod în notițe de tip text",
|
||||
"related_code_notes": "Tema de culori pentru notițele de tip cod"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
"background_effects_button": "Activează efectele de fundal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
|
||||
openService.openDirectory(directory);
|
||||
};
|
||||
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}>{directory}</a>
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}></a>
|
||||
} else {
|
||||
return <span style={style}>{directory}</span>;
|
||||
}
|
||||
|
||||
@@ -30,14 +30,6 @@ function AddLinkDialogComponent() {
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSelection) {
|
||||
setLinkType("hyper-link");
|
||||
} else {
|
||||
setLinkType("reference-link");
|
||||
}
|
||||
}, [ hasSelection ])
|
||||
|
||||
async function setDefaultLinkTitle(noteId: string) {
|
||||
const noteTitle = await tree.getNoteTitle(noteId);
|
||||
setLinkTitle(noteTitle);
|
||||
|
||||
@@ -9,7 +9,6 @@ import appContext from "../../components/app_context";
|
||||
import commandRegistry from "../../services/command_registry";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import useTriliumEvent from "../react/hooks";
|
||||
import shortcutService from "../../services/shortcuts";
|
||||
|
||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||
|
||||
@@ -84,27 +83,6 @@ function JumpToNoteDialogComponent() {
|
||||
$autoComplete
|
||||
.trigger("focus")
|
||||
.trigger("select");
|
||||
|
||||
// Add keyboard shortcut for full search
|
||||
shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => {
|
||||
if (!isCommandMode) {
|
||||
showInFullSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function showInFullSearch() {
|
||||
try {
|
||||
setShown(false);
|
||||
const searchString = actualText.current?.trim();
|
||||
if (searchString && !searchString.startsWith(">")) {
|
||||
await appContext.triggerCommand("searchNotes", {
|
||||
searchString
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to trigger full search:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -130,12 +108,7 @@ function JumpToNoteDialogComponent() {
|
||||
/>}
|
||||
onShown={onShown}
|
||||
onHidden={() => setShown(false)}
|
||||
footer={!isCommandMode && <Button
|
||||
className="show-in-full-text-button"
|
||||
text={t("jump_to_note.search_button")}
|
||||
keyboardShortcut="Ctrl+Enter"
|
||||
onClick={showInFullSearch}
|
||||
/>}
|
||||
footer={!isCommandMode && <Button className="show-in-full-text-button" text={t("jump_to_note.search_button")} keyboardShortcut="Ctrl+Enter" />}
|
||||
show={shown}
|
||||
>
|
||||
<div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div>
|
||||
|
||||
@@ -152,7 +152,7 @@ const TPL = /*html*/`
|
||||
const MAX_SEARCH_RESULTS_IN_TREE = 100;
|
||||
|
||||
// this has to be hanged on the actual elements to effectively intercept and stop click event
|
||||
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
|
||||
const cancelClickPropagation: JQuery.TypeEventHandler<unknown, unknown, unknown, unknown, any> = (e) => e.stopPropagation();
|
||||
|
||||
// TODO: Fix once we remove Node.js API from public
|
||||
type Timeout = NodeJS.Timeout | string | number | undefined;
|
||||
|
||||
@@ -13,6 +13,7 @@ import noteAutocompleteService, { type Suggestion } from "../../../services/note
|
||||
import mimeTypesService from "../../../services/mime_types.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import { buildToolbarConfig } from "./toolbar.js";
|
||||
import ckeditorPluginConfigService from "../../../services/ckeditor_plugin_config.js";
|
||||
|
||||
export const OPEN_SOURCE_LICENSE_KEY = "GPL";
|
||||
|
||||
@@ -164,7 +165,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
},
|
||||
// This value must be kept in sync with the language defined in webpack.config.js.
|
||||
language: "en",
|
||||
removePlugins: getDisabledPlugins()
|
||||
removePlugins: await getDisabledPlugins()
|
||||
};
|
||||
|
||||
// Set up content language.
|
||||
@@ -203,9 +204,11 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
];
|
||||
}
|
||||
|
||||
const toolbarConfig = await buildToolbarConfig(opts.isClassicEditor);
|
||||
|
||||
return {
|
||||
...config,
|
||||
...buildToolbarConfig(opts.isClassicEditor)
|
||||
...toolbarConfig
|
||||
};
|
||||
}
|
||||
|
||||
@@ -237,9 +240,18 @@ function getLicenseKey() {
|
||||
return premiumLicenseKey;
|
||||
}
|
||||
|
||||
function getDisabledPlugins() {
|
||||
async function getDisabledPlugins() {
|
||||
let disabledPlugins: string[] = [];
|
||||
|
||||
// Check user's plugin configuration
|
||||
try {
|
||||
const userDisabledPlugins = await ckeditorPluginConfigService.getDisabledPlugins();
|
||||
disabledPlugins.push(...userDisabledPlugins);
|
||||
} catch (error) {
|
||||
console.warn("Failed to load user plugin configuration, using defaults:", error);
|
||||
}
|
||||
|
||||
// Legacy emoji setting override
|
||||
if (options.get("textNoteEmojiCompletionEnabled") !== "true") {
|
||||
disabledPlugins.push("EmojiMention");
|
||||
}
|
||||
|
||||
@@ -1,33 +1,73 @@
|
||||
import utils from "../../../services/utils.js";
|
||||
import options from "../../../services/options.js";
|
||||
import ckeditorPluginConfigService from "../../../services/ckeditor_plugin_config.js";
|
||||
|
||||
const TEXT_FORMATTING_GROUP = {
|
||||
label: "Text formatting",
|
||||
icon: "text"
|
||||
};
|
||||
|
||||
export function buildToolbarConfig(isClassicToolbar: boolean) {
|
||||
export async function buildToolbarConfig(isClassicToolbar: boolean) {
|
||||
const hiddenItems = await getHiddenToolbarItems();
|
||||
|
||||
if (utils.isMobile()) {
|
||||
return buildMobileToolbar();
|
||||
return buildMobileToolbar(hiddenItems);
|
||||
} else if (isClassicToolbar) {
|
||||
const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true";
|
||||
return buildClassicToolbar(multilineToolbar);
|
||||
return buildClassicToolbar(multilineToolbar, hiddenItems);
|
||||
} else {
|
||||
return buildFloatingToolbar();
|
||||
return buildFloatingToolbar(hiddenItems);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildMobileToolbar() {
|
||||
const classicConfig = buildClassicToolbar(false);
|
||||
async function getHiddenToolbarItems(): Promise<Set<string>> {
|
||||
try {
|
||||
const hiddenItems = await ckeditorPluginConfigService.getHiddenToolbarItems();
|
||||
return new Set(hiddenItems);
|
||||
} catch (error) {
|
||||
console.warn("Failed to get hidden toolbar items, using empty set:", error);
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter toolbar items based on disabled plugins
|
||||
*/
|
||||
function filterToolbarItems(items: (string | object)[], hiddenItems: Set<string>): (string | object)[] {
|
||||
return items.filter(item => {
|
||||
if (typeof item === 'string') {
|
||||
// Don't hide separators
|
||||
if (item === '|') return true;
|
||||
// Check if this item should be hidden
|
||||
return !hiddenItems.has(item);
|
||||
} else if (typeof item === 'object' && item !== null && 'items' in item) {
|
||||
// Filter nested items recursively
|
||||
const nestedItem = item as { items: (string | object)[] };
|
||||
const filteredNested = filterToolbarItems(nestedItem.items, hiddenItems);
|
||||
// Only keep the group if it has at least one non-separator item
|
||||
const hasNonSeparatorItems = filteredNested.some(subItem =>
|
||||
typeof subItem === 'string' ? subItem !== '|' : true
|
||||
);
|
||||
if (hasNonSeparatorItems) {
|
||||
return { ...item, items: filteredNested };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return true;
|
||||
}).filter(item => item !== null) as (string | object)[];
|
||||
}
|
||||
|
||||
export function buildMobileToolbar(hiddenItems: Set<string>) {
|
||||
const classicConfig = buildClassicToolbar(false, hiddenItems);
|
||||
const items: string[] = [];
|
||||
|
||||
for (const item of classicConfig.toolbar.items) {
|
||||
if (typeof item === "object" && "items" in item) {
|
||||
for (const subitem of item.items) {
|
||||
for (const subitem of (item as any).items) {
|
||||
items.push(subitem);
|
||||
}
|
||||
} else {
|
||||
items.push(item);
|
||||
items.push(item as string);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,110 +80,115 @@ export function buildMobileToolbar() {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildClassicToolbar(multilineToolbar: boolean) {
|
||||
export function buildClassicToolbar(multilineToolbar: boolean, hiddenItems: Set<string>) {
|
||||
// For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars.
|
||||
const items = [
|
||||
"heading",
|
||||
"fontSize",
|
||||
"|",
|
||||
"bold",
|
||||
"italic",
|
||||
{
|
||||
...TEXT_FORMATTING_GROUP,
|
||||
items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"]
|
||||
},
|
||||
"|",
|
||||
"fontColor",
|
||||
"fontBackgroundColor",
|
||||
"removeFormat",
|
||||
"|",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
"todoList",
|
||||
"|",
|
||||
"blockQuote",
|
||||
"admonition",
|
||||
"insertTable",
|
||||
"|",
|
||||
"code",
|
||||
"codeBlock",
|
||||
"|",
|
||||
"footnote",
|
||||
{
|
||||
label: "Insert",
|
||||
icon: "plus",
|
||||
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
||||
},
|
||||
"|",
|
||||
"alignment",
|
||||
"outdent",
|
||||
"indent",
|
||||
"|",
|
||||
"insertTemplate",
|
||||
"markdownImport",
|
||||
"cuttonote",
|
||||
"findAndReplace"
|
||||
];
|
||||
|
||||
return {
|
||||
toolbar: {
|
||||
items: [
|
||||
"heading",
|
||||
"fontSize",
|
||||
"|",
|
||||
"bold",
|
||||
"italic",
|
||||
{
|
||||
...TEXT_FORMATTING_GROUP,
|
||||
items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"]
|
||||
},
|
||||
"|",
|
||||
"fontColor",
|
||||
"fontBackgroundColor",
|
||||
"removeFormat",
|
||||
"|",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
"todoList",
|
||||
"|",
|
||||
"blockQuote",
|
||||
"admonition",
|
||||
"insertTable",
|
||||
"|",
|
||||
"code",
|
||||
"codeBlock",
|
||||
"|",
|
||||
"footnote",
|
||||
{
|
||||
label: "Insert",
|
||||
icon: "plus",
|
||||
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
||||
},
|
||||
"|",
|
||||
"alignment",
|
||||
"outdent",
|
||||
"indent",
|
||||
"|",
|
||||
"insertTemplate",
|
||||
"markdownImport",
|
||||
"cuttonote",
|
||||
"findAndReplace"
|
||||
],
|
||||
items: filterToolbarItems(items, hiddenItems),
|
||||
shouldNotGroupWhenFull: multilineToolbar
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFloatingToolbar() {
|
||||
export function buildFloatingToolbar(hiddenItems: Set<string>) {
|
||||
const toolbarItems = [
|
||||
"fontSize",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
{
|
||||
...TEXT_FORMATTING_GROUP,
|
||||
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
|
||||
},
|
||||
"|",
|
||||
"fontColor",
|
||||
"fontBackgroundColor",
|
||||
"|",
|
||||
"code",
|
||||
"link",
|
||||
"bookmark",
|
||||
"removeFormat",
|
||||
"internallink",
|
||||
"cuttonote"
|
||||
];
|
||||
|
||||
const blockToolbarItems = [
|
||||
"heading",
|
||||
"|",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
"todoList",
|
||||
"|",
|
||||
"blockQuote",
|
||||
"admonition",
|
||||
"codeBlock",
|
||||
"insertTable",
|
||||
"footnote",
|
||||
{
|
||||
label: "Insert",
|
||||
icon: "plus",
|
||||
items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
||||
},
|
||||
"|",
|
||||
"alignment",
|
||||
"outdent",
|
||||
"indent",
|
||||
"|",
|
||||
"insertTemplate",
|
||||
"imageUpload",
|
||||
"markdownImport",
|
||||
"specialCharacters",
|
||||
"emoji",
|
||||
"findAndReplace"
|
||||
];
|
||||
|
||||
return {
|
||||
toolbar: {
|
||||
items: [
|
||||
"fontSize",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
{
|
||||
...TEXT_FORMATTING_GROUP,
|
||||
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
|
||||
},
|
||||
"|",
|
||||
"fontColor",
|
||||
"fontBackgroundColor",
|
||||
"|",
|
||||
"code",
|
||||
"link",
|
||||
"bookmark",
|
||||
"removeFormat",
|
||||
"internallink",
|
||||
"cuttonote"
|
||||
]
|
||||
items: filterToolbarItems(toolbarItems, hiddenItems)
|
||||
},
|
||||
|
||||
blockToolbar: [
|
||||
"heading",
|
||||
"|",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
"todoList",
|
||||
"|",
|
||||
"blockQuote",
|
||||
"admonition",
|
||||
"codeBlock",
|
||||
"insertTable",
|
||||
"footnote",
|
||||
{
|
||||
label: "Insert",
|
||||
icon: "plus",
|
||||
items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
||||
},
|
||||
"|",
|
||||
"alignment",
|
||||
"outdent",
|
||||
"indent",
|
||||
"|",
|
||||
"insertTemplate",
|
||||
"imageUpload",
|
||||
"markdownImport",
|
||||
"specialCharacters",
|
||||
"emoji",
|
||||
"findAndReplace"
|
||||
]
|
||||
blockToolbar: filterToolbarItems(blockToolbarItems, hiddenItems)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import PasswordSettings from "./options/password.jsx";
|
||||
import ShortcutSettings from "./options/shortcuts.js";
|
||||
import TextNoteSettings from "./options/text_notes.jsx";
|
||||
import CodeNoteSettings from "./options/code_notes.jsx";
|
||||
import CKEditorPluginSettings from "./options/ckeditor_plugins.jsx";
|
||||
import OtherSettings from "./options/other.jsx";
|
||||
import BackendLogWidget from "./content/backend_log.js";
|
||||
import MultiFactorAuthenticationSettings from "./options/multi_factor_authentication.js";
|
||||
@@ -45,13 +46,14 @@ const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printabl
|
||||
<div class="note-detail-content-widget-content"></div>
|
||||
</div>`;
|
||||
|
||||
export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced";
|
||||
export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsCKEditorPlugins" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced";
|
||||
|
||||
const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextAwareWidget)[] | JSX.Element)> = {
|
||||
_optionsAppearance: <AppearanceSettings />,
|
||||
_optionsShortcuts: <ShortcutSettings />,
|
||||
_optionsTextNotes: <TextNoteSettings />,
|
||||
_optionsCodeNotes: <CodeNoteSettings />,
|
||||
_optionsCKEditorPlugins: <CKEditorPluginSettings />,
|
||||
_optionsImages: <ImageSettings />,
|
||||
_optionsSpellcheck: <SpellcheckSettings />,
|
||||
_optionsPassword: <PasswordSettings />,
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
import { useEffect, useState, useCallback, useMemo } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormText from "../../react/FormText";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import Button from "../../react/Button";
|
||||
import toast from "../../../services/toast";
|
||||
import type {
|
||||
PluginMetadata,
|
||||
PluginConfiguration,
|
||||
PluginRegistry,
|
||||
PluginValidationResult,
|
||||
UpdatePluginConfigRequest,
|
||||
UpdatePluginConfigResponse,
|
||||
QueryPluginsResult,
|
||||
PluginCategory
|
||||
} from "@triliumnext/commons";
|
||||
|
||||
interface PluginStats {
|
||||
enabled: number;
|
||||
total: number;
|
||||
core: number;
|
||||
premium: number;
|
||||
configurable: number;
|
||||
categories: Record<string, number>;
|
||||
hasPremiumLicense: boolean;
|
||||
}
|
||||
|
||||
const CATEGORY_DISPLAY_NAMES: Record<PluginCategory, string> = {
|
||||
formatting: t("ckeditor_plugins.category_formatting"),
|
||||
structure: t("ckeditor_plugins.category_structure"),
|
||||
media: t("ckeditor_plugins.category_media"),
|
||||
tables: t("ckeditor_plugins.category_tables"),
|
||||
advanced: t("ckeditor_plugins.category_advanced"),
|
||||
trilium: t("ckeditor_plugins.category_trilium"),
|
||||
external: t("ckeditor_plugins.category_external")
|
||||
};
|
||||
|
||||
export default function CKEditorPluginSettings() {
|
||||
const [pluginRegistry, setPluginRegistry] = useState<PluginRegistry | null>(null);
|
||||
const [userConfig, setUserConfig] = useState<PluginConfiguration[]>([]);
|
||||
const [stats, setStats] = useState<PluginStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState<PluginValidationResult | null>(null);
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
|
||||
// Load initial data
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [registry, config, statsData] = await Promise.all([
|
||||
server.get<PluginRegistry>('ckeditor-plugins/registry'),
|
||||
server.get<PluginConfiguration[]>('ckeditor-plugins/config'),
|
||||
server.get<PluginStats>('ckeditor-plugins/stats')
|
||||
]);
|
||||
|
||||
setPluginRegistry(registry);
|
||||
setUserConfig(config);
|
||||
setStats(statsData);
|
||||
} catch (error) {
|
||||
toast.showError(`${t("ckeditor_plugins.load_error")}: ${error}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Organize plugins by category
|
||||
const pluginsByCategory = useMemo(() => {
|
||||
if (!pluginRegistry) return {};
|
||||
|
||||
const categories: Record<PluginCategory, PluginMetadata[]> = {
|
||||
formatting: [],
|
||||
structure: [],
|
||||
media: [],
|
||||
tables: [],
|
||||
advanced: [],
|
||||
trilium: [],
|
||||
external: []
|
||||
};
|
||||
|
||||
Object.values(pluginRegistry.plugins).forEach(plugin => {
|
||||
if (!plugin.isCore) { // Don't show core plugins in settings
|
||||
categories[plugin.category].push(plugin);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort plugins within each category by name
|
||||
Object.keys(categories).forEach(category => {
|
||||
categories[category as PluginCategory].sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
return categories;
|
||||
}, [pluginRegistry]);
|
||||
|
||||
// Get enabled status for a plugin
|
||||
const isPluginEnabled = useCallback((pluginId: string): boolean => {
|
||||
return userConfig.find(config => config.id === pluginId)?.enabled ?? false;
|
||||
}, [userConfig]);
|
||||
|
||||
// Toggle plugin enabled state
|
||||
const togglePlugin = useCallback((pluginId: string) => {
|
||||
setUserConfig(prev => prev.map(config =>
|
||||
config.id === pluginId
|
||||
? { ...config, enabled: !config.enabled }
|
||||
: config
|
||||
));
|
||||
}, []);
|
||||
|
||||
// Validate current configuration
|
||||
const validateConfig = useCallback(async () => {
|
||||
if (!userConfig.length) return;
|
||||
|
||||
try {
|
||||
const result = await server.post<PluginValidationResult>('ckeditor-plugins/validate', {
|
||||
plugins: userConfig
|
||||
});
|
||||
setValidationResult(result);
|
||||
setShowValidation(true);
|
||||
return result;
|
||||
} catch (error) {
|
||||
toast.showError(`${t("ckeditor_plugins.validation_error")}: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}, [userConfig]);
|
||||
|
||||
// Save configuration
|
||||
const saveConfiguration = useCallback(async () => {
|
||||
setSaving(true);
|
||||
setShowValidation(false);
|
||||
|
||||
try {
|
||||
const request: UpdatePluginConfigRequest = {
|
||||
plugins: userConfig,
|
||||
validate: true
|
||||
};
|
||||
|
||||
const response = await server.put<UpdatePluginConfigResponse>('ckeditor-plugins/config', request);
|
||||
|
||||
if (response.success) {
|
||||
toast.showMessage(t("ckeditor_plugins.save_success"));
|
||||
await loadData(); // Reload stats
|
||||
|
||||
// Notify user that editor reload might be needed
|
||||
toast.showMessage(t("ckeditor_plugins.reload_editor_notice"), {
|
||||
timeout: 5000
|
||||
});
|
||||
} else {
|
||||
setValidationResult(response.validation);
|
||||
setShowValidation(true);
|
||||
toast.showError(`${t("ckeditor_plugins.save_error")}: ${response.errors?.join(", ")}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.showError(`${t("ckeditor_plugins.save_error")}: ${error}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [userConfig, loadData]);
|
||||
|
||||
// Reset to defaults
|
||||
const resetToDefaults = useCallback(async () => {
|
||||
if (!confirm(t("ckeditor_plugins.reset_confirm"))) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await server.post<UpdatePluginConfigResponse>('ckeditor-plugins/reset');
|
||||
if (response.success) {
|
||||
setUserConfig(response.plugins);
|
||||
toast.showMessage(t("ckeditor_plugins.reset_success"));
|
||||
await loadData();
|
||||
} else {
|
||||
toast.showError(`${t("ckeditor_plugins.reset_error")}: ${response.errors?.join(", ")}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.showError(`${t("ckeditor_plugins.reset_error")}: ${error}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [loadData]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<OptionsSection title={t("ckeditor_plugins.title")}>
|
||||
<FormText>{t("ckeditor_plugins.loading")}</FormText>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (!pluginRegistry || !stats) {
|
||||
return (
|
||||
<OptionsSection title={t("ckeditor_plugins.title")}>
|
||||
<FormText>{t("ckeditor_plugins.load_failed")}</FormText>
|
||||
<Button text={t("ckeditor_plugins.retry")} onClick={loadData} />
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<OptionsSection title={t("ckeditor_plugins.title")}>
|
||||
<FormText>{t("ckeditor_plugins.description")}</FormText>
|
||||
|
||||
{/* Stats overview */}
|
||||
<div className="plugin-stats" style={{
|
||||
backgroundColor: 'var(--accented-background-color)',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<strong>{t("ckeditor_plugins.stats_enabled")}</strong><br />
|
||||
<span style={{ fontSize: '1.2em', color: 'var(--main-text-color)' }}>
|
||||
{stats.enabled}/{stats.configurable}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<strong>{t("ckeditor_plugins.stats_total")}</strong><br />
|
||||
<span style={{ fontSize: '1.2em', color: 'var(--main-text-color)' }}>
|
||||
{stats.total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<strong>{t("ckeditor_plugins.stats_core")}</strong><br />
|
||||
<span style={{ fontSize: '1.2em', color: 'var(--main-text-color)' }}>
|
||||
{stats.core}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<strong>{t("ckeditor_plugins.stats_premium")}</strong><br />
|
||||
<span style={{ fontSize: '1.2em', color: stats.hasPremiumLicense ? 'var(--success-color)' : 'var(--muted-text-color)' }}>
|
||||
{stats.premium} {!stats.hasPremiumLicense && `(${t("ckeditor_plugins.no_license")})`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation results */}
|
||||
{showValidation && validationResult && (
|
||||
<div className="validation-results" style={{ marginBottom: '20px' }}>
|
||||
{!validationResult.valid && (
|
||||
<div className="alert alert-danger">
|
||||
<strong>{t("ckeditor_plugins.validation_errors")}</strong>
|
||||
<ul>
|
||||
{validationResult.errors.map((error, index) => (
|
||||
<li key={index}>{error.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{validationResult.warnings.length > 0 && (
|
||||
<div className="alert alert-warning">
|
||||
<strong>{t("ckeditor_plugins.validation_warnings")}</strong>
|
||||
<ul>
|
||||
{validationResult.warnings.map((warning, index) => (
|
||||
<li key={index}>{warning.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="plugin-actions" style={{ marginBottom: '20px' }}>
|
||||
<Button
|
||||
text={t("ckeditor_plugins.validate")}
|
||||
onClick={validateConfig}
|
||||
disabled={saving}
|
||||
className="btn-secondary"
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
text={t("ckeditor_plugins.save")}
|
||||
onClick={saveConfiguration}
|
||||
disabled={saving}
|
||||
className="btn-primary"
|
||||
size="small"
|
||||
style={{ marginLeft: '10px' }}
|
||||
/>
|
||||
<Button
|
||||
text={t("ckeditor_plugins.reset_defaults")}
|
||||
onClick={resetToDefaults}
|
||||
disabled={saving}
|
||||
className="btn-secondary"
|
||||
size="small"
|
||||
style={{ marginLeft: '10px' }}
|
||||
/>
|
||||
</div>
|
||||
</OptionsSection>
|
||||
|
||||
{/* Plugin categories */}
|
||||
{Object.entries(pluginsByCategory).map(([categoryKey, plugins]) => {
|
||||
if (plugins.length === 0) return null;
|
||||
|
||||
const category = categoryKey as PluginCategory;
|
||||
return (
|
||||
<OptionsSection key={category} title={CATEGORY_DISPLAY_NAMES[category]} level={2}>
|
||||
<div className="plugin-category">
|
||||
{plugins.map(plugin => (
|
||||
<PluginConfigItem
|
||||
key={plugin.id}
|
||||
plugin={plugin}
|
||||
enabled={isPluginEnabled(plugin.id)}
|
||||
onToggle={() => togglePlugin(plugin.id)}
|
||||
hasPremiumLicense={stats.hasPremiumLicense}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</OptionsSection>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PluginConfigItemProps {
|
||||
plugin: PluginMetadata;
|
||||
enabled: boolean;
|
||||
onToggle: () => void;
|
||||
hasPremiumLicense: boolean;
|
||||
}
|
||||
|
||||
function PluginConfigItem({ plugin, enabled, onToggle, hasPremiumLicense }: PluginConfigItemProps) {
|
||||
const canEnable = !plugin.requiresPremium || hasPremiumLicense;
|
||||
|
||||
return (
|
||||
<FormGroup name={`plugin-${plugin.id}`} style={{ marginBottom: '15px' }}>
|
||||
<div className="plugin-item" style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
opacity: canEnable ? 1 : 0.6
|
||||
}}>
|
||||
<FormCheckbox
|
||||
label=""
|
||||
currentValue={enabled && canEnable}
|
||||
onChange={canEnable ? onToggle : undefined}
|
||||
disabled={!canEnable}
|
||||
containerStyle={{ marginRight: '10px', marginTop: '2px' }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<span>{plugin.name}</span>
|
||||
{plugin.requiresPremium && (
|
||||
<span className="badge badge-warning" style={{ fontSize: '0.75em' }}>
|
||||
{t("ckeditor_plugins.premium")}
|
||||
</span>
|
||||
)}
|
||||
{plugin.dependencies.length > 0 && (
|
||||
<span className="badge badge-info" style={{ fontSize: '0.75em' }}>
|
||||
{t("ckeditor_plugins.has_dependencies")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.9em',
|
||||
color: 'var(--muted-text-color)',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{plugin.description}
|
||||
</div>
|
||||
{plugin.dependencies.length > 0 && (
|
||||
<div style={{ fontSize: '0.8em', color: 'var(--muted-text-color)' }}>
|
||||
{t("ckeditor_plugins.depends_on")}: {plugin.dependencies.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{plugin.toolbarItems && plugin.toolbarItems.length > 0 && (
|
||||
<div style={{ fontSize: '0.8em', color: 'var(--muted-text-color)' }}>
|
||||
{t("ckeditor_plugins.toolbar_items")}: {plugin.toolbarItems.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{!canEnable && (
|
||||
<div style={{
|
||||
fontSize: '0.8em',
|
||||
color: 'var(--error-color)',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
{t("ckeditor_plugins.premium_required")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Calendar, DateSelectArg, DatesSetArg, EventChangeArg, EventDropArg, EventInput, EventSourceFunc, EventSourceFuncArg, EventSourceInput, LocaleInput, PluginDef } from "@fullcalendar/core";
|
||||
import type { Calendar, DateSelectArg, DatesSetArg, EventChangeArg, EventDropArg, EventInput, EventSourceFunc, EventSourceFuncArg, EventSourceInput, PluginDef } from "@fullcalendar/core";
|
||||
import froca from "../../services/froca.js";
|
||||
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
@@ -15,22 +15,6 @@ import type { EventImpl } from "@fullcalendar/core/internal";
|
||||
import debounce, { type DebouncedFunction } from "debounce";
|
||||
import type { TouchBarItem } from "../../components/touch_bar.js";
|
||||
import type { SegmentedControlSegment } from "electron";
|
||||
import { LOCALE_IDS } from "@triliumnext/commons";
|
||||
|
||||
// Here we hard-code the imports in order to ensure that they are embedded by webpack without having to load all the languages.
|
||||
const LOCALE_MAPPINGS: Record<LOCALE_IDS, (() => Promise<{ default: LocaleInput }>) | null> = {
|
||||
de: () => import("@fullcalendar/core/locales/de"),
|
||||
es: () => import("@fullcalendar/core/locales/es"),
|
||||
fr: () => import("@fullcalendar/core/locales/fr"),
|
||||
cn: () => import("@fullcalendar/core/locales/zh-cn"),
|
||||
tw: () => import("@fullcalendar/core/locales/zh-tw"),
|
||||
ro: () => import("@fullcalendar/core/locales/ro"),
|
||||
ru: () => import("@fullcalendar/core/locales/ru"),
|
||||
ja: () => import("@fullcalendar/core/locales/ja"),
|
||||
"pt_br": () => import("@fullcalendar/core/locales/pt-br"),
|
||||
uk: () => import("@fullcalendar/core/locales/uk"),
|
||||
en: null
|
||||
};
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="calendar-view">
|
||||
@@ -673,11 +657,31 @@ export default class CalendarView extends ViewMode<{}> {
|
||||
|
||||
}
|
||||
|
||||
export async function getFullCalendarLocale(locale: LOCALE_IDS) {
|
||||
const correspondingLocale = LOCALE_MAPPINGS[locale];
|
||||
if (correspondingLocale) {
|
||||
return (await correspondingLocale()).default;
|
||||
} else {
|
||||
return undefined;
|
||||
export async function getFullCalendarLocale(locale: string) {
|
||||
// Here we hard-code the imports in order to ensure that they are embedded by webpack without having to load all the languages.
|
||||
switch (locale) {
|
||||
case "de":
|
||||
return (await import("@fullcalendar/core/locales/de")).default;
|
||||
case "es":
|
||||
return (await import("@fullcalendar/core/locales/es")).default;
|
||||
case "fr":
|
||||
return (await import("@fullcalendar/core/locales/fr")).default;
|
||||
case "cn":
|
||||
return (await import("@fullcalendar/core/locales/zh-cn")).default;
|
||||
case "tw":
|
||||
return (await import("@fullcalendar/core/locales/zh-tw")).default;
|
||||
case "ro":
|
||||
return (await import("@fullcalendar/core/locales/ro")).default;
|
||||
case "ru":
|
||||
return (await import("@fullcalendar/core/locales/ru")).default;
|
||||
case "ja":
|
||||
return (await import("@fullcalendar/core/locales/ja")).default;
|
||||
case "pt_br":
|
||||
return (await import("@fullcalendar/core/locales/pt-br")).default;
|
||||
case "uk":
|
||||
return (await import("@fullcalendar/core/locales/uk")).default;
|
||||
case "en":
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/desktop",
|
||||
"version": "0.98.1",
|
||||
"version": "0.98.0",
|
||||
"description": "Build your personal knowledge base with Trilium Notes",
|
||||
"private": true,
|
||||
"main": "main.cjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/server",
|
||||
"version": "0.98.1",
|
||||
"version": "0.98.0",
|
||||
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -74,7 +74,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.4.2",
|
||||
"i18next": "25.4.1",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "5.0.0",
|
||||
|
||||
@@ -1,360 +1,27 @@
|
||||
<p>Trilium supports configuration via a file named <code>config.ini</code> and
|
||||
environment variables. This document provides a comprehensive reference
|
||||
for all configuration options.</p>
|
||||
<h2>Configuration Precedence</h2>
|
||||
<p>Configuration values are loaded in the following order of precedence (highest
|
||||
to lowest):</p>
|
||||
environment variables. Please review the file named <a href="https://github.com/TriliumNext/Trilium/blob/main/apps/server/src/assets/config-sample.ini">config-sample.ini</a> in
|
||||
the <a href="https://github.com/TriliumNext/Trilium">Trilium</a> repository
|
||||
to see what values are supported.</p>
|
||||
<p>You can provide the same values via environment variables instead of the <code>config.ini</code> file,
|
||||
and these environment variables use the following format:</p>
|
||||
<ol>
|
||||
<li><strong>Environment variables</strong> (checked first)</li>
|
||||
<li><strong>config.ini file values</strong>
|
||||
</li>
|
||||
<li><strong>Default values</strong>
|
||||
<li>Environment variables should be prefixed with <code>TRILIUM_</code> and
|
||||
use underscores to represent the INI section structure.</li>
|
||||
<li>The format is: <code>TRILIUM_<SECTION>_<KEY>=<VALUE></code>
|
||||
</li>
|
||||
<li>The environment variables will override any matching values from config.ini</li>
|
||||
</ol>
|
||||
<h2>Environment Variable Patterns</h2>
|
||||
<p>Trilium supports multiple environment variable patterns for flexibility.
|
||||
The primary pattern is: <code>TRILIUM_[SECTION]_[KEY]</code>
|
||||
</p>
|
||||
<p>Where:</p>
|
||||
<ul>
|
||||
<li><code>SECTION</code> is the INI section name in UPPERCASE</li>
|
||||
<li><code>KEY</code> is the camelCase configuration key converted to UPPERCASE
|
||||
(e.g., <code>instanceName</code> → <code>INSTANCENAME</code>)</li>
|
||||
</ul>
|
||||
<p>Additionally, shorter aliases are available for common configurations
|
||||
(see Alternative Variables section below).</p>
|
||||
<h2>Environment Variable Reference</h2>
|
||||
<h3>General Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_INSTANCENAME</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>Instance name for API identification</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NOAUTHENTICATION</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable authentication (server only)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NOBACKUP</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable automatic backups</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NODESKTOPICON</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable desktop icon creation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_READONLY</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Enable read-only mode</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Network Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_HOST</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"0.0.0.0"</td>
|
||||
<td>Server host binding</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_PORT</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"3000"</td>
|
||||
<td>Server port</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_HTTPS</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Enable HTTPS</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CERTPATH</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>SSL certificate path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_KEYPATH</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>SSL key path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_TRUSTEDREVERSEPROXY</code>
|
||||
</td>
|
||||
<td>boolean/string</td>
|
||||
<td>false</td>
|
||||
<td>Reverse proxy trust settings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWORIGIN</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed origins</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWMETHODS</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed methods</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWHEADERS</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed headers</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Session Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SESSION_COOKIEMAXAGE</code>
|
||||
</td>
|
||||
<td>integer</td>
|
||||
<td>1814400</td>
|
||||
<td>Session cookie max age in seconds (21 days)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Sync Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCSERVERHOST</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>Sync server host URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"120000"</td>
|
||||
<td>Sync server timeout in milliseconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCPROXY</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>Sync proxy URL</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>MultiFactorAuthentication Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth/OpenID base URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth client ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth client secret</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"<a href="https://accounts.google.com">https://accounts.google.com</a>"</td>
|
||||
<td>OAuth issuer base URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>"Google"</td>
|
||||
<td>OAuth issuer display name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth issuer icon URL</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Logging Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_LOGGING_RETENTIONDAYS</code>
|
||||
</td>
|
||||
<td>integer</td>
|
||||
<td>90</td>
|
||||
<td>Number of days to retain log files</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Alternative Environment Variables</h2>
|
||||
<p>The following alternative environment variable names are also supported
|
||||
and work identically to their longer counterparts:</p>
|
||||
<h3>Network CORS Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWORIGIN</code>)</li>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWMETHODS</code>)</li>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWHEADERS</code>)</li>
|
||||
</ul>
|
||||
<h3>Sync Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_SYNC_SERVER_HOST</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERHOST</code>)</li>
|
||||
<li><code>TRILIUM_SYNC_SERVER_TIMEOUT</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>)</li>
|
||||
<li><code>TRILIUM_SYNC_SERVER_PROXY</code> (alternative to <code>TRILIUM_SYNC_SYNCPROXY</code>)</li>
|
||||
</ul>
|
||||
<h3>OAuth/MFA Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_OAUTH_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_CLIENT_ID</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_CLIENT_SECRET</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_NAME</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_ICON</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>)</li>
|
||||
</ul>
|
||||
<h3>Logging Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_LOGGING_RETENTION_DAYS</code> (alternative to <code>TRILIUM_LOGGING_RETENTIONDAYS</code>)</li>
|
||||
</ul>
|
||||
<h2>Boolean Values</h2>
|
||||
<p>Boolean environment variables accept the following values:</p>
|
||||
<ul>
|
||||
<li><strong>True</strong>: <code>"true"</code>, <code>"1"</code>, <code>1</code>
|
||||
<p>For example, if you have this in your config.ini:</p><pre><code class="language-text-x-trilium-auto">[Network]
|
||||
host=localhost
|
||||
port=8080</code></pre>
|
||||
<p>You can override these values using environment variables:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_NETWORK_HOST=0.0.0.0
|
||||
TRILIUM_NETWORK_PORT=9000</code></pre>
|
||||
<p>The code will:</p>
|
||||
<ol>
|
||||
<li>First load the <code>config.ini</code> file as before</li>
|
||||
<li>Then scan all environment variables for ones starting with <code>TRILIUM_</code>
|
||||
</li>
|
||||
<li><strong>False</strong>: <code>"false"</code>, <code>"0"</code>, <code>0</code>
|
||||
</li>
|
||||
<li>Any other value defaults to <code>false</code>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Using Environment Variables</h2>
|
||||
<p>Both naming patterns are fully supported and can be used interchangeably:</p>
|
||||
<ul>
|
||||
<li>The longer format follows the section/key pattern for consistency with
|
||||
the INI file structure</li>
|
||||
<li>The shorter alternatives provide convenience for common configurations</li>
|
||||
<li>You can use whichever format you prefer - both are equally valid</li>
|
||||
</ul>
|
||||
<h2>Examples</h2>
|
||||
<h3>Docker Compose Example</h3><pre><code class="language-text-x-yaml">services:
|
||||
trilium:
|
||||
image: triliumnext/notes
|
||||
environment:
|
||||
# Using full format
|
||||
TRILIUM_GENERAL_INSTANCENAME: "My Trilium Instance"
|
||||
TRILIUM_NETWORK_PORT: "8080"
|
||||
TRILIUM_NETWORK_CORSALLOWORIGIN: "https://myapp.com"
|
||||
TRILIUM_SYNC_SYNCSERVERHOST: "https://sync.example.com"
|
||||
TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL: "https://auth.example.com"
|
||||
|
||||
# Or using shorter alternatives (equally valid)
|
||||
# TRILIUM_NETWORK_CORS_ALLOW_ORIGIN: "https://myapp.com"
|
||||
# TRILIUM_SYNC_SERVER_HOST: "https://sync.example.com"
|
||||
# TRILIUM_OAUTH_BASE_URL: "https://auth.example.com"</code></pre>
|
||||
<h3>Shell Export Example</h3><pre><code class="language-text-x-sh"># Using either format
|
||||
export TRILIUM_GENERAL_NOAUTHENTICATION=false
|
||||
export TRILIUM_NETWORK_HTTPS=true
|
||||
export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
|
||||
export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem
|
||||
export TRILIUM_LOGGING_RETENTIONDAYS=30
|
||||
|
||||
# Start Trilium
|
||||
npm start</code></pre>
|
||||
<h2>config.ini Reference</h2>
|
||||
<p>For the complete list of configuration options and their INI file format,
|
||||
please review the <a href="https://github.com/TriliumNext/Trilium/blob/main/apps/server/src/assets/config-sample.ini">config-sample.ini</a> file
|
||||
in the Trilium repository</p>
|
||||
<li>Parse these variables into section/key pairs</li>
|
||||
<li>Merge them with the config from the file, with environment variables taking
|
||||
precedence</li>
|
||||
</ol>
|
||||
@@ -5,19 +5,18 @@
|
||||
<p>The <em>Quick search</em> function does a full-text search (that is, it
|
||||
searches through the content of notes and not just the title of a note)
|
||||
and displays the result in an easy-to-access manner.</p>
|
||||
<p>The alternative to the quick search is the <a class="reference-link"
|
||||
href="#root/_help_eIg8jdvaoNNd">Search</a> function, which opens in
|
||||
a dedicated tab and has support for advanced queries.</p>
|
||||
<p>For even faster navigation, it's possible to use <a class="reference-link"
|
||||
href="#root/_help_F1r9QtzQLZqm">Jump to...</a> which will only search
|
||||
<p>The alternative to the quick search is the <a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a> function,
|
||||
which opens in a dedicated tab and has support for advanced queries.</p>
|
||||
<p>For even faster navigation, it's possible to use <a class="reference-link"
|
||||
href="#root/_help_F1r9QtzQLZqm">Jump to Note</a> which will only search
|
||||
through the note titles instead of the content.</p>
|
||||
<h2>Layout</h2>
|
||||
<p>Based on the <a class="reference-link" href="#root/_help_x0JgW8UqGXvq">Vertical and horizontal layout</a>,
|
||||
<p>Based on the <a class="reference-link" href="#root/_help_x0JgW8UqGXvq">Vertical and horizontal layout</a>,
|
||||
the quick search is placed:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="eb498e0518c4efc433c9569270c9c7a5c">On the vertical layout, it is displayed right above the <a class="reference-link"
|
||||
<li>On the vertical layout, it is displayed right above the <a class="reference-link"
|
||||
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
|
||||
<li data-list-item-id="e6a9159606a513e839ca71ff4735857bb">On the horizontal layout, it is displayed in the <a class="reference-link"
|
||||
<li>On the horizontal layout, it is displayed in the <a class="reference-link"
|
||||
href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>, where it can be positioned
|
||||
just like any other icon.</li>
|
||||
</ul>
|
||||
@@ -31,120 +30,37 @@
|
||||
<h3>Infinite Scrolling</h3>
|
||||
<p>Results are loaded progressively as you scroll:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e6d151aab6b52d08e9a93e6f9c29c081a">Initial display shows 15 results</li>
|
||||
<li data-list-item-id="e006eeac7574a398324f214edcb9a383b">Scrolling near the bottom automatically loads 10 more results</li>
|
||||
<li
|
||||
data-list-item-id="e5f6fcb1ec0d496fcf599fa90c3911c89">Continue scrolling to load all matching notes</li>
|
||||
<li>Initial display shows 15 results</li>
|
||||
<li>Scrolling near the bottom automatically loads 10 more results</li>
|
||||
<li>Continue scrolling to load all matching notes</li>
|
||||
</ul>
|
||||
<h3>Visual Features</h3>
|
||||
<ul>
|
||||
<li data-list-item-id="e44f3402a55ac37c63abae20490d66d70"><strong>Highlighting</strong>: Search terms appear in bold with accent
|
||||
<li><strong>Highlighting</strong>: Search terms appear in bold with accent
|
||||
colors</li>
|
||||
<li data-list-item-id="e1c8743ac639f15750171788790df2bb0"><strong>Separation</strong>: Results are separated with dividers</li>
|
||||
<li
|
||||
data-list-item-id="ec5c5dbaa44ba426d220718804b9b27db"><strong>Theme Support</strong>: Highlighting colors adapt to light/dark
|
||||
<li><strong>Separation</strong>: Results are separated with dividers</li>
|
||||
<li><strong>Theme Support</strong>: Highlighting colors adapt to light/dark
|
||||
themes</li>
|
||||
</ul>
|
||||
<h3>Search Behavior</h3>
|
||||
<p>Quick search uses progressive search:</p>
|
||||
<ol>
|
||||
<li data-list-item-id="e9a34edaccc0174140e1183c5e43a2327">Shows exact matches first</li>
|
||||
<li data-list-item-id="e5b751c044ae5189095fd08655a55372f">Includes fuzzy matches when exact results are fewer than 5</li>
|
||||
<li data-list-item-id="ee63c39a04b7511cd4e031cdd963f58d2">Exact matches appear before fuzzy matches</li>
|
||||
<li>Shows exact matches first</li>
|
||||
<li>Includes fuzzy matches when exact results are fewer than 5</li>
|
||||
<li>Exact matches appear before fuzzy matches</li>
|
||||
</ol>
|
||||
<h3>Keyboard Navigation</h3>
|
||||
<ul>
|
||||
<li data-list-item-id="e1161754a60afdea3656561abcb46f9ea">Press <code>Enter</code> to open the first result</li>
|
||||
<li data-list-item-id="ebdffa32bcd3d8e24c3938b472521034d">Use arrow keys to navigate through results</li>
|
||||
<li data-list-item-id="eed08e1e6867dcef7eaa6ce7a21fd5e02">Press <code>Escape</code> to close the quick search</li>
|
||||
<li>Press <code>Enter</code> to open the first result</li>
|
||||
<li>Use arrow keys to navigate through results</li>
|
||||
<li>Press <code>Escape</code> to close the quick search</li>
|
||||
</ul>
|
||||
<h2>Using Quick Search</h2>
|
||||
<ol>
|
||||
<li data-list-item-id="e88738101cdad95c7ffe2fc45d19250b7"><strong>Typo tolerance</strong>: Search finds results despite minor typos</li>
|
||||
<li
|
||||
data-list-item-id="ead4c50c8ae5e86987073741285271140"><strong>Content previews</strong>: 200-character snippets show match context</li>
|
||||
<li
|
||||
data-list-item-id="ee135ac66eafef5962b5221fa149dc31c"><strong>Infinite scrolling</strong>: Additional results load on scroll</li>
|
||||
<li
|
||||
data-list-item-id="ebecf1647bb3e6631383fa1cad5e0d222"><strong>Specific terms</strong>: Specific search terms return more focused
|
||||
results</li>
|
||||
<li data-list-item-id="e7d6ee3a67dbf55e7c72788cde795f2b2"><strong>Match locations</strong>: Bold text indicates where matches occur</li>
|
||||
</ol>
|
||||
<h2>Quick Search - Exact Match Operator</h2>
|
||||
<p>Quick Search now supports the exact match operator (<code>=</code>) at
|
||||
the beginning of your search query. This allows you to search for notes
|
||||
where the title or content exactly matches your search term, rather than
|
||||
just containing it.</p>
|
||||
<h3>Usage</h3>
|
||||
<p>To use exact match in Quick Search:</p>
|
||||
<ol>
|
||||
<li data-list-item-id="e98c91a13502a0ddd321432cd2cdab193">Start your search query with the <code>=</code> operator</li>
|
||||
<li data-list-item-id="e0db9fc3f530c8e0eb96f4df5ef74d955">Follow it immediately with your search term (no space after <code>=</code>)</li>
|
||||
</ol>
|
||||
<h4>Examples</h4>
|
||||
<ul>
|
||||
<li data-list-item-id="e188d5c0a39291e2f665072b02b4b6cc0"><code>=example</code> - Finds notes with title exactly "example" or content
|
||||
exactly "example"</li>
|
||||
<li data-list-item-id="e275568bc5123c979fddff51fac370983"><code>=Project Plan</code> - Finds notes with title exactly "Project Plan"
|
||||
or content exactly "Project Plan"</li>
|
||||
<li data-list-item-id="e04c10070d9800148f641efcbee16ab3d"><code>='hello world'</code> - Use quotes for multi-word exact matches</li>
|
||||
</ul>
|
||||
<h4>Comparison with Regular Search</h4>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Query</th>
|
||||
<th>Behavior</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>example</code>
|
||||
</td>
|
||||
<td>Finds all notes containing "example" anywhere in title or content</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>=example</code>
|
||||
</td>
|
||||
<td>Finds only notes where the title equals "example" or content equals "example"
|
||||
exactly</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<h3>Technical Details</h3>
|
||||
<p>When you use the <code>=</code> operator:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="ebd357e2f6afa77ccb3aed347103d47c3">The search performs an exact match on note titles</li>
|
||||
<li data-list-item-id="e64c84d77017e4cd43fe95c0e4f537044">For note content, it looks for exact matches of the entire content</li>
|
||||
<li
|
||||
data-list-item-id="ef4f790816f24b9484fea127837025935">Partial word matches are excluded</li>
|
||||
<li data-list-item-id="e94a53c59dc4f1a8bef101df66538d06a">The search is case-insensitive</li>
|
||||
</ul>
|
||||
<h3>Limitations</h3>
|
||||
<ul>
|
||||
<li data-list-item-id="e4ed2c12de6681eb26d2ec2daa1985956">The <code>=</code> operator must be at the very beginning of the search
|
||||
query</li>
|
||||
<li data-list-item-id="e30845adb77a12106475b88b68b614009">Spaces after <code>=</code> will treat it as a regular search</li>
|
||||
<li data-list-item-id="e89322d60b3f5cb6b2b318cc7247721cf">Multiple <code>=</code> operators (like <code>==example</code>) are treated
|
||||
as regular text search</li>
|
||||
</ul>
|
||||
<h3>Use Cases</h3>
|
||||
<p>This feature is particularly useful when:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="eb23079c90785534a68963977e993d253">You know the exact title of a note</li>
|
||||
<li data-list-item-id="e92f02cb8b28fc02f264ebeb09376af91">You want to find notes with specific, complete content</li>
|
||||
<li data-list-item-id="e37aa1707a8440213fe404d1ed7a2e941">You need to distinguish between notes with similar but not identical titles</li>
|
||||
<li
|
||||
data-list-item-id="e8b04a0a97aa970e6984370ff17160208">You want to avoid false positives from partial matches</li>
|
||||
</ul>
|
||||
<h3>Related Features</h3>
|
||||
<ul>
|
||||
<li data-list-item-id="e3d0656590d49c6e09ae5f39a0a773dff">For more complex exact matching queries, use the full <a href="Search.md">Search</a> functionality</li>
|
||||
<li
|
||||
data-list-item-id="e7d77021ebedb1b1d25e8bfe2726af21e">For fuzzy matching (finding results despite typos), use the <code>~=</code> operator
|
||||
in the full search</li>
|
||||
<li data-list-item-id="eabcf1ff7a9dfa822192ee9afe3268469">For partial matches with wildcards, use operators like <code>*=*</code>, <code>=*</code>,
|
||||
or <code>*=</code> in the full search</li>
|
||||
</ul>
|
||||
<li><strong>Typo tolerance</strong>: Search finds results despite minor typos</li>
|
||||
<li><strong>Content previews</strong>: 200-character snippets show match context</li>
|
||||
<li><strong>Infinite scrolling</strong>: Additional results load on scroll</li>
|
||||
<li><strong>Specific terms</strong>: Specific search terms return more focused
|
||||
results</li>
|
||||
<li><strong>Match locations</strong>: Bold text indicates where matches occur</li>
|
||||
</ol>
|
||||
@@ -134,8 +134,6 @@ docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium-
|
||||
<li><code>TRILIUM_DATA_DIR</code>: Path to the data directory inside the container
|
||||
(default: <code>/home/node/trilium-data</code>)</li>
|
||||
</ul>
|
||||
<p>For a complete list of configuration environment variables (network settings,
|
||||
authentication, sync, etc.), see <a class="reference-link" href="#root/_help_dmi3wz9muS2O">Configuration (config.ini or environment variables)</a>.</p>
|
||||
<h3>Volume Permissions</h3>
|
||||
<p>If you encounter permission issues with the data volume, ensure that:</p>
|
||||
<ol>
|
||||
|
||||
@@ -49,14 +49,7 @@ class="admonition warning">
|
||||
the <code>config.ini</code> file (check <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a> for
|
||||
more information).
|
||||
<ol>
|
||||
<li>You can also setup through environment variables:
|
||||
<ul>
|
||||
<li>Standard: <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>
|
||||
</li>
|
||||
<li>Legacy (still supported): <code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code>, <code>TRILIUM_OAUTH_CLIENT_SECRET</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>You can also setup through environment variables (<code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code> and <code>TRILIUM_OAUTH_CLIENT_SECRET</code>).</li>
|
||||
<li><code>oauthBaseUrl</code> should be the link of your Trilium instance server,
|
||||
for example, <code>https://<your-trilium-domain></code>.</li>
|
||||
</ol>
|
||||
@@ -71,15 +64,9 @@ class="admonition warning">
|
||||
<p>The default OAuth issuer is Google. To use other services such as Authentik
|
||||
or Auth0, you can configure the settings via <code>oauthIssuerBaseUrl</code>, <code>oauthIssuerName</code>,
|
||||
and <code>oauthIssuerIcon</code> in the <code>config.ini</code> file. Alternatively,
|
||||
these values can be set using environment variables:</p>
|
||||
<ul>
|
||||
<li>Standard: <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>
|
||||
</li>
|
||||
<li>Legacy (still supported): <code>TRILIUM_OAUTH_ISSUER_BASE_URL</code>, <code>TRILIUM_OAUTH_ISSUER_NAME</code>, <code>TRILIUM_OAUTH_ISSUER_ICON</code>
|
||||
</li>
|
||||
</ul>
|
||||
<p><code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> are required
|
||||
for displaying correct issuer information at the Login page.</p>
|
||||
these values can be set using environment variables: <code>TRILIUM_OAUTH_ISSUER_BASE_URL</code>, <code>TRILIUM_OAUTH_ISSUER_NAME</code>,
|
||||
and <code>TRILIUM_OAUTH_ISSUER_ICON</code>. <code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> are
|
||||
required for displaying correct issuer information at the Login page.</p>
|
||||
</aside>
|
||||
<h4>Authentik</h4>
|
||||
<p>If you don’t already have a running Authentik instance, please follow
|
||||
|
||||
@@ -26,10 +26,7 @@ https=true
|
||||
certPath=/[username]/.acme.sh/[hostname]/fullchain.cer
|
||||
keyPath=/[username]/.acme.sh/[hostname]/example.com.key</code></pre>
|
||||
<p>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.
|
||||
For example, you can configure TLS using environment variables:</p><pre><code class="language-text-x-sh">export TRILIUM_NETWORK_HTTPS=true
|
||||
export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
|
||||
export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem</code></pre>
|
||||
to provide all <code>config.ini</code> values as environment variables instead.</p>
|
||||
<p>The above example shows how this is set up in an environment where the
|
||||
certificate was generated using Let's Encrypt's ACME utility. Your paths
|
||||
may differ. For Docker installations, ensure these paths are within a volume
|
||||
|
||||
@@ -288,26 +288,7 @@
|
||||
},
|
||||
"hidden_subtree_templates": {
|
||||
"table": "Tabelle",
|
||||
"board_status_done": "Erledigt",
|
||||
"text-snippet": "Text Ausschnitt",
|
||||
"description": "Beschreibung",
|
||||
"list-view": "Listenansicht",
|
||||
"grid-view": "Gitteransicht",
|
||||
"calendar": "Kalender",
|
||||
"geo-map": "Geokarte",
|
||||
"start-date": "Startdatum",
|
||||
"end-date": "Enddatum",
|
||||
"start-time": "Startzeit",
|
||||
"end-time": "Endzeit",
|
||||
"geolocation": "Geolokation",
|
||||
"built-in-templates": "Integrierte Vorlagen",
|
||||
"board": "Tafel",
|
||||
"status": "Status",
|
||||
"board_note_first": "Erste Notiz",
|
||||
"board_note_second": "Zweite Notiz",
|
||||
"board_note_third": "Dritte Notiz",
|
||||
"board_status_todo": "To-Do",
|
||||
"board_status_progress": "In Bearbeitung"
|
||||
"board_status_done": "Erledigt"
|
||||
},
|
||||
"keyboard_action_names": {
|
||||
"copy-notes-to-clipboard": "Notizen in Zwischenablage kopieren",
|
||||
@@ -412,17 +393,5 @@
|
||||
"old_version": "Eine direkte Migration von Ihrer aktuellen Version wird nicht unterstützt. Bitte führen Sie zunächst ein Upgrade auf die neueste Version v0.60.4 durch und erst dann auf diese Version.",
|
||||
"error_message": "Fehler bei der Migration zu Version {{version}}: {{stack}}",
|
||||
"wrong_db_version": "Die Version der Datenbank ({{version}}) ist neuer als die von der Anwendung erwartete Version ({{targetVersion}}), was bedeutet, dass diese mit einer neueren und inkompatiblen Version von Trilium erstellt wurde. Führen Sie ein Upgrade auf die neueste Version von Trilium durch, um dieses Problem zu beheben."
|
||||
},
|
||||
"modals": {
|
||||
"error_title": "Fehler"
|
||||
},
|
||||
"share_theme": {
|
||||
"site-theme": "Webseite Stil",
|
||||
"search_placeholder": "Suche...",
|
||||
"image_alt": "Artikel Bild",
|
||||
"last-updated": "Zuletzt aktualisiert am {{- date}}",
|
||||
"subpages": "Unterseiten:",
|
||||
"on-this-page": "Auf dieser Seite",
|
||||
"expand": "Erweitern"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,9 +111,7 @@
|
||||
"incorrect-password": "Le mot de passe est incorrect. Veuillez réessayer.",
|
||||
"password": "Mot de passe",
|
||||
"remember-me": "Se souvenir de moi",
|
||||
"button": "Connexion",
|
||||
"sign_in_with_sso": "Se connecter avec {{ ssoIssuerName }}",
|
||||
"incorrect-totp": "TOTP incorrect. Veuillez réessayer."
|
||||
"button": "Connexion"
|
||||
},
|
||||
"set_password": {
|
||||
"title": "Définir un mot de passe",
|
||||
@@ -359,22 +357,6 @@
|
||||
"toggle-ribbon-tab-file-properties": "Afficher/masquer les Propriétés du fichier",
|
||||
"toggle-ribbon-tab-image-properties": "Afficher/masquer les Propriétés de l'image",
|
||||
"toggle-ribbon-tab-owned-attributes": "Afficher/masquer les Attributs propres",
|
||||
"toggle-ribbon-tab-inherited-attributes": "Afficher/masquer les Attributs hérités",
|
||||
"toggle-right-pane": "Afficher le panneau de droite",
|
||||
"print-active-note": "Imprimer la note active",
|
||||
"export-active-note-as-pdf": "Exporter la note active en PDF",
|
||||
"open-note-externally": "Ouvrir la note à l'extérieur",
|
||||
"render-active-note": "Faire un rendu de la note active",
|
||||
"run-active-note": "Lancer la note active",
|
||||
"reload-frontend-app": "Recharger l'application Frontend",
|
||||
"open-developer-tools": "Ouvrir les outils développeur",
|
||||
"find-in-text": "Chercher un texte",
|
||||
"toggle-left-pane": "Afficher le panneau de gauche",
|
||||
"toggle-full-screen": "Passer en mode plein écran",
|
||||
"zoom-out": "Dézoomer",
|
||||
"zoom-in": "Zoomer",
|
||||
"reset-zoom-level": "Réinitilaliser le zoom",
|
||||
"copy-without-formatting": "Copier sans mise en forme",
|
||||
"force-save-revision": "Forcer la sauvegarde de la révision"
|
||||
"toggle-ribbon-tab-inherited-attributes": "Afficher/masquer les Attributs hérités"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"keyboard_actions": {
|
||||
"back-in-note-history": "Navigeer naar vorige notitie in geschiedenis",
|
||||
"forward-in-note-history": "Navigeer naar de volgende notitie in de geschiedenis",
|
||||
"open-jump-to-note-dialog": "Open het dialoogvenster 'Ga naar notitie'",
|
||||
"open-command-palette": "Open het opdrachtvenster",
|
||||
"scroll-to-active-note": "Scroll naar actieve notitie in de notitieboom",
|
||||
"quick-search": "Snelle zoekbalk activeren",
|
||||
"search-in-subtree": "Zoek naar notities in de subboom van de actieve notitie",
|
||||
"expand-subtree": "Subboom van huidige notitie uitbreiden"
|
||||
}
|
||||
}
|
||||
388
apps/server/src/routes/api/ckeditor_plugins.ts
Normal file
388
apps/server/src/routes/api/ckeditor_plugins.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* @module CKEditor Plugins API
|
||||
*
|
||||
* This module provides REST endpoints for managing CKEditor plugin configuration
|
||||
* in Trilium Notes. It handles plugin enablement, validation, and user preferences.
|
||||
*/
|
||||
|
||||
import options from "../../services/options.js";
|
||||
import type { Request, Response } from "express";
|
||||
import type {
|
||||
PluginConfiguration,
|
||||
PluginRegistry,
|
||||
PluginValidationResult,
|
||||
UpdatePluginConfigRequest,
|
||||
UpdatePluginConfigResponse,
|
||||
QueryPluginsOptions,
|
||||
QueryPluginsResult,
|
||||
PluginMetadata
|
||||
} from "@triliumnext/commons";
|
||||
import { PLUGIN_REGISTRY, getPluginMetadata, getPluginsByCategory, getConfigurablePlugins } from "@triliumnext/ckeditor5";
|
||||
import log from "../../services/log.js";
|
||||
|
||||
/**
|
||||
* Get the complete plugin registry with metadata
|
||||
*/
|
||||
export function getPluginRegistry(): PluginRegistry {
|
||||
return PLUGIN_REGISTRY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user's plugin configuration
|
||||
*/
|
||||
export function getUserPluginConfig(): PluginConfiguration[] {
|
||||
const enabledPluginsJson = options.getOptionOrNull("ckeditorEnabledPlugins");
|
||||
|
||||
if (!enabledPluginsJson) {
|
||||
// Return default configuration if none exists
|
||||
return getDefaultPluginConfiguration();
|
||||
}
|
||||
|
||||
try {
|
||||
const enabledPlugins = JSON.parse(enabledPluginsJson) as string[];
|
||||
|
||||
// Convert to PluginConfiguration array
|
||||
return Object.keys(PLUGIN_REGISTRY.plugins).map(pluginId => ({
|
||||
id: pluginId,
|
||||
enabled: enabledPlugins.includes(pluginId)
|
||||
}));
|
||||
} catch (error) {
|
||||
log.error(`Failed to parse CKEditor plugin configuration: ${error}`);
|
||||
return getDefaultPluginConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default plugin configuration (all non-premium plugins enabled)
|
||||
*/
|
||||
function getDefaultPluginConfiguration(): PluginConfiguration[] {
|
||||
return Object.values(PLUGIN_REGISTRY.plugins).map(plugin => ({
|
||||
id: plugin.id,
|
||||
enabled: plugin.defaultEnabled && !plugin.requiresPremium
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user's plugin configuration
|
||||
*/
|
||||
export function updateUserPluginConfig(request: UpdatePluginConfigRequest): UpdatePluginConfigResponse {
|
||||
const { plugins, validate = true } = request;
|
||||
|
||||
try {
|
||||
// Validate if requested
|
||||
let validation: PluginValidationResult | undefined;
|
||||
if (validate) {
|
||||
validation = validatePluginConfiguration(plugins);
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
success: false,
|
||||
validation,
|
||||
plugins: [],
|
||||
errors: validation.errors.map(err => err.message)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
const enabledPluginIds = plugins
|
||||
.filter(plugin => plugin.enabled)
|
||||
.map(plugin => plugin.id);
|
||||
|
||||
options.setOption("ckeditorEnabledPlugins", JSON.stringify(enabledPluginIds));
|
||||
|
||||
log.info(`Updated CKEditor plugin configuration: ${enabledPluginIds.length} plugins enabled`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
validation,
|
||||
plugins: plugins,
|
||||
errors: []
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Failed to update CKEditor plugin configuration: ${error}`);
|
||||
return {
|
||||
success: false,
|
||||
plugins: [],
|
||||
errors: [`Failed to update configuration: ${error}`]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query plugins with filtering options
|
||||
*/
|
||||
export function queryPlugins(options: QueryPluginsOptions = {}): QueryPluginsResult {
|
||||
const { category, enabled, coreOnly, includeConfig } = options;
|
||||
|
||||
let plugins = Object.values(PLUGIN_REGISTRY.plugins);
|
||||
|
||||
// Apply filters
|
||||
if (category) {
|
||||
plugins = plugins.filter(plugin => plugin.category === category);
|
||||
}
|
||||
|
||||
if (coreOnly === true) {
|
||||
plugins = plugins.filter(plugin => plugin.isCore);
|
||||
} else if (coreOnly === false) {
|
||||
plugins = plugins.filter(plugin => !plugin.isCore);
|
||||
}
|
||||
|
||||
// Get user configuration if requested or filtering by enabled status
|
||||
let userConfig: PluginConfiguration[] = [];
|
||||
if (includeConfig || enabled !== undefined) {
|
||||
userConfig = getUserPluginConfig();
|
||||
}
|
||||
|
||||
// Filter by enabled status
|
||||
if (enabled !== undefined) {
|
||||
const enabledPluginIds = new Set(
|
||||
userConfig.filter(config => config.enabled).map(config => config.id)
|
||||
);
|
||||
plugins = plugins.filter(plugin =>
|
||||
enabled ? enabledPluginIds.has(plugin.id) : !enabledPluginIds.has(plugin.id)
|
||||
);
|
||||
}
|
||||
|
||||
// Add user configuration if requested
|
||||
const result = plugins.map(plugin => {
|
||||
if (includeConfig) {
|
||||
const config = userConfig.find(config => config.id === plugin.id);
|
||||
return {
|
||||
...plugin,
|
||||
enabled: config?.enabled ?? false,
|
||||
config: config?.config
|
||||
};
|
||||
}
|
||||
return plugin;
|
||||
});
|
||||
|
||||
// Get available categories
|
||||
const categories = [...new Set(Object.values(PLUGIN_REGISTRY.plugins).map(plugin => plugin.category))];
|
||||
|
||||
return {
|
||||
plugins: result,
|
||||
totalCount: Object.keys(PLUGIN_REGISTRY.plugins).length,
|
||||
categories: categories as any[]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate plugin configuration for dependencies and conflicts
|
||||
*/
|
||||
export function validatePluginConfiguration(plugins: PluginConfiguration[]): PluginValidationResult {
|
||||
const errors: any[] = [];
|
||||
const warnings: any[] = [];
|
||||
const enabledPlugins = new Set(plugins.filter(p => p.enabled).map(p => p.id));
|
||||
const resolvedPlugins = new Set<string>();
|
||||
|
||||
// Check each enabled plugin
|
||||
for (const plugin of plugins.filter(p => p.enabled)) {
|
||||
const metadata = getPluginMetadata(plugin.id);
|
||||
|
||||
if (!metadata) {
|
||||
errors.push({
|
||||
type: "missing_dependency",
|
||||
pluginId: plugin.id,
|
||||
message: `Plugin '${plugin.id}' not found in registry`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check premium requirements
|
||||
if (metadata.requiresPremium && !hasPremiumLicense()) {
|
||||
errors.push({
|
||||
type: "premium_required",
|
||||
pluginId: plugin.id,
|
||||
message: `Plugin '${metadata.name}' requires a premium CKEditor license`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check dependencies
|
||||
for (const depId of metadata.dependencies) {
|
||||
if (!enabledPlugins.has(depId)) {
|
||||
const depMetadata = getPluginMetadata(depId);
|
||||
errors.push({
|
||||
type: "missing_dependency",
|
||||
pluginId: plugin.id,
|
||||
message: `Plugin '${metadata.name}' requires '${depMetadata?.name || depId}' to be enabled`,
|
||||
details: { dependency: depId }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check conflicts
|
||||
for (const conflictId of metadata.conflicts) {
|
||||
if (enabledPlugins.has(conflictId)) {
|
||||
const conflictMetadata = getPluginMetadata(conflictId);
|
||||
errors.push({
|
||||
type: "plugin_conflict",
|
||||
pluginId: plugin.id,
|
||||
message: `Plugin '${metadata.name}' conflicts with '${conflictMetadata?.name || conflictId}'`,
|
||||
details: { conflict: conflictId }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolvedPlugins.add(plugin.id);
|
||||
}
|
||||
|
||||
// Add core plugins to resolved list (they're always enabled)
|
||||
for (const plugin of Object.values(PLUGIN_REGISTRY.plugins)) {
|
||||
if (plugin.isCore) {
|
||||
resolvedPlugins.add(plugin.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for circular dependencies (simplified check)
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
|
||||
function hasCircularDependency(pluginId: string): boolean {
|
||||
if (visiting.has(pluginId)) {
|
||||
return true;
|
||||
}
|
||||
if (visited.has(pluginId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
visiting.add(pluginId);
|
||||
const metadata = getPluginMetadata(pluginId);
|
||||
|
||||
if (metadata) {
|
||||
for (const depId of metadata.dependencies) {
|
||||
if (enabledPlugins.has(depId) && hasCircularDependency(depId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visiting.delete(pluginId);
|
||||
visited.add(pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const pluginId of enabledPlugins) {
|
||||
if (hasCircularDependency(pluginId)) {
|
||||
errors.push({
|
||||
type: "circular_dependency",
|
||||
pluginId: pluginId,
|
||||
message: `Circular dependency detected for plugin '${getPluginMetadata(pluginId)?.name || pluginId}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
resolvedPlugins: Array.from(resolvedPlugins)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has premium CKEditor license
|
||||
*/
|
||||
function hasPremiumLicense(): boolean {
|
||||
// This would check the actual license key
|
||||
// For now, assume no premium license
|
||||
return process.env.VITE_CKEDITOR_KEY !== undefined && process.env.VITE_CKEDITOR_KEY !== "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset plugin configuration to defaults
|
||||
*/
|
||||
export function resetPluginConfigToDefaults(): UpdatePluginConfigResponse {
|
||||
const defaultConfig = getDefaultPluginConfiguration();
|
||||
|
||||
return updateUserPluginConfig({
|
||||
plugins: defaultConfig,
|
||||
validate: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin statistics
|
||||
*/
|
||||
export function getPluginStats() {
|
||||
const userConfig = getUserPluginConfig();
|
||||
const enabledCount = userConfig.filter(p => p.enabled).length;
|
||||
const totalCount = Object.keys(PLUGIN_REGISTRY.plugins).length;
|
||||
const coreCount = Object.values(PLUGIN_REGISTRY.plugins).filter(p => p.isCore).length;
|
||||
const premiumCount = Object.values(PLUGIN_REGISTRY.plugins).filter(p => p.requiresPremium).length;
|
||||
|
||||
const categoryCounts = Object.values(PLUGIN_REGISTRY.plugins).reduce((acc, plugin) => {
|
||||
acc[plugin.category] = (acc[plugin.category] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
enabled: enabledCount,
|
||||
total: totalCount,
|
||||
core: coreCount,
|
||||
premium: premiumCount,
|
||||
configurable: totalCount - coreCount,
|
||||
categories: categoryCounts,
|
||||
hasPremiumLicense: hasPremiumLicense()
|
||||
};
|
||||
}
|
||||
|
||||
// Express route handlers
|
||||
function getPluginRegistryHandler(req: Request, res: Response) {
|
||||
res.json(getPluginRegistry());
|
||||
}
|
||||
|
||||
function getUserPluginConfigHandler(req: Request, res: Response) {
|
||||
res.json(getUserPluginConfig());
|
||||
}
|
||||
|
||||
function updateUserPluginConfigHandler(req: Request, res: Response) {
|
||||
const updateRequest: UpdatePluginConfigRequest = req.body;
|
||||
const result = updateUserPluginConfig(updateRequest);
|
||||
|
||||
if (!result.success) {
|
||||
res.status(400).json(result);
|
||||
} else {
|
||||
res.json(result);
|
||||
}
|
||||
}
|
||||
|
||||
function queryPluginsHandler(req: Request, res: Response) {
|
||||
const queryOptions: QueryPluginsOptions = {
|
||||
category: req.query.category as string,
|
||||
enabled: req.query.enabled === 'true' ? true : req.query.enabled === 'false' ? false : undefined,
|
||||
coreOnly: req.query.coreOnly === 'true' ? true : req.query.coreOnly === 'false' ? false : undefined,
|
||||
includeConfig: req.query.includeConfig === 'true'
|
||||
};
|
||||
|
||||
res.json(queryPlugins(queryOptions));
|
||||
}
|
||||
|
||||
function validatePluginConfigurationHandler(req: Request, res: Response) {
|
||||
const plugins: PluginConfiguration[] = req.body.plugins || [];
|
||||
res.json(validatePluginConfiguration(plugins));
|
||||
}
|
||||
|
||||
function resetPluginConfigToDefaultsHandler(req: Request, res: Response) {
|
||||
const result = resetPluginConfigToDefaults();
|
||||
|
||||
if (!result.success) {
|
||||
res.status(400).json(result);
|
||||
} else {
|
||||
res.json(result);
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginStatsHandler(req: Request, res: Response) {
|
||||
res.json(getPluginStats());
|
||||
}
|
||||
|
||||
export default {
|
||||
getPluginRegistry: getPluginRegistryHandler,
|
||||
getUserPluginConfig: getUserPluginConfigHandler,
|
||||
updateUserPluginConfig: updateUserPluginConfigHandler,
|
||||
queryPlugins: queryPluginsHandler,
|
||||
validatePluginConfiguration: validatePluginConfigurationHandler,
|
||||
resetPluginConfigToDefaults: resetPluginConfigToDefaultsHandler,
|
||||
getPluginStats: getPluginStatsHandler
|
||||
};
|
||||
@@ -97,6 +97,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
||||
"showLoginInShareTheme",
|
||||
"splitEditorOrientation",
|
||||
"seenCallToActions",
|
||||
"ckeditorEnabledPlugins",
|
||||
|
||||
// AI/LLM integration options
|
||||
"aiEnabled",
|
||||
|
||||
@@ -59,6 +59,7 @@ import openaiRoute from "./api/openai.js";
|
||||
import anthropicRoute from "./api/anthropic.js";
|
||||
import llmRoute from "./api/llm.js";
|
||||
import systemInfoRoute from "./api/system_info.js";
|
||||
import ckeditorPluginsRoute from "./api/ckeditor_plugins.js";
|
||||
|
||||
import etapiAuthRoutes from "../etapi/auth.js";
|
||||
import etapiAppInfoRoutes from "../etapi/app_info.js";
|
||||
@@ -220,6 +221,15 @@ function register(app: express.Application) {
|
||||
apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions);
|
||||
apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes);
|
||||
apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales);
|
||||
|
||||
// CKEditor plugins management
|
||||
apiRoute(GET, "/api/ckeditor-plugins/registry", ckeditorPluginsRoute.getPluginRegistry);
|
||||
apiRoute(GET, "/api/ckeditor-plugins/config", ckeditorPluginsRoute.getUserPluginConfig);
|
||||
apiRoute(PUT, "/api/ckeditor-plugins/config", ckeditorPluginsRoute.updateUserPluginConfig);
|
||||
apiRoute(GET, "/api/ckeditor-plugins/query", ckeditorPluginsRoute.queryPlugins);
|
||||
apiRoute(PST, "/api/ckeditor-plugins/validate", ckeditorPluginsRoute.validatePluginConfiguration);
|
||||
apiRoute(PST, "/api/ckeditor-plugins/reset", ckeditorPluginsRoute.resetPluginConfigToDefaults);
|
||||
apiRoute(GET, "/api/ckeditor-plugins/stats", ckeditorPluginsRoute.getPluginStats);
|
||||
|
||||
apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword);
|
||||
apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword);
|
||||
|
||||
@@ -159,7 +159,6 @@ function checkCredentials(req: Request, res: Response, next: NextFunction) {
|
||||
|
||||
if (!passwordEncryptionService.verifyPassword(password)) {
|
||||
res.setHeader("Content-Type", "text/plain").status(401).send("Incorrect password");
|
||||
log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import fs from "fs";
|
||||
import ini from "ini";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("fs");
|
||||
vi.mock("./data_dir.js", () => ({
|
||||
default: {
|
||||
CONFIG_INI_PATH: "/test/config.ini"
|
||||
}
|
||||
}));
|
||||
vi.mock("./resource_dir.js", () => ({
|
||||
default: {
|
||||
RESOURCE_DIR: "/test/resources"
|
||||
}
|
||||
}));
|
||||
|
||||
describe("Config Service", () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original environment
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Clear all TRILIUM env vars
|
||||
Object.keys(process.env).forEach(key => {
|
||||
if (key.startsWith("TRILIUM_")) {
|
||||
delete process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Mock fs to return empty config
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockImplementation((path) => {
|
||||
if (String(path).includes("config-sample.ini")) {
|
||||
return "" as any; // Return string for INI parsing
|
||||
}
|
||||
// Return empty INI config as string
|
||||
return `
|
||||
[General]
|
||||
[Network]
|
||||
[Session]
|
||||
[Sync]
|
||||
[MultiFactorAuthentication]
|
||||
[Logging]
|
||||
` as any;
|
||||
});
|
||||
|
||||
// Clear module cache to reload config with new env vars
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Environment Variable Naming", () => {
|
||||
it("should use standard environment variables following TRILIUM_[SECTION]_[KEY] pattern", async () => {
|
||||
// Set standard env vars
|
||||
process.env.TRILIUM_GENERAL_INSTANCENAME = "test-instance";
|
||||
process.env.TRILIUM_NETWORK_CORSALLOWORIGIN = "https://example.com";
|
||||
process.env.TRILIUM_SYNC_SYNCSERVERHOST = "sync.example.com";
|
||||
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "https://auth.example.com";
|
||||
process.env.TRILIUM_LOGGING_RETENTIONDAYS = "30";
|
||||
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.General.instanceName).toBe("test-instance");
|
||||
expect(config.Network.corsAllowOrigin).toBe("https://example.com");
|
||||
expect(config.Sync.syncServerHost).toBe("sync.example.com");
|
||||
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://auth.example.com");
|
||||
expect(config.Logging.retentionDays).toBe(30);
|
||||
});
|
||||
|
||||
it("should maintain backward compatibility with alias environment variables", async () => {
|
||||
// Set alias/legacy env vars
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://legacy.com";
|
||||
process.env.TRILIUM_SYNC_SERVER_HOST = "legacy-sync.com";
|
||||
process.env.TRILIUM_OAUTH_BASE_URL = "https://legacy-auth.com";
|
||||
process.env.TRILIUM_LOGGING_RETENTION_DAYS = "60";
|
||||
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.Network.corsAllowOrigin).toBe("https://legacy.com");
|
||||
expect(config.Sync.syncServerHost).toBe("legacy-sync.com");
|
||||
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://legacy-auth.com");
|
||||
expect(config.Logging.retentionDays).toBe(60);
|
||||
});
|
||||
|
||||
it("should prioritize standard env vars over aliases when both are set", async () => {
|
||||
// Set both standard and alias env vars - standard should win
|
||||
process.env.TRILIUM_NETWORK_CORSALLOWORIGIN = "standard-cors.com";
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "alias-cors.com";
|
||||
|
||||
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "standard-auth.com";
|
||||
process.env.TRILIUM_OAUTH_BASE_URL = "alias-auth.com";
|
||||
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.Network.corsAllowOrigin).toBe("standard-cors.com");
|
||||
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("standard-auth.com");
|
||||
});
|
||||
|
||||
it("should handle all CORS environment variables correctly", async () => {
|
||||
// Test with standard naming
|
||||
process.env.TRILIUM_NETWORK_CORSALLOWORIGIN = "*";
|
||||
process.env.TRILIUM_NETWORK_CORSALLOWMETHODS = "GET,POST,PUT";
|
||||
process.env.TRILIUM_NETWORK_CORSALLOWHEADERS = "Content-Type,Authorization";
|
||||
|
||||
let { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.Network.corsAllowOrigin).toBe("*");
|
||||
expect(config.Network.corsAllowMethods).toBe("GET,POST,PUT");
|
||||
expect(config.Network.corsAllowHeaders).toBe("Content-Type,Authorization");
|
||||
|
||||
// Clear and test with alias naming
|
||||
delete process.env.TRILIUM_NETWORK_CORSALLOWORIGIN;
|
||||
delete process.env.TRILIUM_NETWORK_CORSALLOWMETHODS;
|
||||
delete process.env.TRILIUM_NETWORK_CORSALLOWHEADERS;
|
||||
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://app.com";
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_METHODS = "GET,POST";
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_HEADERS = "X-Custom-Header";
|
||||
|
||||
vi.resetModules();
|
||||
config = (await import("./config.js")).default;
|
||||
|
||||
expect(config.Network.corsAllowOrigin).toBe("https://app.com");
|
||||
expect(config.Network.corsAllowMethods).toBe("GET,POST");
|
||||
expect(config.Network.corsAllowHeaders).toBe("X-Custom-Header");
|
||||
});
|
||||
|
||||
it("should handle all OAuth/MFA environment variables correctly", async () => {
|
||||
// Test with standard naming
|
||||
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "https://oauth.standard.com";
|
||||
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID = "standard-client-id";
|
||||
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET = "standard-secret";
|
||||
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL = "https://issuer.standard.com";
|
||||
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME = "Standard Auth";
|
||||
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON = "standard-icon.png";
|
||||
|
||||
let { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://oauth.standard.com");
|
||||
expect(config.MultiFactorAuthentication.oauthClientId).toBe("standard-client-id");
|
||||
expect(config.MultiFactorAuthentication.oauthClientSecret).toBe("standard-secret");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerBaseUrl).toBe("https://issuer.standard.com");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerName).toBe("Standard Auth");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerIcon).toBe("standard-icon.png");
|
||||
|
||||
// Clear and test with alias naming
|
||||
Object.keys(process.env).forEach(key => {
|
||||
if (key.startsWith("TRILIUM_MULTIFACTORAUTHENTICATION_")) {
|
||||
delete process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
process.env.TRILIUM_OAUTH_BASE_URL = "https://oauth.alias.com";
|
||||
process.env.TRILIUM_OAUTH_CLIENT_ID = "alias-client-id";
|
||||
process.env.TRILIUM_OAUTH_CLIENT_SECRET = "alias-secret";
|
||||
process.env.TRILIUM_OAUTH_ISSUER_BASE_URL = "https://issuer.alias.com";
|
||||
process.env.TRILIUM_OAUTH_ISSUER_NAME = "Alias Auth";
|
||||
process.env.TRILIUM_OAUTH_ISSUER_ICON = "alias-icon.png";
|
||||
|
||||
vi.resetModules();
|
||||
config = (await import("./config.js")).default;
|
||||
|
||||
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://oauth.alias.com");
|
||||
expect(config.MultiFactorAuthentication.oauthClientId).toBe("alias-client-id");
|
||||
expect(config.MultiFactorAuthentication.oauthClientSecret).toBe("alias-secret");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerBaseUrl).toBe("https://issuer.alias.com");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerName).toBe("Alias Auth");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerIcon).toBe("alias-icon.png");
|
||||
});
|
||||
|
||||
it("should handle all Sync environment variables correctly", async () => {
|
||||
// Test with standard naming
|
||||
process.env.TRILIUM_SYNC_SYNCSERVERHOST = "sync-standard.com";
|
||||
process.env.TRILIUM_SYNC_SYNCSERVERTIMEOUT = "60000";
|
||||
process.env.TRILIUM_SYNC_SYNCPROXY = "proxy-standard.com";
|
||||
|
||||
let { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.Sync.syncServerHost).toBe("sync-standard.com");
|
||||
expect(config.Sync.syncServerTimeout).toBe("60000");
|
||||
expect(config.Sync.syncProxy).toBe("proxy-standard.com");
|
||||
|
||||
// Clear and test with alias naming
|
||||
delete process.env.TRILIUM_SYNC_SYNCSERVERHOST;
|
||||
delete process.env.TRILIUM_SYNC_SYNCSERVERTIMEOUT;
|
||||
delete process.env.TRILIUM_SYNC_SYNCPROXY;
|
||||
|
||||
process.env.TRILIUM_SYNC_SERVER_HOST = "sync-alias.com";
|
||||
process.env.TRILIUM_SYNC_SERVER_TIMEOUT = "30000";
|
||||
process.env.TRILIUM_SYNC_SERVER_PROXY = "proxy-alias.com";
|
||||
|
||||
vi.resetModules();
|
||||
config = (await import("./config.js")).default;
|
||||
|
||||
expect(config.Sync.syncServerHost).toBe("sync-alias.com");
|
||||
expect(config.Sync.syncServerTimeout).toBe("30000");
|
||||
expect(config.Sync.syncProxy).toBe("proxy-alias.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("INI Config Integration", () => {
|
||||
it("should fall back to INI config when no env vars are set", async () => {
|
||||
// Mock INI config with values
|
||||
vi.mocked(fs.readFileSync).mockImplementation((path) => {
|
||||
if (String(path).includes("config-sample.ini")) {
|
||||
return "" as any;
|
||||
}
|
||||
return `
|
||||
[General]
|
||||
instanceName=ini-instance
|
||||
|
||||
[Network]
|
||||
corsAllowOrigin=https://ini-cors.com
|
||||
port=9000
|
||||
|
||||
[Sync]
|
||||
syncServerHost=ini-sync.com
|
||||
|
||||
[MultiFactorAuthentication]
|
||||
oauthBaseUrl=https://ini-oauth.com
|
||||
|
||||
[Logging]
|
||||
retentionDays=45
|
||||
` as any;
|
||||
});
|
||||
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.General.instanceName).toBe("ini-instance");
|
||||
expect(config.Network.corsAllowOrigin).toBe("https://ini-cors.com");
|
||||
expect(config.Network.port).toBe("9000");
|
||||
expect(config.Sync.syncServerHost).toBe("ini-sync.com");
|
||||
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://ini-oauth.com");
|
||||
expect(config.Logging.retentionDays).toBe(45);
|
||||
});
|
||||
|
||||
it("should prioritize env vars over INI config", async () => {
|
||||
// Mock INI config with values
|
||||
vi.mocked(fs.readFileSync).mockImplementation((path) => {
|
||||
if (String(path).includes("config-sample.ini")) {
|
||||
return "" as any;
|
||||
}
|
||||
return `
|
||||
[General]
|
||||
instanceName=ini-instance
|
||||
|
||||
[Network]
|
||||
corsAllowOrigin=https://ini-cors.com
|
||||
` as any;
|
||||
});
|
||||
|
||||
// Set env vars that should override INI
|
||||
process.env.TRILIUM_GENERAL_INSTANCENAME = "env-instance";
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://env-cors.com"; // Using alias
|
||||
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.General.instanceName).toBe("env-instance");
|
||||
expect(config.Network.corsAllowOrigin).toBe("https://env-cors.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Type Transformations", () => {
|
||||
it("should correctly transform boolean values", async () => {
|
||||
process.env.TRILIUM_GENERAL_NOAUTHENTICATION = "true";
|
||||
process.env.TRILIUM_GENERAL_NOBACKUP = "1";
|
||||
process.env.TRILIUM_GENERAL_READONLY = "false";
|
||||
process.env.TRILIUM_NETWORK_HTTPS = "0";
|
||||
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.General.noAuthentication).toBe(true);
|
||||
expect(config.General.noBackup).toBe(true);
|
||||
expect(config.General.readOnly).toBe(false);
|
||||
expect(config.Network.https).toBe(false);
|
||||
});
|
||||
|
||||
it("should correctly transform integer values", async () => {
|
||||
process.env.TRILIUM_SESSION_COOKIEMAXAGE = "3600";
|
||||
process.env.TRILIUM_LOGGING_RETENTIONDAYS = "7";
|
||||
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.Session.cookieMaxAge).toBe(3600);
|
||||
expect(config.Logging.retentionDays).toBe(7);
|
||||
});
|
||||
|
||||
it("should use default values for invalid integers", async () => {
|
||||
process.env.TRILIUM_SESSION_COOKIEMAXAGE = "invalid";
|
||||
process.env.TRILIUM_LOGGING_RETENTION_DAYS = "not-a-number"; // Using alias
|
||||
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
expect(config.Session.cookieMaxAge).toBe(21 * 24 * 60 * 60); // Default
|
||||
expect(config.Logging.retentionDays).toBe(90); // Default
|
||||
});
|
||||
});
|
||||
|
||||
describe("Default Values", () => {
|
||||
it("should use correct default values when no config is provided", async () => {
|
||||
const { default: config } = await import("./config.js");
|
||||
|
||||
// General defaults
|
||||
expect(config.General.instanceName).toBe("");
|
||||
expect(config.General.noAuthentication).toBe(false);
|
||||
expect(config.General.noBackup).toBe(false);
|
||||
expect(config.General.noDesktopIcon).toBe(false);
|
||||
expect(config.General.readOnly).toBe(false);
|
||||
|
||||
// Network defaults
|
||||
expect(config.Network.host).toBe("0.0.0.0");
|
||||
expect(config.Network.port).toBe("3000");
|
||||
expect(config.Network.https).toBe(false);
|
||||
expect(config.Network.certPath).toBe("");
|
||||
expect(config.Network.keyPath).toBe("");
|
||||
expect(config.Network.trustedReverseProxy).toBe(false);
|
||||
expect(config.Network.corsAllowOrigin).toBe("");
|
||||
expect(config.Network.corsAllowMethods).toBe("");
|
||||
expect(config.Network.corsAllowHeaders).toBe("");
|
||||
|
||||
// Session defaults
|
||||
expect(config.Session.cookieMaxAge).toBe(21 * 24 * 60 * 60);
|
||||
|
||||
// Sync defaults
|
||||
expect(config.Sync.syncServerHost).toBe("");
|
||||
expect(config.Sync.syncServerTimeout).toBe("120000");
|
||||
expect(config.Sync.syncProxy).toBe("");
|
||||
|
||||
// OAuth defaults
|
||||
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("");
|
||||
expect(config.MultiFactorAuthentication.oauthClientId).toBe("");
|
||||
expect(config.MultiFactorAuthentication.oauthClientSecret).toBe("");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerBaseUrl).toBe("https://accounts.google.com");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerName).toBe("Google");
|
||||
expect(config.MultiFactorAuthentication.oauthIssuerIcon).toBe("");
|
||||
|
||||
// Logging defaults
|
||||
expect(config.Logging.retentionDays).toBe(90);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,3 @@
|
||||
/**
|
||||
* ╔════════════════════════════════════════════════════════════════════════════╗
|
||||
* ║ TRILIUM CONFIGURATION RESOLUTION ORDER ║
|
||||
* ╠════════════════════════════════════════════════════════════════════════════╣
|
||||
* ║ ║
|
||||
* ║ Priority │ Source │ Example ║
|
||||
* ║ ─────────┼─────────────────────────────────┼─────────────────────────────║
|
||||
* ║ 1 │ Environment Variables │ TRILIUM_NETWORK_PORT=8080 ║
|
||||
* ║ ↓ │ (Highest Priority - Overrides all) ║
|
||||
* ║ │ ║
|
||||
* ║ 2 │ config.ini File │ [Network] ║
|
||||
* ║ ↓ │ (User Configuration) │ port=8080 ║
|
||||
* ║ │ ║
|
||||
* ║ 3 │ Default Values │ port='3000' ║
|
||||
* ║ │ (Lowest Priority - Fallback) │ (hardcoded defaults) ║
|
||||
* ║ ║
|
||||
* ╠════════════════════════════════════════════════════════════════════════════╣
|
||||
* ║ IMPORTANT: Environment variables ALWAYS override config.ini values! ║
|
||||
* ╚════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
import ini from "ini";
|
||||
import fs from "fs";
|
||||
import dataDir from "./data_dir.js";
|
||||
@@ -26,594 +5,153 @@ import path from "path";
|
||||
import resourceDir from "./resource_dir.js";
|
||||
import { envToBoolean, stringToInt } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Path to the sample configuration file that serves as a template for new installations.
|
||||
* This file contains all available configuration options with documentation.
|
||||
*/
|
||||
const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini");
|
||||
|
||||
/**
|
||||
* Initialize config.ini file if it doesn't exist.
|
||||
* On first run, copies the sample configuration to the data directory,
|
||||
* allowing users to customize their settings.
|
||||
*/
|
||||
if (!fs.existsSync(dataDir.CONFIG_INI_PATH)) {
|
||||
const configSample = fs.readFileSync(configSampleFilePath).toString("utf8");
|
||||
|
||||
fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type definition for the parsed INI configuration structure.
|
||||
* The ini parser returns an object with string keys and values that can be
|
||||
* strings, booleans, numbers, or nested objects.
|
||||
*/
|
||||
type IniConfigValue = string | number | boolean | null | undefined;
|
||||
type IniConfigSection = Record<string, IniConfigValue>;
|
||||
type IniConfig = Record<string, IniConfigSection | IniConfigValue>;
|
||||
const iniConfig = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
|
||||
|
||||
/**
|
||||
* Parse the config.ini file into a JavaScript object.
|
||||
* This object contains all user-defined configuration from the INI file,
|
||||
* which will be merged with environment variables and defaults.
|
||||
*/
|
||||
const iniConfig = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8")) as IniConfig;
|
||||
|
||||
/**
|
||||
* Complete type-safe configuration interface for Trilium.
|
||||
* This interface defines all configuration options available through
|
||||
* environment variables, config.ini, or defaults.
|
||||
*/
|
||||
export interface TriliumConfig {
|
||||
/** General application settings */
|
||||
General: {
|
||||
/** Custom instance name for identifying this Trilium instance */
|
||||
instanceName: string;
|
||||
/** Whether to disable authentication (useful for local-only instances) */
|
||||
noAuthentication: boolean;
|
||||
/** Whether to disable automatic backups */
|
||||
noBackup: boolean;
|
||||
/** Whether to prevent desktop icon creation (desktop app only) */
|
||||
noDesktopIcon: boolean;
|
||||
/** Whether to run in read-only mode (prevents all data modifications) */
|
||||
readOnly: boolean;
|
||||
};
|
||||
/** Network and server configuration */
|
||||
Network: {
|
||||
/** Host/IP address to bind the server to (e.g., '0.0.0.0' for all interfaces) */
|
||||
host: string;
|
||||
/** Port number for the HTTP/HTTPS server */
|
||||
port: string;
|
||||
/** Whether to enable HTTPS (requires certPath and keyPath) */
|
||||
https: boolean;
|
||||
/** Path to SSL certificate file (required when https=true) */
|
||||
certPath: string;
|
||||
/** Path to SSL private key file (required when https=true) */
|
||||
keyPath: string;
|
||||
/** Trust reverse proxy headers (boolean or specific IP/subnet string) */
|
||||
trustedReverseProxy: boolean | string;
|
||||
/** CORS allowed origins (comma-separated list or '*' for all) */
|
||||
corsAllowOrigin: string;
|
||||
/** CORS allowed methods (comma-separated HTTP methods) */
|
||||
corsAllowMethods: string;
|
||||
/** CORS allowed headers (comma-separated header names) */
|
||||
corsAllowHeaders: string;
|
||||
};
|
||||
/** Session management configuration */
|
||||
Session: {
|
||||
/** Maximum age of session cookies in seconds (default: 21 days) */
|
||||
cookieMaxAge: number;
|
||||
};
|
||||
/** Synchronization settings for multi-instance setups */
|
||||
Sync: {
|
||||
/** URL of the sync server to connect to */
|
||||
syncServerHost: string;
|
||||
/** Timeout for sync operations in milliseconds */
|
||||
syncServerTimeout: string;
|
||||
/** Proxy URL for sync connections (if behind corporate proxy) */
|
||||
syncProxy: string;
|
||||
};
|
||||
/** Multi-factor authentication and OAuth/OpenID configuration */
|
||||
MultiFactorAuthentication: {
|
||||
/** Base URL for OAuth authentication endpoint */
|
||||
oauthBaseUrl: string;
|
||||
/** OAuth client ID from your identity provider */
|
||||
oauthClientId: string;
|
||||
/** OAuth client secret from your identity provider */
|
||||
oauthClientSecret: string;
|
||||
/** Base URL of the OAuth issuer (e.g., 'https://accounts.google.com') */
|
||||
oauthIssuerBaseUrl: string;
|
||||
/** Display name of the OAuth provider (shown in UI) */
|
||||
oauthIssuerName: string;
|
||||
/** URL to the OAuth provider's icon/logo */
|
||||
oauthIssuerIcon: string;
|
||||
};
|
||||
/** Logging configuration */
|
||||
Logging: {
|
||||
/**
|
||||
* The number of days to keep the log files around. When rotating the logs,
|
||||
* log files created by Trilium older than the specified amount of time will be deleted.
|
||||
* The number of days to keep the log files around. When rotating the logs, log files created by Trilium older than the specified amount of time will be deleted.
|
||||
*/
|
||||
retentionDays: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default retention period for log files in days.
|
||||
* After this period, old log files are automatically deleted during rotation.
|
||||
*/
|
||||
export const LOGGING_DEFAULT_RETENTION_DAYS = 90;
|
||||
|
||||
/**
|
||||
* Configuration value source with precedence handling.
|
||||
* This interface defines how each configuration value is resolved from multiple sources.
|
||||
*/
|
||||
interface ConfigValue<T> {
|
||||
/**
|
||||
* Standard environment variable name following TRILIUM_[SECTION]_[KEY] pattern.
|
||||
* This is the primary way to override configuration via environment.
|
||||
*/
|
||||
standardEnvVar?: string;
|
||||
/**
|
||||
* Alternative environment variable names for additional flexibility.
|
||||
* These provide shorter or more intuitive names for common settings.
|
||||
*/
|
||||
aliasEnvVars?: string[];
|
||||
/**
|
||||
* Function to retrieve the value from the parsed INI configuration.
|
||||
* Returns undefined if the value is not set in config.ini.
|
||||
*/
|
||||
iniGetter: () => IniConfigValue | IniConfigSection;
|
||||
/**
|
||||
* Default value used when no environment variable or INI value is found.
|
||||
* This ensures every configuration has a sensible default.
|
||||
*/
|
||||
defaultValue: T;
|
||||
/**
|
||||
* Optional transformer function to convert string values to the correct type.
|
||||
* Common transformers handle boolean and integer conversions.
|
||||
*/
|
||||
transformer?: (value: unknown) => T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core configuration resolution function.
|
||||
*
|
||||
* Resolves configuration values using a clear precedence order:
|
||||
* 1. Standard environment variable (highest priority) - Follows TRILIUM_[SECTION]_[KEY] pattern
|
||||
* 2. Alias environment variables - Alternative names for convenience and compatibility
|
||||
* 3. INI config file value - User-defined settings in config.ini
|
||||
* 4. Default value (lowest priority) - Fallback to ensure valid configuration
|
||||
*
|
||||
* This precedence allows for flexible configuration management:
|
||||
* - Environment variables for container/cloud deployments
|
||||
* - config.ini for traditional installations
|
||||
* - Defaults ensure the application always has valid settings
|
||||
*
|
||||
* @param config - Configuration value definition with sources and transformers
|
||||
* @returns The resolved configuration value with appropriate type
|
||||
*/
|
||||
function getConfigValue<T>(config: ConfigValue<T>): T {
|
||||
// Check standard env var first
|
||||
if (config.standardEnvVar && process.env[config.standardEnvVar] !== undefined) {
|
||||
const value = process.env[config.standardEnvVar];
|
||||
return config.transformer ? config.transformer(value) : value as T;
|
||||
}
|
||||
|
||||
// Check alternative env vars for additional flexibility
|
||||
if (config.aliasEnvVars) {
|
||||
for (const aliasVar of config.aliasEnvVars) {
|
||||
if (process.env[aliasVar] !== undefined) {
|
||||
const value = process.env[aliasVar];
|
||||
return config.transformer ? config.transformer(value) : value as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check INI config
|
||||
const iniValue = config.iniGetter();
|
||||
if (iniValue !== undefined && iniValue !== null && iniValue !== '') {
|
||||
return config.transformer ? config.transformer(iniValue) : iniValue as T;
|
||||
}
|
||||
|
||||
// Return default
|
||||
return config.defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to safely access INI config sections.
|
||||
* The ini parser can return either a section object or a primitive value,
|
||||
* so we need to check the type before accessing nested properties.
|
||||
*
|
||||
* @param sectionName - The name of the INI section to access
|
||||
* @returns The section object or undefined if not found or not an object
|
||||
*/
|
||||
function getIniSection(sectionName: string): IniConfigSection | undefined {
|
||||
const section = iniConfig[sectionName];
|
||||
if (section && typeof section === 'object' && !Array.isArray(section)) {
|
||||
return section as IniConfigSection;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a value to boolean, handling various input formats.
|
||||
*
|
||||
* This function provides flexible boolean parsing to handle different
|
||||
* configuration sources (environment variables, INI files, etc.):
|
||||
* - String "true"/"false" (case-insensitive)
|
||||
* - String "1"/"0"
|
||||
* - Numeric 1/0
|
||||
* - Actual boolean values
|
||||
* - Any other value defaults to false
|
||||
*
|
||||
* @param value - The value to transform (string, number, boolean, etc.)
|
||||
* @returns The boolean value or false as default
|
||||
*/
|
||||
function transformBoolean(value: unknown): boolean {
|
||||
// First try the standard envToBoolean function which handles "true"/"false" strings
|
||||
const result = envToBoolean(String(value));
|
||||
if (result !== undefined) return result;
|
||||
|
||||
// Handle numeric boolean values (both string and number types)
|
||||
if (value === "1" || value === 1) return true;
|
||||
if (value === "0" || value === 0) return false;
|
||||
|
||||
// Default to false for any other value
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete configuration mapping that defines how each setting is resolved.
|
||||
*
|
||||
* This mapping structure:
|
||||
* 1. Mirrors the INI file sections for consistency
|
||||
* 2. Defines multiple sources for each configuration value
|
||||
* 3. Provides type transformers where needed
|
||||
* 4. Maintains compatibility with various environment variable formats
|
||||
*
|
||||
* Environment Variable Patterns:
|
||||
* - Standard: TRILIUM_[SECTION]_[KEY] (e.g., TRILIUM_GENERAL_INSTANCENAME)
|
||||
* - Aliases: Shorter alternatives (e.g., TRILIUM_OAUTH_BASE_URL)
|
||||
*
|
||||
* Both patterns are equally valid and can be used based on preference.
|
||||
* The standard pattern provides consistency, while aliases offer convenience.
|
||||
*/
|
||||
const configMapping = {
|
||||
General: {
|
||||
instanceName: {
|
||||
standardEnvVar: 'TRILIUM_GENERAL_INSTANCENAME',
|
||||
iniGetter: () => getIniSection("General")?.instanceName,
|
||||
defaultValue: ''
|
||||
},
|
||||
noAuthentication: {
|
||||
standardEnvVar: 'TRILIUM_GENERAL_NOAUTHENTICATION',
|
||||
iniGetter: () => getIniSection("General")?.noAuthentication,
|
||||
defaultValue: false,
|
||||
transformer: transformBoolean
|
||||
},
|
||||
noBackup: {
|
||||
standardEnvVar: 'TRILIUM_GENERAL_NOBACKUP',
|
||||
iniGetter: () => getIniSection("General")?.noBackup,
|
||||
defaultValue: false,
|
||||
transformer: transformBoolean
|
||||
},
|
||||
noDesktopIcon: {
|
||||
standardEnvVar: 'TRILIUM_GENERAL_NODESKTOPICON',
|
||||
iniGetter: () => getIniSection("General")?.noDesktopIcon,
|
||||
defaultValue: false,
|
||||
transformer: transformBoolean
|
||||
},
|
||||
readOnly: {
|
||||
standardEnvVar: 'TRILIUM_GENERAL_READONLY',
|
||||
iniGetter: () => getIniSection("General")?.readOnly,
|
||||
defaultValue: false,
|
||||
transformer: transformBoolean
|
||||
}
|
||||
},
|
||||
Network: {
|
||||
host: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_HOST',
|
||||
iniGetter: () => getIniSection("Network")?.host,
|
||||
defaultValue: '0.0.0.0'
|
||||
},
|
||||
port: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_PORT',
|
||||
iniGetter: () => getIniSection("Network")?.port,
|
||||
defaultValue: '3000'
|
||||
},
|
||||
https: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_HTTPS',
|
||||
iniGetter: () => getIniSection("Network")?.https,
|
||||
defaultValue: false,
|
||||
transformer: transformBoolean
|
||||
},
|
||||
certPath: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_CERTPATH',
|
||||
iniGetter: () => getIniSection("Network")?.certPath,
|
||||
defaultValue: ''
|
||||
},
|
||||
keyPath: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_KEYPATH',
|
||||
iniGetter: () => getIniSection("Network")?.keyPath,
|
||||
defaultValue: ''
|
||||
},
|
||||
trustedReverseProxy: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_TRUSTEDREVERSEPROXY',
|
||||
iniGetter: () => getIniSection("Network")?.trustedReverseProxy,
|
||||
defaultValue: false as boolean | string
|
||||
},
|
||||
corsAllowOrigin: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_CORSALLOWORIGIN',
|
||||
// alternative with underscore format
|
||||
aliasEnvVars: ['TRILIUM_NETWORK_CORS_ALLOW_ORIGIN'],
|
||||
iniGetter: () => getIniSection("Network")?.corsAllowOrigin,
|
||||
defaultValue: ''
|
||||
},
|
||||
corsAllowMethods: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_CORSALLOWMETHODS',
|
||||
// alternative with underscore format
|
||||
aliasEnvVars: ['TRILIUM_NETWORK_CORS_ALLOW_METHODS'],
|
||||
iniGetter: () => getIniSection("Network")?.corsAllowMethods,
|
||||
defaultValue: ''
|
||||
},
|
||||
corsAllowHeaders: {
|
||||
standardEnvVar: 'TRILIUM_NETWORK_CORSALLOWHEADERS',
|
||||
// alternative with underscore format
|
||||
aliasEnvVars: ['TRILIUM_NETWORK_CORS_ALLOW_HEADERS'],
|
||||
iniGetter: () => getIniSection("Network")?.corsAllowHeaders,
|
||||
defaultValue: ''
|
||||
}
|
||||
},
|
||||
Session: {
|
||||
cookieMaxAge: {
|
||||
standardEnvVar: 'TRILIUM_SESSION_COOKIEMAXAGE',
|
||||
iniGetter: () => getIniSection("Session")?.cookieMaxAge,
|
||||
defaultValue: 21 * 24 * 60 * 60, // 21 Days in Seconds
|
||||
transformer: (value: unknown) => parseInt(String(value)) || 21 * 24 * 60 * 60
|
||||
}
|
||||
},
|
||||
Sync: {
|
||||
syncServerHost: {
|
||||
standardEnvVar: 'TRILIUM_SYNC_SYNCSERVERHOST',
|
||||
// alternative format
|
||||
aliasEnvVars: ['TRILIUM_SYNC_SERVER_HOST'],
|
||||
iniGetter: () => getIniSection("Sync")?.syncServerHost,
|
||||
defaultValue: ''
|
||||
},
|
||||
syncServerTimeout: {
|
||||
standardEnvVar: 'TRILIUM_SYNC_SYNCSERVERTIMEOUT',
|
||||
// alternative format
|
||||
aliasEnvVars: ['TRILIUM_SYNC_SERVER_TIMEOUT'],
|
||||
iniGetter: () => getIniSection("Sync")?.syncServerTimeout,
|
||||
defaultValue: '120000'
|
||||
},
|
||||
syncProxy: {
|
||||
standardEnvVar: 'TRILIUM_SYNC_SYNCPROXY',
|
||||
// alternative shorter formats
|
||||
aliasEnvVars: ['TRILIUM_SYNC_SERVER_PROXY'],
|
||||
// The INI config uses 'syncServerProxy' key for historical reasons (see config-sample.ini)
|
||||
// We check both 'syncProxy' and 'syncServerProxy' for backward compatibility with old configs
|
||||
iniGetter: () => getIniSection("Sync")?.syncProxy || getIniSection("Sync")?.syncServerProxy,
|
||||
defaultValue: ''
|
||||
}
|
||||
},
|
||||
MultiFactorAuthentication: {
|
||||
oauthBaseUrl: {
|
||||
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL',
|
||||
// alternative shorter format (commonly used)
|
||||
aliasEnvVars: ['TRILIUM_OAUTH_BASE_URL'],
|
||||
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthBaseUrl,
|
||||
defaultValue: ''
|
||||
},
|
||||
oauthClientId: {
|
||||
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID',
|
||||
// alternative format
|
||||
aliasEnvVars: ['TRILIUM_OAUTH_CLIENT_ID'],
|
||||
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthClientId,
|
||||
defaultValue: ''
|
||||
},
|
||||
oauthClientSecret: {
|
||||
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET',
|
||||
// alternative format
|
||||
aliasEnvVars: ['TRILIUM_OAUTH_CLIENT_SECRET'],
|
||||
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthClientSecret,
|
||||
defaultValue: ''
|
||||
},
|
||||
oauthIssuerBaseUrl: {
|
||||
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL',
|
||||
// alternative format
|
||||
aliasEnvVars: ['TRILIUM_OAUTH_ISSUER_BASE_URL'],
|
||||
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthIssuerBaseUrl,
|
||||
defaultValue: 'https://accounts.google.com'
|
||||
},
|
||||
oauthIssuerName: {
|
||||
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME',
|
||||
// alternative format
|
||||
aliasEnvVars: ['TRILIUM_OAUTH_ISSUER_NAME'],
|
||||
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthIssuerName,
|
||||
defaultValue: 'Google'
|
||||
},
|
||||
oauthIssuerIcon: {
|
||||
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON',
|
||||
// alternative format
|
||||
aliasEnvVars: ['TRILIUM_OAUTH_ISSUER_ICON'],
|
||||
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthIssuerIcon,
|
||||
defaultValue: ''
|
||||
}
|
||||
},
|
||||
Logging: {
|
||||
retentionDays: {
|
||||
standardEnvVar: 'TRILIUM_LOGGING_RETENTIONDAYS',
|
||||
// alternative with underscore format
|
||||
aliasEnvVars: ['TRILIUM_LOGGING_RETENTION_DAYS'],
|
||||
iniGetter: () => getIniSection("Logging")?.retentionDays,
|
||||
defaultValue: LOGGING_DEFAULT_RETENTION_DAYS,
|
||||
transformer: (value: unknown) => stringToInt(String(value)) ?? LOGGING_DEFAULT_RETENTION_DAYS
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the final configuration object by resolving all values through the mapping.
|
||||
*
|
||||
* This creates the runtime configuration used throughout the application by:
|
||||
* 1. Iterating through each section and key in the mapping
|
||||
* 2. Calling getConfigValue() to resolve each setting with proper precedence
|
||||
* 3. Applying type transformers where needed (booleans, integers)
|
||||
* 4. Returning a fully typed TriliumConfig object
|
||||
*
|
||||
* The resulting config object is immutable at runtime and represents
|
||||
* the complete application configuration.
|
||||
*/
|
||||
//prettier-ignore
|
||||
const config: TriliumConfig = {
|
||||
|
||||
General: {
|
||||
instanceName: getConfigValue(configMapping.General.instanceName),
|
||||
noAuthentication: getConfigValue(configMapping.General.noAuthentication),
|
||||
noBackup: getConfigValue(configMapping.General.noBackup),
|
||||
noDesktopIcon: getConfigValue(configMapping.General.noDesktopIcon),
|
||||
readOnly: getConfigValue(configMapping.General.readOnly)
|
||||
instanceName:
|
||||
process.env.TRILIUM_GENERAL_INSTANCENAME || iniConfig.General.instanceName || "",
|
||||
|
||||
noAuthentication:
|
||||
envToBoolean(process.env.TRILIUM_GENERAL_NOAUTHENTICATION) || iniConfig.General.noAuthentication || false,
|
||||
|
||||
noBackup:
|
||||
envToBoolean(process.env.TRILIUM_GENERAL_NOBACKUP) || iniConfig.General.noBackup || false,
|
||||
|
||||
noDesktopIcon:
|
||||
envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false,
|
||||
|
||||
readOnly:
|
||||
envToBoolean(process.env.TRILIUM_GENERAL_READONLY) || iniConfig.General.readOnly || false
|
||||
},
|
||||
|
||||
Network: {
|
||||
host: getConfigValue(configMapping.Network.host),
|
||||
port: getConfigValue(configMapping.Network.port),
|
||||
https: getConfigValue(configMapping.Network.https),
|
||||
certPath: getConfigValue(configMapping.Network.certPath),
|
||||
keyPath: getConfigValue(configMapping.Network.keyPath),
|
||||
trustedReverseProxy: getConfigValue(configMapping.Network.trustedReverseProxy),
|
||||
corsAllowOrigin: getConfigValue(configMapping.Network.corsAllowOrigin),
|
||||
corsAllowMethods: getConfigValue(configMapping.Network.corsAllowMethods),
|
||||
corsAllowHeaders: getConfigValue(configMapping.Network.corsAllowHeaders)
|
||||
host:
|
||||
process.env.TRILIUM_NETWORK_HOST || iniConfig.Network.host || "0.0.0.0",
|
||||
|
||||
port:
|
||||
process.env.TRILIUM_NETWORK_PORT || iniConfig.Network.port || "3000",
|
||||
|
||||
https:
|
||||
envToBoolean(process.env.TRILIUM_NETWORK_HTTPS) || iniConfig.Network.https || false,
|
||||
|
||||
certPath:
|
||||
process.env.TRILIUM_NETWORK_CERTPATH || iniConfig.Network.certPath || "",
|
||||
|
||||
keyPath:
|
||||
process.env.TRILIUM_NETWORK_KEYPATH || iniConfig.Network.keyPath || "",
|
||||
|
||||
trustedReverseProxy:
|
||||
process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false,
|
||||
|
||||
corsAllowOrigin:
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN || iniConfig.Network.corsAllowOrigin || "",
|
||||
|
||||
corsAllowMethods:
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_METHODS || iniConfig.Network.corsAllowMethods || "",
|
||||
|
||||
corsAllowHeaders:
|
||||
process.env.TRILIUM_NETWORK_CORS_ALLOW_HEADERS || iniConfig.Network.corsAllowHeaders || ""
|
||||
},
|
||||
|
||||
Session: {
|
||||
cookieMaxAge: getConfigValue(configMapping.Session.cookieMaxAge)
|
||||
cookieMaxAge:
|
||||
parseInt(String(process.env.TRILIUM_SESSION_COOKIEMAXAGE)) || parseInt(iniConfig?.Session?.cookieMaxAge) || 21 * 24 * 60 * 60 // 21 Days in Seconds
|
||||
},
|
||||
|
||||
Sync: {
|
||||
syncServerHost: getConfigValue(configMapping.Sync.syncServerHost),
|
||||
syncServerTimeout: getConfigValue(configMapping.Sync.syncServerTimeout),
|
||||
syncProxy: getConfigValue(configMapping.Sync.syncProxy)
|
||||
syncServerHost:
|
||||
process.env.TRILIUM_SYNC_SERVER_HOST || iniConfig?.Sync?.syncServerHost || "",
|
||||
|
||||
syncServerTimeout:
|
||||
process.env.TRILIUM_SYNC_SERVER_TIMEOUT || iniConfig?.Sync?.syncServerTimeout || "120000",
|
||||
|
||||
syncProxy:
|
||||
// additionally checking in iniConfig for inconsistently named syncProxy for backwards compatibility
|
||||
process.env.TRILIUM_SYNC_SERVER_PROXY || iniConfig?.Sync?.syncProxy || iniConfig?.Sync?.syncServerProxy || ""
|
||||
},
|
||||
|
||||
MultiFactorAuthentication: {
|
||||
oauthBaseUrl: getConfigValue(configMapping.MultiFactorAuthentication.oauthBaseUrl),
|
||||
oauthClientId: getConfigValue(configMapping.MultiFactorAuthentication.oauthClientId),
|
||||
oauthClientSecret: getConfigValue(configMapping.MultiFactorAuthentication.oauthClientSecret),
|
||||
oauthIssuerBaseUrl: getConfigValue(configMapping.MultiFactorAuthentication.oauthIssuerBaseUrl),
|
||||
oauthIssuerName: getConfigValue(configMapping.MultiFactorAuthentication.oauthIssuerName),
|
||||
oauthIssuerIcon: getConfigValue(configMapping.MultiFactorAuthentication.oauthIssuerIcon)
|
||||
oauthBaseUrl:
|
||||
process.env.TRILIUM_OAUTH_BASE_URL || iniConfig?.MultiFactorAuthentication?.oauthBaseUrl || "",
|
||||
|
||||
oauthClientId:
|
||||
process.env.TRILIUM_OAUTH_CLIENT_ID || iniConfig?.MultiFactorAuthentication?.oauthClientId || "",
|
||||
|
||||
oauthClientSecret:
|
||||
process.env.TRILIUM_OAUTH_CLIENT_SECRET || iniConfig?.MultiFactorAuthentication?.oauthClientSecret || "",
|
||||
|
||||
oauthIssuerBaseUrl:
|
||||
process.env.TRILIUM_OAUTH_ISSUER_BASE_URL || iniConfig?.MultiFactorAuthentication?.oauthIssuerBaseUrl || "https://accounts.google.com",
|
||||
|
||||
oauthIssuerName:
|
||||
process.env.TRILIUM_OAUTH_ISSUER_NAME || iniConfig?.MultiFactorAuthentication?.oauthIssuerName || "Google",
|
||||
|
||||
oauthIssuerIcon:
|
||||
process.env.TRILIUM_OAUTH_ISSUER_ICON || iniConfig?.MultiFactorAuthentication?.oauthIssuerIcon || ""
|
||||
},
|
||||
|
||||
Logging: {
|
||||
retentionDays: getConfigValue(configMapping.Logging.retentionDays)
|
||||
retentionDays:
|
||||
stringToInt(process.env.TRILIUM_LOGGING_RETENTION_DAYS) ??
|
||||
stringToInt(iniConfig?.Logging?.retentionDays) ??
|
||||
LOGGING_DEFAULT_RETENTION_DAYS
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* =====================================================================
|
||||
* ENVIRONMENT VARIABLE REFERENCE
|
||||
* =====================================================================
|
||||
*
|
||||
* Trilium supports flexible environment variable configuration with multiple
|
||||
* naming patterns. Both formats below are equally valid and can be used
|
||||
* based on your preference.
|
||||
*
|
||||
* CONFIGURATION PRECEDENCE:
|
||||
* 1. Environment variables (highest priority)
|
||||
* 2. config.ini file values
|
||||
* 3. Default values (lowest priority)
|
||||
*
|
||||
* FULL FORMAT VARIABLES (following TRILIUM_[SECTION]_[KEY] pattern):
|
||||
* ====================================================================
|
||||
*
|
||||
* General Section:
|
||||
* - TRILIUM_GENERAL_INSTANCENAME : Custom instance identifier
|
||||
* - TRILIUM_GENERAL_NOAUTHENTICATION : Disable auth (true/false)
|
||||
* - TRILIUM_GENERAL_NOBACKUP : Disable backups (true/false)
|
||||
* - TRILIUM_GENERAL_NODESKTOPICON : No desktop icon (true/false)
|
||||
* - TRILIUM_GENERAL_READONLY : Read-only mode (true/false)
|
||||
*
|
||||
* Network Section:
|
||||
* - TRILIUM_NETWORK_HOST : Bind address (e.g., "0.0.0.0")
|
||||
* - TRILIUM_NETWORK_PORT : Server port (e.g., "8080")
|
||||
* - TRILIUM_NETWORK_HTTPS : Enable HTTPS (true/false)
|
||||
* - TRILIUM_NETWORK_CERTPATH : SSL certificate file path
|
||||
* - TRILIUM_NETWORK_KEYPATH : SSL private key file path
|
||||
* - TRILIUM_NETWORK_TRUSTEDREVERSEPROXY : Trust proxy headers (true/false/IP)
|
||||
* - TRILIUM_NETWORK_CORSALLOWORIGIN : CORS allowed origins
|
||||
* - TRILIUM_NETWORK_CORSALLOWMETHODS : CORS allowed HTTP methods
|
||||
* - TRILIUM_NETWORK_CORSALLOWHEADERS : CORS allowed headers
|
||||
*
|
||||
* Session Section:
|
||||
* - TRILIUM_SESSION_COOKIEMAXAGE : Cookie lifetime in seconds
|
||||
*
|
||||
* Sync Section:
|
||||
* - TRILIUM_SYNC_SYNCSERVERHOST : Sync server URL
|
||||
* - TRILIUM_SYNC_SYNCSERVERTIMEOUT : Sync timeout in milliseconds
|
||||
* - TRILIUM_SYNC_SYNCPROXY : Proxy URL for sync
|
||||
*
|
||||
* Multi-Factor Authentication Section:
|
||||
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL : OAuth base URL
|
||||
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID : OAuth client ID
|
||||
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET : OAuth client secret
|
||||
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL : OAuth issuer URL
|
||||
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME : OAuth provider name
|
||||
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON : OAuth provider icon
|
||||
*
|
||||
* Logging Section:
|
||||
* - TRILIUM_LOGGING_RETENTIONDAYS : Log retention period in days
|
||||
*
|
||||
* SHORTER ALTERNATIVE VARIABLES (equally valid, for convenience):
|
||||
* ================================================================
|
||||
*
|
||||
* Network CORS (with underscores):
|
||||
* - TRILIUM_NETWORK_CORS_ALLOW_ORIGIN : Same as TRILIUM_NETWORK_CORSALLOWORIGIN
|
||||
* - TRILIUM_NETWORK_CORS_ALLOW_METHODS : Same as TRILIUM_NETWORK_CORSALLOWMETHODS
|
||||
* - TRILIUM_NETWORK_CORS_ALLOW_HEADERS : Same as TRILIUM_NETWORK_CORSALLOWHEADERS
|
||||
*
|
||||
* Sync (with SERVER prefix):
|
||||
* - TRILIUM_SYNC_SERVER_HOST : Same as TRILIUM_SYNC_SYNCSERVERHOST
|
||||
* - TRILIUM_SYNC_SERVER_TIMEOUT : Same as TRILIUM_SYNC_SYNCSERVERTIMEOUT
|
||||
* - TRILIUM_SYNC_SERVER_PROXY : Same as TRILIUM_SYNC_SYNCPROXY
|
||||
*
|
||||
* OAuth (simplified without section name):
|
||||
* - TRILIUM_OAUTH_BASE_URL : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL
|
||||
* - TRILIUM_OAUTH_CLIENT_ID : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID
|
||||
* - TRILIUM_OAUTH_CLIENT_SECRET : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET
|
||||
* - TRILIUM_OAUTH_ISSUER_BASE_URL : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL
|
||||
* - TRILIUM_OAUTH_ISSUER_NAME : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME
|
||||
* - TRILIUM_OAUTH_ISSUER_ICON : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON
|
||||
*
|
||||
* Logging (with underscore):
|
||||
* - TRILIUM_LOGGING_RETENTION_DAYS : Same as TRILIUM_LOGGING_RETENTIONDAYS
|
||||
*
|
||||
* BOOLEAN VALUES:
|
||||
* - Accept: "true", "false", "1", "0", 1, 0
|
||||
* - Default to false for invalid values
|
||||
*
|
||||
* EXAMPLES:
|
||||
* export TRILIUM_NETWORK_PORT="8080" # Using full format
|
||||
* export TRILIUM_OAUTH_CLIENT_ID="my-client-id" # Using shorter alternative
|
||||
* export TRILIUM_GENERAL_NOAUTHENTICATION="true" # Boolean value
|
||||
* export TRILIUM_SYNC_SERVER_HOST="https://sync.example.com" # Using alternative with SERVER
|
||||
*/
|
||||
|
||||
/**
|
||||
* The exported configuration object used throughout the Trilium application.
|
||||
* This object is resolved once at startup and remains immutable during runtime.
|
||||
*
|
||||
* To override any setting:
|
||||
* 1. Set an environment variable (see documentation above)
|
||||
* 2. Edit config.ini in your data directory
|
||||
* 3. Defaults will be used if neither is provided
|
||||
*
|
||||
* @example
|
||||
* // Accessing configuration in other modules:
|
||||
* import config from './services/config.js';
|
||||
*
|
||||
* if (config.General.noAuthentication) {
|
||||
* // Skip authentication checks
|
||||
* }
|
||||
*
|
||||
* const server = https.createServer({
|
||||
* cert: fs.readFileSync(config.Network.certPath),
|
||||
* key: fs.readFileSync(config.Network.keyPath)
|
||||
* });
|
||||
*/
|
||||
export default config;
|
||||
export default config;
|
||||
|
||||
@@ -5,6 +5,7 @@ import log from "./log.js";
|
||||
import dateUtils from "./date_utils.js";
|
||||
import keyboardActions from "./keyboard_actions.js";
|
||||
import { SANITIZER_DEFAULT_ALLOWED_TAGS, type KeyboardShortcutWithRequiredActionName, type OptionMap, type OptionNames } from "@triliumnext/commons";
|
||||
import { getDefaultPluginConfiguration } from "@triliumnext/ckeditor5";
|
||||
|
||||
function initDocumentOptions() {
|
||||
optionService.createOption("documentId", randomSecureToken(16), false);
|
||||
@@ -184,6 +185,9 @@ const defaultOptions: DefaultOption[] = [
|
||||
{ name: "textNoteEditorMultilineToolbar", value: "false", isSynced: true },
|
||||
{ name: "textNoteEmojiCompletionEnabled", value: "true", isSynced: true },
|
||||
{ name: "textNoteCompletionEnabled", value: "true", isSynced: true },
|
||||
|
||||
// CKEditor plugin configuration
|
||||
{ name: "ckeditorEnabledPlugins", value: getDefaultPluginConfiguration, isSynced: true },
|
||||
|
||||
// HTML import configuration
|
||||
{ name: "layoutOrientation", value: "vertical", isSynced: false },
|
||||
|
||||
@@ -59,34 +59,6 @@ describe("Lexer fulltext", () => {
|
||||
it("escaping special characters", () => {
|
||||
expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual(["hello", "#~'"]);
|
||||
});
|
||||
|
||||
it("recognizes leading = operator for exact match", () => {
|
||||
const result1 = lex("=example");
|
||||
expect(result1.fulltextTokens.map((t) => t.token)).toEqual(["example"]);
|
||||
expect(result1.leadingOperator).toBe("=");
|
||||
|
||||
const result2 = lex("=hello world");
|
||||
expect(result2.fulltextTokens.map((t) => t.token)).toEqual(["hello", "world"]);
|
||||
expect(result2.leadingOperator).toBe("=");
|
||||
|
||||
const result3 = lex("='hello world'");
|
||||
expect(result3.fulltextTokens.map((t) => t.token)).toEqual(["hello world"]);
|
||||
expect(result3.leadingOperator).toBe("=");
|
||||
});
|
||||
|
||||
it("doesn't treat = as leading operator in other contexts", () => {
|
||||
const result1 = lex("==example");
|
||||
expect(result1.fulltextTokens.map((t) => t.token)).toEqual(["==example"]);
|
||||
expect(result1.leadingOperator).toBe("");
|
||||
|
||||
const result2 = lex("= example");
|
||||
expect(result2.fulltextTokens.map((t) => t.token)).toEqual(["=", "example"]);
|
||||
expect(result2.leadingOperator).toBe("");
|
||||
|
||||
const result3 = lex("example");
|
||||
expect(result3.fulltextTokens.map((t) => t.token)).toEqual(["example"]);
|
||||
expect(result3.leadingOperator).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Lexer expression", () => {
|
||||
|
||||
@@ -10,18 +10,10 @@ function lex(str: string) {
|
||||
let quotes: boolean | string = false; // otherwise contains used quote - ', " or `
|
||||
let fulltextEnded = false;
|
||||
let currentWord = "";
|
||||
let leadingOperator = "";
|
||||
|
||||
function isSymbolAnOperator(chr: string) {
|
||||
return ["=", "*", ">", "<", "!", "-", "+", "%", ","].includes(chr);
|
||||
}
|
||||
|
||||
// Check if the string starts with an exact match operator
|
||||
// This allows users to use "=searchterm" for exact matching
|
||||
if (str.startsWith("=") && str.length > 1 && str[1] !== "=" && str[1] !== " ") {
|
||||
leadingOperator = "=";
|
||||
str = str.substring(1); // Remove the leading operator from the string
|
||||
}
|
||||
|
||||
function isPreviousSymbolAnOperator() {
|
||||
if (currentWord.length === 0) {
|
||||
@@ -136,8 +128,7 @@ function lex(str: string) {
|
||||
return {
|
||||
fulltextQuery,
|
||||
fulltextTokens,
|
||||
expressionTokens,
|
||||
leadingOperator
|
||||
expressionTokens
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import type SearchContext from "../search_context.js";
|
||||
import type { TokenData, TokenStructure } from "./types.js";
|
||||
import type Expression from "../expressions/expression.js";
|
||||
|
||||
function getFulltext(_tokens: TokenData[], searchContext: SearchContext, leadingOperator?: string) {
|
||||
function getFulltext(_tokens: TokenData[], searchContext: SearchContext) {
|
||||
const tokens: string[] = _tokens.map((t) => removeDiacritic(t.token));
|
||||
|
||||
searchContext.highlightedTokens.push(...tokens);
|
||||
@@ -33,19 +33,8 @@ function getFulltext(_tokens: TokenData[], searchContext: SearchContext, leading
|
||||
return null;
|
||||
}
|
||||
|
||||
// If user specified "=" at the beginning, they want exact match
|
||||
const operator = leadingOperator === "=" ? "=" : "*=*";
|
||||
|
||||
if (!searchContext.fastSearch) {
|
||||
// For exact match with "=", we need different behavior
|
||||
if (leadingOperator === "=" && tokens.length === 1) {
|
||||
// Exact match on title OR exact match on content
|
||||
return new OrExp([
|
||||
new PropertyComparisonExp(searchContext, "title", "=", tokens[0]),
|
||||
new NoteContentFulltextExp("=", { tokens, flatText: false })
|
||||
]);
|
||||
}
|
||||
return new OrExp([new NoteFlatTextExp(tokens), new NoteContentFulltextExp(operator, { tokens, flatText: true })]);
|
||||
return new OrExp([new NoteFlatTextExp(tokens), new NoteContentFulltextExp("*=*", { tokens, flatText: true })]);
|
||||
} else {
|
||||
return new NoteFlatTextExp(tokens);
|
||||
}
|
||||
@@ -439,10 +428,9 @@ export interface ParseOpts {
|
||||
expressionTokens: TokenStructure;
|
||||
searchContext: SearchContext;
|
||||
originalQuery?: string;
|
||||
leadingOperator?: string;
|
||||
}
|
||||
|
||||
function parse({ fulltextTokens, expressionTokens, searchContext, leadingOperator }: ParseOpts) {
|
||||
function parse({ fulltextTokens, expressionTokens, searchContext }: ParseOpts) {
|
||||
let expression: Expression | undefined | null;
|
||||
|
||||
try {
|
||||
@@ -456,7 +444,7 @@ function parse({ fulltextTokens, expressionTokens, searchContext, leadingOperato
|
||||
let exp = AndExp.of([
|
||||
searchContext.includeArchivedNotes ? null : new PropertyComparisonExp(searchContext, "isarchived", "=", "false"),
|
||||
getAncestorExp(searchContext),
|
||||
getFulltext(fulltextTokens, searchContext, leadingOperator),
|
||||
getFulltext(fulltextTokens, searchContext),
|
||||
expression
|
||||
]);
|
||||
|
||||
|
||||
@@ -234,28 +234,6 @@ describe("Search", () => {
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("leading = operator for exact match", () => {
|
||||
rootNote
|
||||
.child(note("Example Note").label("type", "document"))
|
||||
.child(note("Examples of Usage").label("type", "tutorial"))
|
||||
.child(note("Sample").label("type", "example"));
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
// Using leading = for exact title match
|
||||
let searchResults = searchService.findResultsWithQuery("=Example Note", searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Example Note")).toBeTruthy();
|
||||
|
||||
// Without =, it should find all notes containing "example"
|
||||
searchResults = searchService.findResultsWithQuery("example", searchContext);
|
||||
expect(searchResults.length).toEqual(3);
|
||||
|
||||
// = operator should not match partial words
|
||||
searchResults = searchService.findResultsWithQuery("=Example", searchContext);
|
||||
expect(searchResults.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("fuzzy attribute search", () => {
|
||||
rootNote.child(note("Europe")
|
||||
.label("country", "", true)
|
||||
|
||||
@@ -367,7 +367,7 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S
|
||||
}
|
||||
|
||||
function parseQueryToExpression(query: string, searchContext: SearchContext) {
|
||||
const { fulltextQuery, fulltextTokens, expressionTokens, leadingOperator } = lex(query);
|
||||
const { fulltextQuery, fulltextTokens, expressionTokens } = lex(query);
|
||||
searchContext.fulltextQuery = fulltextQuery;
|
||||
|
||||
let structuredExpressionTokens: TokenStructure;
|
||||
@@ -383,8 +383,7 @@ function parseQueryToExpression(query: string, searchContext: SearchContext) {
|
||||
fulltextTokens,
|
||||
expressionTokens: structuredExpressionTokens,
|
||||
searchContext,
|
||||
originalQuery: query,
|
||||
leadingOperator
|
||||
originalQuery: query
|
||||
});
|
||||
|
||||
if (searchContext.debug) {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import lex from "./apps/server/dist/services/search/services/lex.js";
|
||||
import parse from "./apps/server/dist/services/search/services/parse.js";
|
||||
import SearchContext from "./apps/server/dist/services/search/search_context.js";
|
||||
|
||||
// Test the integration of the lexer and parser
|
||||
const testCases = [
|
||||
"=example",
|
||||
"example",
|
||||
"=hello world"
|
||||
];
|
||||
|
||||
for (const query of testCases) {
|
||||
console.log(`\n=== Testing: "${query}" ===`);
|
||||
|
||||
const lexResult = lex(query);
|
||||
console.log("Lex result:");
|
||||
console.log(" Fulltext tokens:", lexResult.fulltextTokens.map(t => t.token));
|
||||
console.log(" Leading operator:", lexResult.leadingOperator || "(none)");
|
||||
|
||||
const searchContext = new SearchContext.default({ fastSearch: false });
|
||||
|
||||
try {
|
||||
const expression = parse.default({
|
||||
fulltextTokens: lexResult.fulltextTokens,
|
||||
expressionTokens: [],
|
||||
searchContext,
|
||||
originalQuery: query,
|
||||
leadingOperator: lexResult.leadingOperator
|
||||
});
|
||||
|
||||
console.log("Parse result: Success");
|
||||
console.log(" Expression type:", expression.constructor.name);
|
||||
} catch (e) {
|
||||
console.log("Parse result: Error -", e.message);
|
||||
}
|
||||
}
|
||||
2
docs/Developer Guide/!!!meta.json
vendored
2
docs/Developer Guide/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.98.0",
|
||||
"appVersion": "0.97.2",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
||||
102
docs/Release Notes/!!!meta.json
vendored
102
docs/Release Notes/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.98.0",
|
||||
"appVersion": "0.97.2",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
@@ -61,32 +61,6 @@
|
||||
"attachments": [],
|
||||
"dirFileName": "Release Notes",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "QOJwjruOUr4k",
|
||||
"notePath": [
|
||||
"hD3V4hiu2VW4",
|
||||
"QOJwjruOUr4k"
|
||||
],
|
||||
"title": "v0.98.1",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "template",
|
||||
"value": "wyurrlcDl416",
|
||||
"isInheritable": false,
|
||||
"position": 60
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "v0.98.1.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "PLUoryywi0BC",
|
||||
@@ -95,7 +69,7 @@
|
||||
"PLUoryywi0BC"
|
||||
],
|
||||
"title": "v0.98.0",
|
||||
"notePosition": 20,
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -121,7 +95,7 @@
|
||||
"lvOuiWsLDv8F"
|
||||
],
|
||||
"title": "v0.97.2",
|
||||
"notePosition": 30,
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -147,7 +121,7 @@
|
||||
"OtFZ6Nd9vM3n"
|
||||
],
|
||||
"title": "v0.97.1",
|
||||
"notePosition": 40,
|
||||
"notePosition": 30,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -173,7 +147,7 @@
|
||||
"SJZ5PwfzHSQ1"
|
||||
],
|
||||
"title": "v0.97.0",
|
||||
"notePosition": 50,
|
||||
"notePosition": 40,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -199,7 +173,7 @@
|
||||
"mYXFde3LuNR7"
|
||||
],
|
||||
"title": "v0.96.0",
|
||||
"notePosition": 60,
|
||||
"notePosition": 50,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -225,7 +199,7 @@
|
||||
"jthwbL0FdaeU"
|
||||
],
|
||||
"title": "v0.95.0",
|
||||
"notePosition": 70,
|
||||
"notePosition": 60,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -251,7 +225,7 @@
|
||||
"7HGYsJbLuhnv"
|
||||
],
|
||||
"title": "v0.94.1",
|
||||
"notePosition": 80,
|
||||
"notePosition": 70,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -277,7 +251,7 @@
|
||||
"Neq53ujRGBqv"
|
||||
],
|
||||
"title": "v0.94.0",
|
||||
"notePosition": 90,
|
||||
"notePosition": 80,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -303,7 +277,7 @@
|
||||
"VN3xnce1vLkX"
|
||||
],
|
||||
"title": "v0.93.0",
|
||||
"notePosition": 100,
|
||||
"notePosition": 90,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -321,7 +295,7 @@
|
||||
"WRaBfQqPr6qo"
|
||||
],
|
||||
"title": "v0.92.7",
|
||||
"notePosition": 110,
|
||||
"notePosition": 100,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -347,7 +321,7 @@
|
||||
"a2rwfKNmUFU1"
|
||||
],
|
||||
"title": "v0.92.6",
|
||||
"notePosition": 120,
|
||||
"notePosition": 110,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -365,7 +339,7 @@
|
||||
"fEJ8qErr0BKL"
|
||||
],
|
||||
"title": "v0.92.5-beta",
|
||||
"notePosition": 130,
|
||||
"notePosition": 120,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -383,7 +357,7 @@
|
||||
"kkkZQQGSXjwy"
|
||||
],
|
||||
"title": "v0.92.4",
|
||||
"notePosition": 140,
|
||||
"notePosition": 130,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -401,7 +375,7 @@
|
||||
"vAroNixiezaH"
|
||||
],
|
||||
"title": "v0.92.3-beta",
|
||||
"notePosition": 150,
|
||||
"notePosition": 140,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -419,7 +393,7 @@
|
||||
"mHEq1wxAKNZd"
|
||||
],
|
||||
"title": "v0.92.2-beta",
|
||||
"notePosition": 160,
|
||||
"notePosition": 150,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -437,7 +411,7 @@
|
||||
"IykjoAmBpc61"
|
||||
],
|
||||
"title": "v0.92.1-beta",
|
||||
"notePosition": 170,
|
||||
"notePosition": 160,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -455,7 +429,7 @@
|
||||
"dq2AJ9vSBX4Y"
|
||||
],
|
||||
"title": "v0.92.0-beta",
|
||||
"notePosition": 180,
|
||||
"notePosition": 170,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -473,7 +447,7 @@
|
||||
"3a8aMe4jz4yM"
|
||||
],
|
||||
"title": "v0.91.6",
|
||||
"notePosition": 190,
|
||||
"notePosition": 180,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -491,7 +465,7 @@
|
||||
"8djQjkiDGESe"
|
||||
],
|
||||
"title": "v0.91.5",
|
||||
"notePosition": 200,
|
||||
"notePosition": 190,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -509,7 +483,7 @@
|
||||
"OylxVoVJqNmr"
|
||||
],
|
||||
"title": "v0.91.4-beta",
|
||||
"notePosition": 210,
|
||||
"notePosition": 200,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -527,7 +501,7 @@
|
||||
"tANGQDvnyhrj"
|
||||
],
|
||||
"title": "v0.91.3-beta",
|
||||
"notePosition": 220,
|
||||
"notePosition": 210,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -545,7 +519,7 @@
|
||||
"hMoBfwSoj1SC"
|
||||
],
|
||||
"title": "v0.91.2-beta",
|
||||
"notePosition": 230,
|
||||
"notePosition": 220,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -563,7 +537,7 @@
|
||||
"a2XMSKROCl9z"
|
||||
],
|
||||
"title": "v0.91.1-beta",
|
||||
"notePosition": 240,
|
||||
"notePosition": 230,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -581,7 +555,7 @@
|
||||
"yqXFvWbLkuMD"
|
||||
],
|
||||
"title": "v0.90.12",
|
||||
"notePosition": 250,
|
||||
"notePosition": 240,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -599,7 +573,7 @@
|
||||
"veS7pg311yJP"
|
||||
],
|
||||
"title": "v0.90.11-beta",
|
||||
"notePosition": 260,
|
||||
"notePosition": 250,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -617,7 +591,7 @@
|
||||
"sq5W9TQxRqMq"
|
||||
],
|
||||
"title": "v0.90.10-beta",
|
||||
"notePosition": 270,
|
||||
"notePosition": 260,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -635,7 +609,7 @@
|
||||
"yFEGVCUM9tPx"
|
||||
],
|
||||
"title": "v0.90.9-beta",
|
||||
"notePosition": 280,
|
||||
"notePosition": 270,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -653,7 +627,7 @@
|
||||
"o4wAGqOQuJtV"
|
||||
],
|
||||
"title": "v0.90.8",
|
||||
"notePosition": 290,
|
||||
"notePosition": 280,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -686,7 +660,7 @@
|
||||
"i4A5g9iOg9I0"
|
||||
],
|
||||
"title": "v0.90.7-beta",
|
||||
"notePosition": 300,
|
||||
"notePosition": 290,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -704,7 +678,7 @@
|
||||
"ThNf2GaKgXUs"
|
||||
],
|
||||
"title": "v0.90.6-beta",
|
||||
"notePosition": 310,
|
||||
"notePosition": 300,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -722,7 +696,7 @@
|
||||
"G4PAi554kQUr"
|
||||
],
|
||||
"title": "v0.90.5-beta",
|
||||
"notePosition": 320,
|
||||
"notePosition": 310,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -749,7 +723,7 @@
|
||||
"zATRobGRCmBn"
|
||||
],
|
||||
"title": "v0.90.4",
|
||||
"notePosition": 330,
|
||||
"notePosition": 320,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -767,7 +741,7 @@
|
||||
"sCDLf8IKn3Iz"
|
||||
],
|
||||
"title": "v0.90.3",
|
||||
"notePosition": 340,
|
||||
"notePosition": 330,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -785,7 +759,7 @@
|
||||
"VqqyBu4AuTjC"
|
||||
],
|
||||
"title": "v0.90.2-beta",
|
||||
"notePosition": 350,
|
||||
"notePosition": 340,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -803,7 +777,7 @@
|
||||
"RX3Nl7wInLsA"
|
||||
],
|
||||
"title": "v0.90.1-beta",
|
||||
"notePosition": 360,
|
||||
"notePosition": 350,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -821,7 +795,7 @@
|
||||
"GyueACukPWjk"
|
||||
],
|
||||
"title": "v0.90.0-beta",
|
||||
"notePosition": 370,
|
||||
"notePosition": 360,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -839,7 +813,7 @@
|
||||
"wyurrlcDl416"
|
||||
],
|
||||
"title": "Release Template",
|
||||
"notePosition": 380,
|
||||
"notePosition": 370,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
|
||||
2
docs/Release Notes/Release Notes/v0.98.0.md
vendored
2
docs/Release Notes/Release Notes/v0.98.0.md
vendored
@@ -44,14 +44,12 @@
|
||||
## 🌍 Internationalization
|
||||
|
||||
* Improvements to multiple languages:
|
||||
|
||||
* Chinese (Traditional)
|
||||
* Spanish
|
||||
* Some work started on new languages:
|
||||
|
||||
Portuguese (Brazil), Japanese, Russian, Serbian, Italian, Greek, Catalan
|
||||
* Added new languages:
|
||||
|
||||
* Russian (translations by @questamor)
|
||||
* Japanese language (translations by [acwr47](https://hosted.weblate.org/user/acwr47/))\[…\]
|
||||
|
||||
|
||||
40
docs/Release Notes/Release Notes/v0.98.1.md
vendored
40
docs/Release Notes/Release Notes/v0.98.1.md
vendored
@@ -1,40 +0,0 @@
|
||||
# v0.98.1
|
||||
> [!IMPORTANT]
|
||||
> If you enjoyed this release, consider showing a token of appreciation by:
|
||||
>
|
||||
> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right).
|
||||
> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran).
|
||||
|
||||
## 🐞 Bugfixes
|
||||
|
||||
* [Keyboard shortcut catches QWERTY keys instead of owner's](https://github.com/TriliumNext/Trilium/issues/6547)
|
||||
* [\`-character doesn't work in shortcuts](https://github.com/TriliumNext/Trilium/issues/6784)
|
||||
* Quick search: attribute search no longer working
|
||||
* Settings not fitting well on mobile.
|
||||
* [Attributes/tags not showing up in search results](https://github.com/TriliumNext/Trilium/pull/6752)
|
||||
* [Note links always follow note title](https://github.com/TriliumNext/Trilium/issues/6776)
|
||||
|
||||
## ✨ Improvements
|
||||
|
||||
* Quick search: format multi-line results better
|
||||
* [Add UI performance-related settings](https://github.com/TriliumNext/Trilium/pull/6747) by @adoriandoran
|
||||
* [Reduce or disable search animation](https://github.com/TriliumNext/Trilium/issues/6698) by @adoriandoran
|
||||
* Fuzzy search should have a "non fuzzy" option by @perfectra1n
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
* [Swagger UI for the internal API](https://github.com/TriliumNext/Trilium/pull/6719) by @perfectra1n
|
||||
* [Improve documentation on environment variables](https://github.com/TriliumNext/Trilium/pull/6727) by @perfectra1n
|
||||
|
||||
## 🌍 Internationalization
|
||||
|
||||
* Thanks to our contributors on Weblate:
|
||||
* Added support for the Ukrainian language.
|
||||
* Increased coverage for most of the languages.
|
||||
|
||||
## 🛠️ Technical updates
|
||||
|
||||
* Mermaid diagrams: patch for CVE-2025-54880
|
||||
* The settings were ported to React. **If you notice any issues with the settings, let us know and we'll promptly fix them.**
|
||||
* [Improve management for settings INI](https://github.com/TriliumNext/Trilium/pull/6726) by @perfectra1n
|
||||
* Log same error message on API 401 as on login error to allow fail2ban blocking by @hulmgulm
|
||||
2
docs/User Guide/!!!meta.json
vendored
2
docs/User Guide/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.98.0",
|
||||
"appVersion": "0.97.2",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
||||
@@ -1,163 +1,30 @@
|
||||
# Configuration (config.ini or environment variables)
|
||||
Trilium supports configuration via a file named `config.ini` and environment variables. This document provides a comprehensive reference for all configuration options.
|
||||
Trilium supports configuration via a file named `config.ini` and environment variables. Please review the file named [config-sample.ini](https://github.com/TriliumNext/Trilium/blob/main/apps/server/src/assets/config-sample.ini) in the [Trilium](https://github.com/TriliumNext/Trilium) repository to see what values are supported.
|
||||
|
||||
## Configuration Precedence
|
||||
You can provide the same values via environment variables instead of the `config.ini` file, and these environment variables use the following format:
|
||||
|
||||
Configuration values are loaded in the following order of precedence (highest to lowest):
|
||||
1. Environment variables should be prefixed with `TRILIUM_` and use underscores to represent the INI section structure.
|
||||
2. The format is: `TRILIUM_<SECTION>_<KEY>=<VALUE>`
|
||||
3. The environment variables will override any matching values from config.ini
|
||||
|
||||
1. **Environment variables** (checked first)
|
||||
2. **config.ini file values**
|
||||
3. **Default values**
|
||||
For example, if you have this in your config.ini:
|
||||
|
||||
## Environment Variable Patterns
|
||||
|
||||
Trilium supports multiple environment variable patterns for flexibility. The primary pattern is: `TRILIUM_[SECTION]_[KEY]`
|
||||
|
||||
Where:
|
||||
|
||||
* `SECTION` is the INI section name in UPPERCASE
|
||||
* `KEY` is the camelCase configuration key converted to UPPERCASE (e.g., `instanceName` → `INSTANCENAME`)
|
||||
|
||||
Additionally, shorter aliases are available for common configurations (see Alternative Variables section below).
|
||||
|
||||
## Environment Variable Reference
|
||||
|
||||
### General Section
|
||||
|
||||
| Environment Variable | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `TRILIUM_GENERAL_INSTANCENAME` | string | "" | Instance name for API identification |
|
||||
| `TRILIUM_GENERAL_NOAUTHENTICATION` | boolean | false | Disable authentication (server only) |
|
||||
| `TRILIUM_GENERAL_NOBACKUP` | boolean | false | Disable automatic backups |
|
||||
| `TRILIUM_GENERAL_NODESKTOPICON` | boolean | false | Disable desktop icon creation |
|
||||
| `TRILIUM_GENERAL_READONLY` | boolean | false | Enable read-only mode |
|
||||
|
||||
### Network Section
|
||||
|
||||
| Environment Variable | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `TRILIUM_NETWORK_HOST` | string | "0.0.0.0" | Server host binding |
|
||||
| `TRILIUM_NETWORK_PORT` | string | "3000" | Server port |
|
||||
| `TRILIUM_NETWORK_HTTPS` | boolean | false | Enable HTTPS |
|
||||
| `TRILIUM_NETWORK_CERTPATH` | string | "" | SSL certificate path |
|
||||
| `TRILIUM_NETWORK_KEYPATH` | string | "" | SSL key path |
|
||||
| `TRILIUM_NETWORK_TRUSTEDREVERSEPROXY` | boolean/string | false | Reverse proxy trust settings |
|
||||
| `TRILIUM_NETWORK_CORSALLOWORIGIN` | string | "" | CORS allowed origins |
|
||||
| `TRILIUM_NETWORK_CORSALLOWMETHODS` | string | "" | CORS allowed methods |
|
||||
| `TRILIUM_NETWORK_CORSALLOWHEADERS` | string | "" | CORS allowed headers |
|
||||
|
||||
### Session Section
|
||||
|
||||
| Environment Variable | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `TRILIUM_SESSION_COOKIEMAXAGE` | integer | 1814400 | Session cookie max age in seconds (21 days) |
|
||||
|
||||
### Sync Section
|
||||
|
||||
| Environment Variable | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `TRILIUM_SYNC_SYNCSERVERHOST` | string | "" | Sync server host URL |
|
||||
| `TRILIUM_SYNC_SYNCSERVERTIMEOUT` | string | "120000" | Sync server timeout in milliseconds |
|
||||
| `TRILIUM_SYNC_SYNCPROXY` | string | "" | Sync proxy URL |
|
||||
|
||||
### MultiFactorAuthentication Section
|
||||
|
||||
| Environment Variable | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL` | string | "" | OAuth/OpenID base URL |
|
||||
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID` | string | "" | OAuth client ID |
|
||||
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET` | string | "" | OAuth client secret |
|
||||
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL` | string | "[https://accounts.google.com](https://accounts.google.com)" | OAuth issuer base URL |
|
||||
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME` | string | "Google" | OAuth issuer display name |
|
||||
| `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON` | string | "" | OAuth issuer icon URL |
|
||||
|
||||
### Logging Section
|
||||
|
||||
| Environment Variable | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `TRILIUM_LOGGING_RETENTIONDAYS` | integer | 90 | Number of days to retain log files |
|
||||
|
||||
## Alternative Environment Variables
|
||||
|
||||
The following alternative environment variable names are also supported and work identically to their longer counterparts:
|
||||
|
||||
### Network CORS Variables
|
||||
|
||||
* `TRILIUM_NETWORK_CORS_ALLOW_ORIGIN` (alternative to `TRILIUM_NETWORK_CORSALLOWORIGIN`)
|
||||
* `TRILIUM_NETWORK_CORS_ALLOW_METHODS` (alternative to `TRILIUM_NETWORK_CORSALLOWMETHODS`)
|
||||
* `TRILIUM_NETWORK_CORS_ALLOW_HEADERS` (alternative to `TRILIUM_NETWORK_CORSALLOWHEADERS`)
|
||||
|
||||
### Sync Variables
|
||||
|
||||
* `TRILIUM_SYNC_SERVER_HOST` (alternative to `TRILIUM_SYNC_SYNCSERVERHOST`)
|
||||
* `TRILIUM_SYNC_SERVER_TIMEOUT` (alternative to `TRILIUM_SYNC_SYNCSERVERTIMEOUT`)
|
||||
* `TRILIUM_SYNC_SERVER_PROXY` (alternative to `TRILIUM_SYNC_SYNCPROXY`)
|
||||
|
||||
### OAuth/MFA Variables
|
||||
|
||||
* `TRILIUM_OAUTH_BASE_URL` (alternative to `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL`)
|
||||
* `TRILIUM_OAUTH_CLIENT_ID` (alternative to `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID`)
|
||||
* `TRILIUM_OAUTH_CLIENT_SECRET` (alternative to `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET`)
|
||||
* `TRILIUM_OAUTH_ISSUER_BASE_URL` (alternative to `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL`)
|
||||
* `TRILIUM_OAUTH_ISSUER_NAME` (alternative to `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME`)
|
||||
* `TRILIUM_OAUTH_ISSUER_ICON` (alternative to `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON`)
|
||||
|
||||
### Logging Variables
|
||||
|
||||
* `TRILIUM_LOGGING_RETENTION_DAYS` (alternative to `TRILIUM_LOGGING_RETENTIONDAYS`)
|
||||
|
||||
## Boolean Values
|
||||
|
||||
Boolean environment variables accept the following values:
|
||||
|
||||
* **True**: `"true"`, `"1"`, `1`
|
||||
* **False**: `"false"`, `"0"`, `0`
|
||||
* Any other value defaults to `false`
|
||||
|
||||
## Using Environment Variables
|
||||
|
||||
Both naming patterns are fully supported and can be used interchangeably:
|
||||
|
||||
* The longer format follows the section/key pattern for consistency with the INI file structure
|
||||
* The shorter alternatives provide convenience for common configurations
|
||||
* You can use whichever format you prefer - both are equally valid
|
||||
|
||||
## Examples
|
||||
|
||||
### Docker Compose Example
|
||||
|
||||
```yaml
|
||||
services:
|
||||
trilium:
|
||||
image: triliumnext/notes
|
||||
environment:
|
||||
# Using full format
|
||||
TRILIUM_GENERAL_INSTANCENAME: "My Trilium Instance"
|
||||
TRILIUM_NETWORK_PORT: "8080"
|
||||
TRILIUM_NETWORK_CORSALLOWORIGIN: "https://myapp.com"
|
||||
TRILIUM_SYNC_SYNCSERVERHOST: "https://sync.example.com"
|
||||
TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL: "https://auth.example.com"
|
||||
|
||||
# Or using shorter alternatives (equally valid)
|
||||
# TRILIUM_NETWORK_CORS_ALLOW_ORIGIN: "https://myapp.com"
|
||||
# TRILIUM_SYNC_SERVER_HOST: "https://sync.example.com"
|
||||
# TRILIUM_OAUTH_BASE_URL: "https://auth.example.com"
|
||||
```
|
||||
[Network]
|
||||
host=localhost
|
||||
port=8080
|
||||
```
|
||||
|
||||
### Shell Export Example
|
||||
You can override these values using environment variables:
|
||||
|
||||
```sh
|
||||
# Using either format
|
||||
export TRILIUM_GENERAL_NOAUTHENTICATION=false
|
||||
export TRILIUM_NETWORK_HTTPS=true
|
||||
export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
|
||||
export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem
|
||||
export TRILIUM_LOGGING_RETENTIONDAYS=30
|
||||
|
||||
# Start Trilium
|
||||
npm start
|
||||
```
|
||||
TRILIUM_NETWORK_HOST=0.0.0.0
|
||||
TRILIUM_NETWORK_PORT=9000
|
||||
```
|
||||
|
||||
## config.ini Reference
|
||||
The code will:
|
||||
|
||||
For the complete list of configuration options and their INI file format, please review the [config-sample.ini](https://github.com/TriliumNext/Trilium/blob/main/apps/server/src/assets/config-sample.ini) file in the Trilium repository
|
||||
1. First load the `config.ini` file as before
|
||||
2. Then scan all environment variables for ones starting with `TRILIUM_`
|
||||
3. Parse these variables into section/key pairs
|
||||
4. Merge them with the config from the file, with environment variables taking precedence
|
||||
@@ -3,16 +3,16 @@
|
||||
|
||||
The _Quick search_ function does a full-text search (that is, it searches through the content of notes and not just the title of a note) and displays the result in an easy-to-access manner.
|
||||
|
||||
The alternative to the quick search is the <a class="reference-link" href="Search.md">Search</a> function, which opens in a dedicated tab and has support for advanced queries.
|
||||
The alternative to the quick search is the <a class="reference-link" href="Search.md">Search</a> function, which opens in a dedicated tab and has support for advanced queries.
|
||||
|
||||
For even faster navigation, it's possible to use <a class="reference-link" href="Jump%20to.md">Jump to...</a> which will only search through the note titles instead of the content.
|
||||
For even faster navigation, it's possible to use <a class="reference-link" href="Jump%20to.md">Jump to Note</a> which will only search through the note titles instead of the content.
|
||||
|
||||
## Layout
|
||||
|
||||
Based on the <a class="reference-link" href="../UI%20Elements/Vertical%20and%20horizontal%20layout.md">Vertical and horizontal layout</a>, the quick search is placed:
|
||||
Based on the <a class="reference-link" href="../UI%20Elements/Vertical%20and%20horizontal%20layout.md">Vertical and horizontal layout</a>, the quick search is placed:
|
||||
|
||||
* On the vertical layout, it is displayed right above the <a class="reference-link" href="../UI%20Elements/Note%20Tree.md">Note Tree</a>.
|
||||
* On the horizontal layout, it is displayed in the <a class="reference-link" href="../UI%20Elements/Launch%20Bar.md">Launch Bar</a>, where it can be positioned just like any other icon.
|
||||
* On the vertical layout, it is displayed right above the <a class="reference-link" href="../UI%20Elements/Note%20Tree.md">Note Tree</a>.
|
||||
* On the horizontal layout, it is displayed in the <a class="reference-link" href="../UI%20Elements/Launch%20Bar.md">Launch Bar</a>, where it can be positioned just like any other icon.
|
||||
|
||||
## Search Features
|
||||
|
||||
@@ -56,58 +56,4 @@ Quick search uses progressive search:
|
||||
2. **Content previews**: 200-character snippets show match context
|
||||
3. **Infinite scrolling**: Additional results load on scroll
|
||||
4. **Specific terms**: Specific search terms return more focused results
|
||||
5. **Match locations**: Bold text indicates where matches occur
|
||||
|
||||
## Quick Search - Exact Match Operator
|
||||
|
||||
Quick Search now supports the exact match operator (`=`) at the beginning of your search query. This allows you to search for notes where the title or content exactly matches your search term, rather than just containing it.
|
||||
|
||||
### Usage
|
||||
|
||||
To use exact match in Quick Search:
|
||||
|
||||
1. Start your search query with the `=` operator
|
||||
2. Follow it immediately with your search term (no space after `=`)
|
||||
|
||||
#### Examples
|
||||
|
||||
* `=example` - Finds notes with title exactly "example" or content exactly "example"
|
||||
* `=Project Plan` - Finds notes with title exactly "Project Plan" or content exactly "Project Plan"
|
||||
* `='hello world'` - Use quotes for multi-word exact matches
|
||||
|
||||
#### Comparison with Regular Search
|
||||
|
||||
| Query | Behavior |
|
||||
| --- | --- |
|
||||
| `example` | Finds all notes containing "example" anywhere in title or content |
|
||||
| `=example` | Finds only notes where the title equals "example" or content equals "example" exactly |
|
||||
|
||||
### Technical Details
|
||||
|
||||
When you use the `=` operator:
|
||||
|
||||
* The search performs an exact match on note titles
|
||||
* For note content, it looks for exact matches of the entire content
|
||||
* Partial word matches are excluded
|
||||
* The search is case-insensitive
|
||||
|
||||
### Limitations
|
||||
|
||||
* The `=` operator must be at the very beginning of the search query
|
||||
* Spaces after `=` will treat it as a regular search
|
||||
* Multiple `=` operators (like `==example`) are treated as regular text search
|
||||
|
||||
### Use Cases
|
||||
|
||||
This feature is particularly useful when:
|
||||
|
||||
* You know the exact title of a note
|
||||
* You want to find notes with specific, complete content
|
||||
* You need to distinguish between notes with similar but not identical titles
|
||||
* You want to avoid false positives from partial matches
|
||||
|
||||
### Related Features
|
||||
|
||||
* For more complex exact matching queries, use the full [Search](Search.md) functionality
|
||||
* For fuzzy matching (finding results despite typos), use the `~=` operator in the full search
|
||||
* For partial matches with wildcards, use operators like `*=*`, `=*`, or `*=` in the full search
|
||||
5. **Match locations**: Bold text indicates where matches occur
|
||||
@@ -187,8 +187,6 @@ docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium-
|
||||
* `TRILIUM_GID`: GID to use for the container process (passed to Docker's `--user` flag)
|
||||
* `TRILIUM_DATA_DIR`: Path to the data directory inside the container (default: `/home/node/trilium-data`)
|
||||
|
||||
For a complete list of configuration environment variables (network settings, authentication, sync, etc.), see <a class="reference-link" href="#root/dmi3wz9muS2O">Configuration (config.ini or environment variables)</a>.
|
||||
|
||||
### Volume Permissions
|
||||
|
||||
If you encounter permission issues with the data volume, ensure that:
|
||||
|
||||
@@ -37,9 +37,7 @@ MFA can only be set up on a server instance.
|
||||
In order to setup OpenID, you will need to setup a authentication provider. This requires a bit of extra setup. Follow [these instructions](https://developers.google.com/identity/openid-connect/openid-connect) to setup an OpenID service through google. The Redirect URL of Trilium is `https://<your-trilium-domain>/callback`.
|
||||
|
||||
1. Set the `oauthBaseUrl`, `oauthClientId` and `oauthClientSecret` in the `config.ini` file (check <a class="reference-link" href="../../Advanced%20Usage/Configuration%20(config.ini%20or%20e.md">Configuration (config.ini or environment variables)</a> for more information).
|
||||
1. You can also setup through environment variables:
|
||||
* Standard: `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL`, `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID`, `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET`
|
||||
* Legacy (still supported): `TRILIUM_OAUTH_BASE_URL`, `TRILIUM_OAUTH_CLIENT_ID`, `TRILIUM_OAUTH_CLIENT_SECRET`
|
||||
1. You can also setup through environment variables (`TRILIUM_OAUTH_BASE_URL`, `TRILIUM_OAUTH_CLIENT_ID` and `TRILIUM_OAUTH_CLIENT_SECRET`).
|
||||
2. `oauthBaseUrl` should be the link of your Trilium instance server, for example, `https://<your-trilium-domain>`.
|
||||
2. Restart the server
|
||||
3. Go to "Menu" -> "Options" -> "MFA"
|
||||
@@ -48,12 +46,7 @@ In order to setup OpenID, you will need to setup a authentication provider. This
|
||||
6. Refresh the page and login through OpenID provider
|
||||
|
||||
> [!NOTE]
|
||||
> The default OAuth issuer is Google. To use other services such as Authentik or Auth0, you can configure the settings via `oauthIssuerBaseUrl`, `oauthIssuerName`, and `oauthIssuerIcon` in the `config.ini` file. Alternatively, these values can be set using environment variables:
|
||||
>
|
||||
> * Standard: `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL`, `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME`, `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON`
|
||||
> * Legacy (still supported): `TRILIUM_OAUTH_ISSUER_BASE_URL`, `TRILIUM_OAUTH_ISSUER_NAME`, `TRILIUM_OAUTH_ISSUER_ICON`
|
||||
>
|
||||
> `oauthIssuerName` and `oauthIssuerIcon` are required for displaying correct issuer information at the Login page.
|
||||
> The default OAuth issuer is Google. To use other services such as Authentik or Auth0, you can configure the settings via `oauthIssuerBaseUrl`, `oauthIssuerName`, and `oauthIssuerIcon` in the `config.ini` file. Alternatively, these values can be set using environment variables: `TRILIUM_OAUTH_ISSUER_BASE_URL`, `TRILIUM_OAUTH_ISSUER_NAME`, and `TRILIUM_OAUTH_ISSUER_ICON`. `oauthIssuerName` and `oauthIssuerIcon` are required for displaying correct issuer information at the Login page.
|
||||
|
||||
#### Authentik
|
||||
|
||||
|
||||
@@ -25,13 +25,7 @@ certPath=/[username]/.acme.sh/[hostname]/fullchain.cer
|
||||
keyPath=/[username]/.acme.sh/[hostname]/example.com.key
|
||||
```
|
||||
|
||||
You can also review the [configuration](../../Advanced%20Usage/Configuration%20\(config.ini%20or%20e.md) file to provide all `config.ini` values as environment variables instead. For example, you can configure TLS using environment variables:
|
||||
|
||||
```sh
|
||||
export TRILIUM_NETWORK_HTTPS=true
|
||||
export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
|
||||
export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem
|
||||
```
|
||||
You can also review the [configuration](../../Advanced%20Usage/Configuration%20\(config.ini%20or%20e.md) file to provide all `config.ini` values as environment variables instead.
|
||||
|
||||
The above example shows how this is set up in an environment where the certificate was generated using Let's Encrypt's ACME utility. Your paths may differ. For Docker installations, ensure these paths are within a volume or another directory accessible by the Docker container, such as `/home/node/trilium-data/[DIR IN DATA DIRECTORY]`.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/source",
|
||||
"version": "0.98.1",
|
||||
"version": "0.98.0",
|
||||
"description": "Build your personal knowledge base with Trilium Notes",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
@@ -58,7 +58,7 @@
|
||||
"react-refresh": "^0.17.0",
|
||||
"rollup-plugin-webpack-stats": "2.1.4",
|
||||
"tslib": "^2.3.0",
|
||||
"tsx": "4.20.5",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "~5.9.0",
|
||||
"typescript-eslint": "^8.19.0",
|
||||
"upath": "2.0.1",
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
"ckeditor5-premium-features": "46.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jquery": "3.5.33"
|
||||
"@types/jquery": "3.5.32"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins.js
|
||||
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";
|
||||
import "./translation_overrides.js";
|
||||
export { EditorWatchdog } from "ckeditor5";
|
||||
export { PREMIUM_PLUGINS } from "./plugins.js";
|
||||
export { PREMIUM_PLUGINS, PLUGIN_REGISTRY, getPluginMetadata, getPluginsByCategory, getConfigurablePlugins, getDefaultPluginConfiguration } from "./plugins.js";
|
||||
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, WatchdogConfig } from "ckeditor5";
|
||||
export type { TemplateDefinition } from "ckeditor5-premium-features";
|
||||
export { default as buildExtraCommands } from "./extra_slash_commands.js";
|
||||
|
||||
@@ -30,6 +30,8 @@ import CodeBlockLanguageDropdown from "./plugins/code_block_language_dropdown.js
|
||||
import MoveBlockUpDownPlugin from "./plugins/move_block_updown.js";
|
||||
import ScrollOnUndoRedoPlugin from "./plugins/scroll_on_undo_redo.js"
|
||||
|
||||
import type { PluginMetadata, PluginRegistry } from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* Plugins that are specific to Trilium and not part of the CKEditor 5 core, included in both text editors but not in the attribute editor.
|
||||
*/
|
||||
@@ -159,3 +161,613 @@ export const POPUP_EDITOR_PLUGINS: typeof Plugin[] = [
|
||||
...COMMON_PLUGINS,
|
||||
BlockToolbar,
|
||||
];
|
||||
|
||||
/**
|
||||
* Plugin metadata registry for CKEditor plugins in Trilium.
|
||||
* This defines the configurable plugins with their metadata, dependencies, and categorization.
|
||||
*/
|
||||
export const PLUGIN_REGISTRY: PluginRegistry = {
|
||||
plugins: {
|
||||
// Core plugins (cannot be disabled)
|
||||
"clipboard": {
|
||||
id: "clipboard",
|
||||
name: "Clipboard",
|
||||
description: "Basic clipboard operations (copy, paste, cut)",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: true,
|
||||
commands: ["copy", "paste", "cut"]
|
||||
},
|
||||
"enter": {
|
||||
id: "enter",
|
||||
name: "Enter Key",
|
||||
description: "Enter key handling for line breaks and paragraphs",
|
||||
category: "structure",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: true,
|
||||
},
|
||||
"typing": {
|
||||
id: "typing",
|
||||
name: "Typing",
|
||||
description: "Basic text input and keyboard handling",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: true,
|
||||
},
|
||||
"undo": {
|
||||
id: "undo",
|
||||
name: "Undo/Redo",
|
||||
description: "Undo and redo functionality",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: true,
|
||||
commands: ["undo", "redo"],
|
||||
toolbarItems: ["undo", "redo"]
|
||||
},
|
||||
"paragraph": {
|
||||
id: "paragraph",
|
||||
name: "Paragraph",
|
||||
description: "Basic paragraph formatting",
|
||||
category: "structure",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: true,
|
||||
commands: ["paragraph"]
|
||||
},
|
||||
|
||||
// Formatting plugins
|
||||
"bold": {
|
||||
id: "bold",
|
||||
name: "Bold",
|
||||
description: "Bold text formatting",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["bold"],
|
||||
toolbarItems: ["bold"]
|
||||
},
|
||||
"italic": {
|
||||
id: "italic",
|
||||
name: "Italic",
|
||||
description: "Italic text formatting",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["italic"],
|
||||
toolbarItems: ["italic"]
|
||||
},
|
||||
"underline": {
|
||||
id: "underline",
|
||||
name: "Underline",
|
||||
description: "Underline text formatting",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["underline"],
|
||||
toolbarItems: ["underline"]
|
||||
},
|
||||
"strikethrough": {
|
||||
id: "strikethrough",
|
||||
name: "Strikethrough",
|
||||
description: "Strikethrough text formatting",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["strikethrough"],
|
||||
toolbarItems: ["strikethrough"]
|
||||
},
|
||||
"code": {
|
||||
id: "code",
|
||||
name: "Inline Code",
|
||||
description: "Inline code formatting",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["code"],
|
||||
toolbarItems: ["code"]
|
||||
},
|
||||
"subscript": {
|
||||
id: "subscript",
|
||||
name: "Subscript",
|
||||
description: "Subscript text formatting",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["subscript"],
|
||||
toolbarItems: ["subscript"]
|
||||
},
|
||||
"superscript": {
|
||||
id: "superscript",
|
||||
name: "Superscript",
|
||||
description: "Superscript text formatting",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["superscript"],
|
||||
toolbarItems: ["superscript"]
|
||||
},
|
||||
|
||||
// Structure plugins
|
||||
"heading": {
|
||||
id: "heading",
|
||||
name: "Headings",
|
||||
description: "Heading levels (H2-H6)",
|
||||
category: "structure",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["heading"],
|
||||
toolbarItems: ["heading"]
|
||||
},
|
||||
"blockquote": {
|
||||
id: "blockquote",
|
||||
name: "Block Quote",
|
||||
description: "Block quote formatting",
|
||||
category: "structure",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["blockQuote"],
|
||||
toolbarItems: ["blockQuote"]
|
||||
},
|
||||
"list": {
|
||||
id: "list",
|
||||
name: "Lists",
|
||||
description: "Bulleted and numbered lists",
|
||||
category: "structure",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["bulletedList", "numberedList"],
|
||||
toolbarItems: ["bulletedList", "numberedList"]
|
||||
},
|
||||
"todolist": {
|
||||
id: "todolist",
|
||||
name: "Todo List",
|
||||
description: "Checkable todo list items",
|
||||
category: "structure",
|
||||
defaultEnabled: true,
|
||||
dependencies: ["list"],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["todoList"],
|
||||
toolbarItems: ["todoList"]
|
||||
},
|
||||
"alignment": {
|
||||
id: "alignment",
|
||||
name: "Text Alignment",
|
||||
description: "Text alignment (left, center, right, justify)",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["alignment"],
|
||||
toolbarItems: ["alignment"]
|
||||
},
|
||||
"indent": {
|
||||
id: "indent",
|
||||
name: "Indentation",
|
||||
description: "Text and block indentation",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["indent", "outdent"],
|
||||
toolbarItems: ["indent", "outdent"]
|
||||
},
|
||||
|
||||
// Media plugins
|
||||
"image": {
|
||||
id: "image",
|
||||
name: "Images",
|
||||
description: "Image insertion, resizing and styling",
|
||||
category: "media",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["insertImage"],
|
||||
toolbarItems: ["insertImage"]
|
||||
},
|
||||
"link": {
|
||||
id: "link",
|
||||
name: "Links",
|
||||
description: "Hyperlinks and internal note links",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["link", "unlink"],
|
||||
toolbarItems: ["link", "unlink"]
|
||||
},
|
||||
|
||||
// Table plugins
|
||||
"table": {
|
||||
id: "table",
|
||||
name: "Tables",
|
||||
description: "Table creation and editing",
|
||||
category: "tables",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["insertTable"],
|
||||
toolbarItems: ["insertTable"]
|
||||
},
|
||||
|
||||
// Advanced plugins
|
||||
"codeblock": {
|
||||
id: "codeblock",
|
||||
name: "Code Blocks",
|
||||
description: "Syntax-highlighted code blocks",
|
||||
category: "advanced",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["codeBlock"],
|
||||
toolbarItems: ["codeBlock"]
|
||||
},
|
||||
"math": {
|
||||
id: "math",
|
||||
name: "Math Formulas",
|
||||
description: "Mathematical formulas using KaTeX",
|
||||
category: "advanced",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["math"],
|
||||
toolbarItems: ["math"]
|
||||
},
|
||||
"mermaid": {
|
||||
id: "mermaid",
|
||||
name: "Mermaid Diagrams",
|
||||
description: "Diagram creation using Mermaid syntax",
|
||||
category: "advanced",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["mermaid"],
|
||||
toolbarItems: ["mermaid"]
|
||||
},
|
||||
"admonition": {
|
||||
id: "admonition",
|
||||
name: "Admonitions",
|
||||
description: "Callout boxes and admonition blocks",
|
||||
category: "advanced",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["admonition"],
|
||||
toolbarItems: ["admonition"]
|
||||
},
|
||||
"footnotes": {
|
||||
id: "footnotes",
|
||||
name: "Footnotes",
|
||||
description: "Footnote references and definitions",
|
||||
category: "advanced",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["footnote"],
|
||||
toolbarItems: ["footnote"]
|
||||
},
|
||||
"keyboard": {
|
||||
id: "keyboard",
|
||||
name: "Keyboard Shortcuts",
|
||||
description: "Visual keyboard shortcut markers",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["kbd"],
|
||||
toolbarItems: ["kbd"]
|
||||
},
|
||||
"horizontalline": {
|
||||
id: "horizontalline",
|
||||
name: "Horizontal Line",
|
||||
description: "Horizontal rule/divider line",
|
||||
category: "structure",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["horizontalLine"],
|
||||
toolbarItems: ["horizontalLine"]
|
||||
},
|
||||
"pagebreak": {
|
||||
id: "pagebreak",
|
||||
name: "Page Break",
|
||||
description: "Page break for printing",
|
||||
category: "structure",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["pageBreak"],
|
||||
toolbarItems: ["pageBreak"]
|
||||
},
|
||||
"removeformat": {
|
||||
id: "removeformat",
|
||||
name: "Remove Formatting",
|
||||
description: "Remove text formatting",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["removeFormat"],
|
||||
toolbarItems: ["removeFormat"]
|
||||
},
|
||||
"findandreplace": {
|
||||
id: "findandreplace",
|
||||
name: "Find and Replace",
|
||||
description: "Text search and replace functionality",
|
||||
category: "advanced",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["findAndReplace"],
|
||||
toolbarItems: ["findAndReplace"]
|
||||
},
|
||||
"font": {
|
||||
id: "font",
|
||||
name: "Font Styling",
|
||||
description: "Font family, size, color, and background color",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["fontFamily", "fontSize", "fontColor", "fontBackgroundColor"],
|
||||
toolbarItems: ["fontFamily", "fontSize", "fontColor", "fontBackgroundColor"]
|
||||
},
|
||||
"specialcharacters": {
|
||||
id: "specialcharacters",
|
||||
name: "Special Characters",
|
||||
description: "Insert special characters and symbols",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["specialCharacters"],
|
||||
toolbarItems: ["specialCharacters"]
|
||||
},
|
||||
"emoji": {
|
||||
id: "emoji",
|
||||
name: "Emoji Support",
|
||||
description: "Emoji insertion and autocomplete",
|
||||
category: "formatting",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["emoji"],
|
||||
toolbarItems: ["emoji"]
|
||||
},
|
||||
|
||||
// Premium plugins
|
||||
"slashcommand": {
|
||||
id: "slashcommand",
|
||||
name: "Slash Commands",
|
||||
description: "Quick command insertion with / key",
|
||||
category: "advanced",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: true,
|
||||
isCore: false,
|
||||
commands: ["slashCommand"]
|
||||
},
|
||||
"template": {
|
||||
id: "template",
|
||||
name: "Templates",
|
||||
description: "Text templates and snippets",
|
||||
category: "advanced",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: true,
|
||||
isCore: false,
|
||||
commands: ["template"],
|
||||
toolbarItems: ["template"]
|
||||
},
|
||||
|
||||
// Trilium-specific plugins
|
||||
"uploadimage": {
|
||||
id: "uploadimage",
|
||||
name: "Image Upload",
|
||||
description: "Trilium-specific image upload handling",
|
||||
category: "trilium",
|
||||
defaultEnabled: true,
|
||||
dependencies: ["image"],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
},
|
||||
"cuttonote": {
|
||||
id: "cuttonote",
|
||||
name: "Cut to Note",
|
||||
description: "Cut selected text to create a new note",
|
||||
category: "trilium",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["cutToNote"]
|
||||
},
|
||||
"internallink": {
|
||||
id: "internallink",
|
||||
name: "Internal Links",
|
||||
description: "Trilium-specific internal note linking",
|
||||
category: "trilium",
|
||||
defaultEnabled: true,
|
||||
dependencies: ["link"],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
},
|
||||
"insertdatetime": {
|
||||
id: "insertdatetime",
|
||||
name: "Insert Date/Time",
|
||||
description: "Insert current date and time",
|
||||
category: "trilium",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["insertDateTime"]
|
||||
},
|
||||
"includenote": {
|
||||
id: "includenote",
|
||||
name: "Include Note",
|
||||
description: "Include content from other notes",
|
||||
category: "trilium",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["includeNote"]
|
||||
},
|
||||
"uploadfile": {
|
||||
id: "uploadfile",
|
||||
name: "File Upload",
|
||||
description: "Upload and attach files",
|
||||
category: "trilium",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["uploadFile"]
|
||||
},
|
||||
"markdownimport": {
|
||||
id: "markdownimport",
|
||||
name: "Markdown Import",
|
||||
description: "Import markdown content",
|
||||
category: "trilium",
|
||||
defaultEnabled: true,
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
requiresPremium: false,
|
||||
isCore: false,
|
||||
commands: ["markdownImport"]
|
||||
}
|
||||
},
|
||||
version: "1.0.0",
|
||||
lastModified: new Date().toISOString()
|
||||
};
|
||||
|
||||
/**
|
||||
* Get plugin metadata by ID
|
||||
*/
|
||||
export function getPluginMetadata(pluginId: string): PluginMetadata | undefined {
|
||||
return PLUGIN_REGISTRY.plugins[pluginId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all plugins in a category
|
||||
*/
|
||||
export function getPluginsByCategory(category: string): PluginMetadata[] {
|
||||
return Object.values(PLUGIN_REGISTRY.plugins).filter(plugin => plugin.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get core plugins (cannot be disabled)
|
||||
*/
|
||||
export function getCorePlugins(): PluginMetadata[] {
|
||||
return Object.values(PLUGIN_REGISTRY.plugins).filter(plugin => plugin.isCore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configurable plugins (can be enabled/disabled)
|
||||
*/
|
||||
export function getConfigurablePlugins(): PluginMetadata[] {
|
||||
return Object.values(PLUGIN_REGISTRY.plugins).filter(plugin => !plugin.isCore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default enabled plugins configuration as JSON string
|
||||
*/
|
||||
export function getDefaultPluginConfiguration(): string {
|
||||
const defaultConfig: Record<string, boolean> = {};
|
||||
Object.values(PLUGIN_REGISTRY.plugins).forEach(plugin => {
|
||||
if (!plugin.isCore) {
|
||||
defaultConfig[plugin.id] = plugin.defaultEnabled;
|
||||
}
|
||||
});
|
||||
return JSON.stringify(defaultConfig);
|
||||
}
|
||||
|
||||
@@ -4,20 +4,23 @@
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../ckeditor5-footnotes"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-math"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-admonition"
|
||||
"path": "../commons"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-mermaid"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-math"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-keyboard-marker"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-footnotes"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-admonition"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
|
||||
@@ -20,19 +20,22 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../ckeditor5-footnotes"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-math"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-admonition"
|
||||
"path": "../commons/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-mermaid"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-math"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-keyboard-marker"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-footnotes"
|
||||
},
|
||||
{
|
||||
"path": "../ckeditor5-admonition"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/commons",
|
||||
"version": "0.98.1",
|
||||
"version": "0.98.0",
|
||||
"description": "Shared library between the clients (e.g. browser, Electron) and the server, mostly for type definitions and utility methods.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./lib/i18n.js";
|
||||
export * from "./lib/options_interface.js";
|
||||
export * from "./lib/ckeditor_plugin_interface.js";
|
||||
export * from "./lib/keyboard_actions_interface.js";
|
||||
export * from "./lib/hidden_subtree.js";
|
||||
export * from "./lib/rows.js";
|
||||
|
||||
163
packages/commons/src/lib/ckeditor_plugin_interface.ts
Normal file
163
packages/commons/src/lib/ckeditor_plugin_interface.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @module CKEditor Plugin Interface
|
||||
*
|
||||
* This module defines the TypeScript interfaces and types for managing
|
||||
* CKEditor plugins in Trilium Notes. It provides type-safe configuration
|
||||
* for plugin enablement, metadata, and dependency management.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines the categories of CKEditor plugins available in Trilium.
|
||||
*/
|
||||
export type PluginCategory =
|
||||
| "formatting" // Text formatting (bold, italic, etc.)
|
||||
| "structure" // Document structure (headings, lists, etc.)
|
||||
| "media" // Images, files, embeds
|
||||
| "tables" // Table-related functionality
|
||||
| "advanced" // Advanced features (math, mermaid, etc.)
|
||||
| "trilium" // Trilium-specific plugins
|
||||
| "external"; // Third-party plugins
|
||||
|
||||
/**
|
||||
* Represents the metadata for a CKEditor plugin.
|
||||
*/
|
||||
export interface PluginMetadata {
|
||||
/** Unique identifier for the plugin */
|
||||
id: string;
|
||||
/** Human-readable display name */
|
||||
name: string;
|
||||
/** Brief description of the plugin's functionality */
|
||||
description: string;
|
||||
/** Category this plugin belongs to */
|
||||
category: PluginCategory;
|
||||
/** Whether this plugin is enabled by default for new users */
|
||||
defaultEnabled: boolean;
|
||||
/** Array of plugin IDs that this plugin depends on */
|
||||
dependencies: string[];
|
||||
/** Array of plugin IDs that conflict with this plugin */
|
||||
conflicts: string[];
|
||||
/** Whether this plugin requires a premium CKEditor license */
|
||||
requiresPremium: boolean;
|
||||
/** Whether this plugin is part of the core editor functionality (cannot be disabled) */
|
||||
isCore: boolean;
|
||||
/** Toolbar items/commands provided by this plugin */
|
||||
toolbarItems?: string[];
|
||||
/** Commands provided by this plugin */
|
||||
commands?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a user's CKEditor plugin preferences.
|
||||
*/
|
||||
export interface PluginConfiguration {
|
||||
/** Plugin ID */
|
||||
id: string;
|
||||
/** Whether the plugin is enabled for this user */
|
||||
enabled: boolean;
|
||||
/** User-specific configuration for the plugin (if any) */
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete registry of available CKEditor plugins.
|
||||
*/
|
||||
export interface PluginRegistry {
|
||||
/** Map of plugin ID to plugin metadata */
|
||||
plugins: Record<string, PluginMetadata>;
|
||||
/** Version of the plugin registry (for cache invalidation) */
|
||||
version: string;
|
||||
/** Last modified timestamp */
|
||||
lastModified: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of plugin dependency validation.
|
||||
*/
|
||||
export interface PluginValidationResult {
|
||||
/** Whether the configuration is valid */
|
||||
valid: boolean;
|
||||
/** Array of validation errors */
|
||||
errors: PluginValidationError[];
|
||||
/** Array of warnings (non-blocking issues) */
|
||||
warnings: PluginValidationWarning[];
|
||||
/** Resolved list of plugins that should be enabled */
|
||||
resolvedPlugins: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error for plugin configuration.
|
||||
*/
|
||||
export interface PluginValidationError {
|
||||
/** Type of error */
|
||||
type: "missing_dependency" | "circular_dependency" | "plugin_conflict" | "premium_required";
|
||||
/** Plugin ID that caused the error */
|
||||
pluginId: string;
|
||||
/** Human-readable error message */
|
||||
message: string;
|
||||
/** Additional context about the error */
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation warning for plugin configuration.
|
||||
*/
|
||||
export interface PluginValidationWarning {
|
||||
/** Type of warning */
|
||||
type: "dependency_disabled" | "unused_dependency" | "performance_impact";
|
||||
/** Plugin ID that caused the warning */
|
||||
pluginId: string;
|
||||
/** Human-readable warning message */
|
||||
message: string;
|
||||
/** Additional context about the warning */
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to update plugin configuration.
|
||||
*/
|
||||
export interface UpdatePluginConfigRequest {
|
||||
/** Array of plugin configurations to update */
|
||||
plugins: PluginConfiguration[];
|
||||
/** Whether to validate dependencies before saving */
|
||||
validate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from updating plugin configuration.
|
||||
*/
|
||||
export interface UpdatePluginConfigResponse {
|
||||
/** Whether the update was successful */
|
||||
success: boolean;
|
||||
/** Validation result (if validation was requested) */
|
||||
validation?: PluginValidationResult;
|
||||
/** Updated plugin configurations */
|
||||
plugins: PluginConfiguration[];
|
||||
/** Any errors that occurred during the update */
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for querying the plugin registry.
|
||||
*/
|
||||
export interface QueryPluginsOptions {
|
||||
/** Filter by category */
|
||||
category?: PluginCategory;
|
||||
/** Filter by enabled status */
|
||||
enabled?: boolean;
|
||||
/** Filter by core status */
|
||||
coreOnly?: boolean;
|
||||
/** Include user configuration in results */
|
||||
includeConfig?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of querying plugins.
|
||||
*/
|
||||
export interface QueryPluginsResult {
|
||||
/** Array of plugin metadata */
|
||||
plugins: (PluginMetadata & { enabled?: boolean; config?: Record<string, unknown> })[];
|
||||
/** Total count of plugins (before filtering) */
|
||||
totalCount: number;
|
||||
/** Categories available in the registry */
|
||||
categories: PluginCategory[];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export interface Locale {
|
||||
/** `true` if the language is not supported by the application as a display language, but it is selectable by the user for the content. */
|
||||
contentOnly?: boolean;
|
||||
/** The value to pass to `--lang` for the Electron instance in order to set it as a locale. Not setting it will hide it from the list of supported locales. */
|
||||
electronLocale?: "en" | "de" | "es" | "fr" | "zh_CN" | "zh_TW" | "ro" | "af" | "am" | "ar" | "bg" | "bn" | "ca" | "cs" | "da" | "el" | "en_GB" | "es_419" | "et" | "fa" | "fi" | "fil" | "gu" | "he" | "hi" | "hr" | "hu" | "id" | "it" | "ja" | "kn" | "ko" | "lt" | "lv" | "ml" | "mr" | "ms" | "nb" | "nl" | "pl" | "pt_BR" | "pt_PT" | "ru" | "sk" | "sl" | "sr" | "sv" | "sw" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "vi";
|
||||
electronLocale?: "en" | "de" | "es" | "fr" | "zh_CN" | "zh_TW" | "ro" | "af" | "am" | "ar" | "bg" | "bn" | "ca" | "cs" | "da" | "el" | "en-GB" | "es-419" | "et" | "fa" | "fi" | "fil" | "gu" | "he" | "hi" | "hr" | "hu" | "id" | "it" | "ja" | "kn" | "ko" | "lt" | "lv" | "ml" | "mr" | "ms" | "nb" | "nl" | "pl" | "pt-BR" | "pt-PT" | "ru" | "sk" | "sl" | "sr" | "sv" | "sw" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "vi";
|
||||
}
|
||||
|
||||
const UNSORTED_LOCALES: Locale[] = [
|
||||
@@ -16,7 +16,7 @@ const UNSORTED_LOCALES: Locale[] = [
|
||||
{ id: "es", name: "Español", electronLocale: "es" },
|
||||
{ id: "fr", name: "Français", electronLocale: "fr" },
|
||||
{ id: "ja", name: "日本語", electronLocale: "ja" },
|
||||
{ id: "pt_br", name: "Português (Brasil)", electronLocale: "pt_BR" },
|
||||
{ id: "pt_br", name: "Português (Brasil)", electronLocale: "pt-BR" },
|
||||
{ id: "ro", name: "Română", electronLocale: "ro" },
|
||||
{ id: "ru", name: "Русский", electronLocale: "ru" },
|
||||
{ id: "tw", name: "繁體中文", electronLocale: "zh_TW" },
|
||||
|
||||
@@ -150,6 +150,9 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
||||
codeOpenAiModel: string;
|
||||
aiSelectedProvider: string;
|
||||
seenCallToActions: string;
|
||||
|
||||
// CKEditor plugin options
|
||||
ckeditorEnabledPlugins: string;
|
||||
}
|
||||
|
||||
export type OptionNames = keyof OptionDefinitions;
|
||||
|
||||
412
pnpm-lock.yaml
generated
412
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user