mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 10:05:40 +02:00
Custom dictionary (#9317)
This commit is contained in:
20
CLAUDE.md
20
CLAUDE.md
@@ -122,6 +122,12 @@ Trilium provides powerful user scripting capabilities:
|
||||
- Third-party components (e.g., mind-map context menu) should use i18next `t()` for their labels, with the English strings added to `en/translation.json` under a dedicated namespace (e.g., `"mind-map"`)
|
||||
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
|
||||
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
|
||||
- **Server-side translations** (e.g. hidden subtree titles) go in `apps/server/src/assets/translations/en/server.json`, not in the client `translation.json`
|
||||
|
||||
### Electron Desktop App
|
||||
- Desktop entry point: `apps/desktop/src/main.ts`, window management: `apps/server/src/services/window.ts`
|
||||
- IPC communication: use `electron.ipcMain.on(channel, handler)` on server side, `electron.ipcRenderer.send(channel, data)` on client side
|
||||
- Electron-only features should check `isElectron()` from `apps/client/src/services/utils.ts` (client) or `utils.isElectron` (server)
|
||||
|
||||
### Security Considerations
|
||||
- Per-note encryption with granular protected sessions
|
||||
@@ -153,6 +159,20 @@ Trilium provides powerful user scripting capabilities:
|
||||
- Create new package in `packages/` following existing plugin structure
|
||||
- Register in `packages/ckeditor5/src/plugins.ts`
|
||||
|
||||
### Adding Hidden System Notes
|
||||
The hidden subtree (`_hidden`) contains system notes with predictable IDs (prefixed with `_`). Defined in `apps/server/src/services/hidden_subtree.ts` via the `HiddenSubtreeItem` interface from `@triliumnext/commons`.
|
||||
|
||||
1. Add the note definition to `buildHiddenSubtreeDefinition()` in `apps/server/src/services/hidden_subtree.ts`
|
||||
2. Add a translation key for the title in `apps/server/src/assets/translations/en/server.json` under `"hidden-subtree"`
|
||||
3. The note is auto-created on startup by `checkHiddenSubtree()` — uses deterministic IDs so all sync cluster instances generate the same structure
|
||||
4. Key properties: `id` (must start with `_`), `title`, `type`, `icon` (format: `bx-icon-name` without `bx ` prefix), `attributes`, `children`, `content`
|
||||
5. Use `enforceAttributes: true` to keep attributes in sync, `enforceBranches: true` for correct placement, `enforceDeleted: true` to remove deprecated notes
|
||||
6. For launcher bar entries, see `hidden_subtree_launcherbar.ts`; for templates, see `hidden_subtree_templates.ts`
|
||||
|
||||
### Writing to Notes from Server Services
|
||||
- `note.setContent()` requires a CLS (Continuation Local Storage) context — wrap calls in `cls.init(() => { ... })` (from `apps/server/src/services/cls.ts`)
|
||||
- Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not
|
||||
|
||||
### Adding New LLM Tools
|
||||
Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/` and automatically registered for both the LLM chat and MCP server.
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ function setupContextMenu() {
|
||||
items.push({
|
||||
title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }),
|
||||
uiIcon: "bx bx-plus",
|
||||
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
|
||||
handler: () => electron.ipcRenderer.send("add-word-to-dictionary", params.misspelledWord)
|
||||
});
|
||||
|
||||
items.push({ kind: "separator" });
|
||||
|
||||
@@ -1498,12 +1498,15 @@
|
||||
"spellcheck": {
|
||||
"title": "Spell Check",
|
||||
"description": "These options apply only for desktop builds, browsers will use their own native spell check.",
|
||||
"enable": "Enable spellcheck",
|
||||
"language_code_label": "Language code(s)",
|
||||
"language_code_placeholder": "for example \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "Multiple languages can be separated by comma, e.g. \"en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Available language codes:",
|
||||
"restart-required": "Changes to the spell check options will take effect after application restart."
|
||||
"enable": "Check spelling",
|
||||
"language_code_label": "Spell Check Languages",
|
||||
"restart-required": "Changes to the spell check options will take effect after application restart.",
|
||||
"custom_dictionary_title": "Custom Dictionary",
|
||||
"custom_dictionary_description": "Words added to the dictionary are synced across all your devices.",
|
||||
"custom_dictionary_edit": "Custom words",
|
||||
"custom_dictionary_edit_description": "Edit the list of words that should not be flagged by the spell checker. Changes will be visible after a restart.",
|
||||
"custom_dictionary_open": "Edit dictionary",
|
||||
"related_description": "Configure spell check languages and custom dictionary."
|
||||
},
|
||||
"sync_2": {
|
||||
"config_title": "Sync Configuration",
|
||||
|
||||
@@ -45,3 +45,15 @@
|
||||
.option-row.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.option-row-link.use-tn-links {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
margin-inline: calc(-1 * var(--options-card-padding, 15px));
|
||||
padding-inline: var(--options-card-padding, 15px);
|
||||
transition: background-color 250ms ease-in-out;
|
||||
}
|
||||
|
||||
.option-row-link:hover {
|
||||
background: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { cloneElement, VNode } from "preact";
|
||||
import "./OptionsRow.css";
|
||||
|
||||
import { cloneElement, VNode } from "preact";
|
||||
|
||||
import { useUniqueName } from "../../../react/hooks";
|
||||
|
||||
interface OptionsRowProps {
|
||||
@@ -25,4 +27,24 @@ export default function OptionsRow({ name, label, description, children, centere
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface OptionsRowLinkProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export function OptionsRowLink({ label, description, href }: OptionsRowLinkProps) {
|
||||
return (
|
||||
<a href={href} className="option-row option-row-link use-tn-links no-tooltip-preview">
|
||||
<div className="option-row-label">
|
||||
<label style={{ cursor: "pointer" }}>{label}</label>
|
||||
{description && <small className="option-row-description">{description}</small>}
|
||||
</div>
|
||||
<div className="option-row-input">
|
||||
<span className="bx bx-chevron-right" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import OptionsSection from "./OptionsSection";
|
||||
import type { OptionPages } from "../../ContentWidget";
|
||||
import { t } from "../../../../services/i18n";
|
||||
import type { OptionPages } from "../../ContentWidget";
|
||||
import { OptionsRowLink } from "./OptionsRow";
|
||||
import OptionsSection from "./OptionsSection";
|
||||
|
||||
interface RelatedSettingsItem {
|
||||
title: string;
|
||||
description?: string;
|
||||
targetPage: OptionPages;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface RelatedSettingsProps {
|
||||
items: {
|
||||
title: string;
|
||||
targetPage: OptionPages;
|
||||
}[];
|
||||
items: RelatedSettingsItem[];
|
||||
}
|
||||
|
||||
export default function RelatedSettings({ items }: RelatedSettingsProps) {
|
||||
const filteredItems = items.filter(item => item.enabled !== false);
|
||||
|
||||
if (filteredItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("settings.related_settings")}>
|
||||
<nav className="use-tn-links" style={{ padding: 0, margin: 0, listStyleType: "none" }}>
|
||||
{items.map(item => (
|
||||
<li>
|
||||
<a href={`#root/_hidden/_options/${item.targetPage}`}>{item.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</nav>
|
||||
{filteredItems.map((item) => (
|
||||
<OptionsRowLink
|
||||
key={item.targetPage}
|
||||
label={item.title}
|
||||
description={item.description}
|
||||
href={`#root/_hidden/_options/${item.targetPage}`}
|
||||
/>
|
||||
))}
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import { useTriliumOption, useTriliumOptionJson } from "../../react/hooks";
|
||||
import type { Locale } from "@triliumnext/commons";
|
||||
import { restartDesktopApp } from "../../../services/utils";
|
||||
import { isElectron, restartDesktopApp } from "../../../services/utils";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import FormText from "../../react/FormText";
|
||||
import RawHtml from "../../react/RawHtml";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import Button from "../../react/Button";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import RelatedSettings from "./components/RelatedSettings";
|
||||
import { LocaleSelector } from "./components/LocaleSelector";
|
||||
|
||||
export default function InternationalizationOptions() {
|
||||
@@ -19,8 +20,17 @@ export default function InternationalizationOptions() {
|
||||
<>
|
||||
<LocalizationOptions />
|
||||
<ContentLanguages />
|
||||
{isElectron() && (
|
||||
<RelatedSettings items={[
|
||||
{
|
||||
title: t("spellcheck.title"),
|
||||
description: t("spellcheck.related_description"),
|
||||
targetPage: "_optionsSpellcheck"
|
||||
}
|
||||
]} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function LocalizationOptions() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { isElectron } from "../../../services/utils";
|
||||
import { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
@@ -93,7 +94,8 @@ function OcrSettings() {
|
||||
<RelatedSettings items={[
|
||||
{
|
||||
title: t("images.ocr_related_content_languages"),
|
||||
targetPage: "_optionsLocalization"
|
||||
targetPage: "_optionsLocalization",
|
||||
enabled: isElectron(), // This setting is only relevant for desktop, as web browsers use their own native OCR which doesn't support language selection.
|
||||
}
|
||||
]} />
|
||||
</>
|
||||
|
||||
@@ -1,63 +1,132 @@
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { useCallback, useMemo } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import { t } from "../../../services/i18n";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import { dynamicRequire, isElectron, restartDesktopApp } from "../../../services/utils";
|
||||
import Button from "../../react/Button";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import NoItems from "../../react/NoItems";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import { dynamicRequire, isElectron } from "../../../services/utils";
|
||||
|
||||
export default function SpellcheckSettings() {
|
||||
if (isElectron()) {
|
||||
return <ElectronSpellcheckSettings />
|
||||
} else {
|
||||
return <WebSpellcheckSettings />
|
||||
return <ElectronSpellcheckSettings />;
|
||||
}
|
||||
return <WebSpellcheckSettings />;
|
||||
}
|
||||
|
||||
interface SpellcheckLanguage {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function ElectronSpellcheckSettings() {
|
||||
const [ spellCheckEnabled, setSpellCheckEnabled ] = useTriliumOptionBool("spellCheckEnabled");
|
||||
|
||||
return (
|
||||
<>
|
||||
<OptionsSection title={t("spellcheck.title")}>
|
||||
<FormText>{t("spellcheck.restart-required")}</FormText>
|
||||
|
||||
<OptionsRow name="spell-check-enabled" label={t("spellcheck.enable")}>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={spellCheckEnabled}
|
||||
onChange={setSpellCheckEnabled}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRow name="restart" centered>
|
||||
<Button
|
||||
name="restart-app-button"
|
||||
text={t("electron_integration.restart-app-button")}
|
||||
size="micro"
|
||||
onClick={restartDesktopApp}
|
||||
/>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
|
||||
{spellCheckEnabled && <SpellcheckLanguages />}
|
||||
{spellCheckEnabled && <CustomDictionary />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SpellcheckLanguages() {
|
||||
const [ spellCheckLanguageCode, setSpellCheckLanguageCode ] = useTriliumOption("spellCheckLanguageCode");
|
||||
|
||||
const availableLanguageCodes = useMemo(() => {
|
||||
const selectedCodes = useMemo(() =>
|
||||
(spellCheckLanguageCode ?? "")
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => c.length > 0),
|
||||
[spellCheckLanguageCode]
|
||||
);
|
||||
|
||||
const setSelectedCodes = useCallback((codes: string[]) => {
|
||||
setSpellCheckLanguageCode(codes.join(", "));
|
||||
}, [setSpellCheckLanguageCode]);
|
||||
|
||||
const availableLanguages = useMemo<SpellcheckLanguage[]>(() => {
|
||||
if (!isElectron()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { webContents } = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||
return webContents.session.availableSpellCheckerLanguages as string[];
|
||||
}, [])
|
||||
const { webContents } = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||
const codes = webContents.session.availableSpellCheckerLanguages as string[];
|
||||
const displayNames = new Intl.DisplayNames([navigator.language], { type: "language" });
|
||||
|
||||
return codes.map((code) => ({
|
||||
code,
|
||||
name: displayNames.of(code) ?? code
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("spellcheck.title")}>
|
||||
<FormText>{t("spellcheck.restart-required")}</FormText>
|
||||
|
||||
<FormCheckbox
|
||||
name="spell-check-enabled"
|
||||
label={t("spellcheck.enable")}
|
||||
currentValue={spellCheckEnabled} onChange={setSpellCheckEnabled}
|
||||
<OptionsSection title={t("spellcheck.language_code_label")}>
|
||||
<CheckboxList
|
||||
values={availableLanguages}
|
||||
keyProperty="code" titleProperty="name"
|
||||
currentValue={selectedCodes}
|
||||
onChange={setSelectedCodes}
|
||||
columnWidth="200px"
|
||||
/>
|
||||
|
||||
<FormGroup name="spell-check-languages" label={t("spellcheck.language_code_label")} description={t("spellcheck.multiple_languages_info")}>
|
||||
<FormTextBox
|
||||
placeholder={t("spellcheck.language_code_placeholder")}
|
||||
currentValue={spellCheckLanguageCode} onChange={setSpellCheckLanguageCode}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormText>
|
||||
<strong>{t("spellcheck.available_language_codes_label")} </strong>
|
||||
{availableLanguageCodes.join(", ")}
|
||||
</FormText>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CustomDictionary() {
|
||||
function openDictionary() {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: "_customDictionary" });
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("spellcheck.custom_dictionary_title")}>
|
||||
<FormText>{t("spellcheck.custom_dictionary_description")}</FormText>
|
||||
|
||||
<OptionsRow name="custom-dictionary" label={t("spellcheck.custom_dictionary_edit")} description={t("spellcheck.custom_dictionary_edit_description")}>
|
||||
<Button
|
||||
name="open-custom-dictionary"
|
||||
text={t("spellcheck.custom_dictionary_open")}
|
||||
icon="bx bx-edit"
|
||||
onClick={openDictionary}
|
||||
/>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function WebSpellcheckSettings() {
|
||||
return (
|
||||
<OptionsSection title={t("spellcheck.title")}>
|
||||
<p>{t("spellcheck.description")}</p>
|
||||
<OptionsSection>
|
||||
<NoItems
|
||||
text={t("spellcheck.description")}
|
||||
icon="bx bx-check-double"
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
|
||||
import App from "./support/app";
|
||||
|
||||
test("Native Title Bar not displayed on web", async ({ page, context }) => {
|
||||
@@ -18,8 +19,6 @@ test("Tray settings not displayed on web", async ({ page, context }) => {
|
||||
test("Spellcheck settings not displayed on web", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto({ url: "http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck" });
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Spell Check" })).toBeVisible();
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||
await expect(app.currentNoteSplitContent.getByText("These options apply only for desktop builds")).toBeVisible();
|
||||
await expect(app.currentNoteSplitContent.getByText("Enable spellcheck")).toBeHidden();
|
||||
await expect(app.currentNoteSplitContent.getByText("Check spelling")).toBeHidden();
|
||||
});
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
@@ -12,12 +12,13 @@
|
||||
on other Chromium-based browsers as well, but they are not officially supported.</li>
|
||||
</ul>
|
||||
<h2>Obtaining the extension</h2>
|
||||
<aside class="admonition warning">
|
||||
<p>The extension is currently under development. A preview with unsigned
|
||||
extensions is available on <a href="https://github.com/TriliumNext/Trilium/actions/runs/21318809414">GitHub Actions</a>.</p>
|
||||
<p>We have already submitted the extension to both Chrome and Firefox web
|
||||
stores, but they are pending validation.</p>
|
||||
</aside>
|
||||
<p>The extension is available from the official browser web stores:</p>
|
||||
<ul>
|
||||
<li><strong>Firefox</strong>: <a href="https://addons.mozilla.org/firefox/addon/trilium-notes-web-clipper/">Trilium Web Clipper on Firefox Add-ons</a>
|
||||
</li>
|
||||
<li><strong>Chrome</strong>: <a href="https://chromewebstore.google.com/detail/trilium-web-clipper/ofoiklieachadcaeffficgjaajojpkpi">Trilium Web Clipper on Chrome Web Store</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Functionality</h2>
|
||||
<ul>
|
||||
<li>select text and clip it with the right-click context menu</li>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<img style="aspect-ratio:886/663;" src="2_Mermaid Diagrams_image.png"
|
||||
width="886" height="663">
|
||||
</figure>
|
||||
|
||||
<h2>Types of diagrams</h2>
|
||||
<p>Trilium supports Mermaid, which adds support for various diagrams such
|
||||
as flowchart, sequence diagram, class diagram, state diagram, pie charts,
|
||||
@@ -48,34 +49,30 @@
|
||||
<img src="1_Mermaid Diagrams_image.png">
|
||||
</li>
|
||||
<li>The preview can be moved around by holding the left mouse button and dragging.</li>
|
||||
<li
|
||||
>Zooming can also be done by using the scroll wheel.</li>
|
||||
<li>The zoom and position on the preview will remain fixed as the diagram
|
||||
changes, to be able to work more easily with large diagrams.</li>
|
||||
</ul>
|
||||
<li>Zooming can also be done by using the scroll wheel.</li>
|
||||
<li>The zoom and position on the preview will remain fixed as the diagram
|
||||
changes, to be able to work more easily with large diagrams.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>The size of the source/preview panes can be adjusted by hovering over
|
||||
the border between them and dragging it with the mouse.</li>
|
||||
<li>In the <a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a> area:
|
||||
<ul>
|
||||
<li>The source/preview can be laid out left-right or bottom-top via the <em>Move editing pane to the left / bottom</em> option.</li>
|
||||
<li
|
||||
>Press <em>Lock editing</em> to automatically mark the note as read-only.
|
||||
<li>Press <em>Lock editing</em> to automatically mark the note as read-only.
|
||||
In this mode, the code pane is hidden and the diagram is displayed full-size.
|
||||
Similarly, press <em>Unlock editing</em> to mark a read-only note as editable.</li>
|
||||
<li
|
||||
>Press the <em>Copy image reference to the clipboard</em> to be able to insert
|
||||
the image representation of the diagram into a text note. See <a class="reference-link"
|
||||
href="#root/_help_0Ofbk1aSuVRu">Image references</a> for more information.</li>
|
||||
<li
|
||||
>Press the <em>Export diagram as SVG</em> to download a scalable/vector rendering
|
||||
of the diagram. Can be used to present the diagram without degrading when
|
||||
zooming.</li>
|
||||
<li>Press the <em>Copy image reference to the clipboard</em> to be able to insert
|
||||
the image representation of the diagram into a text note. See <a class="reference-link"
|
||||
href="#root/_help_0Ofbk1aSuVRu">Image references</a> for more information.</li>
|
||||
<li>Press the <em>Export diagram as SVG</em> to download a scalable/vector rendering
|
||||
of the diagram. Can be used to present the diagram without degrading when
|
||||
zooming.</li>
|
||||
<li>Press the <em>Export diagram as PNG</em> to download a normal image (at
|
||||
1x scale, raster) of the diagram. Can be used to send the diagram in more
|
||||
traditional channels such as e-mail.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Errors in the diagram</h2>
|
||||
<p>If there is an error in the source code, the error will be displayed in
|
||||
|
||||
86
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Spell Check.html
generated
vendored
Normal file
86
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Spell Check.html
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
<p>Trilium supports spell checking for your notes. How it works depends on
|
||||
whether you're using the <strong>desktop application</strong> (Electron)
|
||||
or accessing Trilium through a <strong>web browser</strong>.</p>
|
||||
<h2>Desktop</h2>
|
||||
<p>The desktop app uses Chromium's built-in spellchecker. You can configure
|
||||
it from <em>Options</em><strong> </strong>→ <em>Spell Check</em>.</p>
|
||||
<h3>Enabling spell check</h3>
|
||||
<p>Toggle <em>Check spelling</em> to enable or disable the spellchecker. A
|
||||
restart is required for changes to take effect — use the restart button
|
||||
at the bottom of the section.</p>
|
||||
<h3>Choosing languages</h3>
|
||||
<p>When spell check is enabled, a <em>Spell Check Languages</em> section appears
|
||||
listing all languages available on your system. Select one or more languages
|
||||
by checking the boxes. The spellchecker will accept words that are valid
|
||||
in <em>any</em> of the selected languages.</p>
|
||||
<p>The available languages depend on your operating system's installed language
|
||||
packs. For example, on Windows you can add languages through <em>Options </em>→ <em>Time & Language </em>→ <em>Language & Region </em>→ <em>Add a language</em>.</p>
|
||||
<aside
|
||||
class="admonition note">
|
||||
<p>The changes take effect only after restarting the application.</p>
|
||||
</aside>
|
||||
<h3>Custom dictionary</h3>
|
||||
<aside class="admonition tip">
|
||||
<p>This function is available starting with Trilium v0.103.0.</p>
|
||||
</aside>
|
||||
<p>Words you add to the dictionary (e.g. via the right-click context menu
|
||||
→ "Add to dictionary") are stored in a <strong>synced note</strong> inside
|
||||
Trilium. This means your custom dictionary automatically syncs across all
|
||||
your devices.</p>
|
||||
<p>You can view and edit the dictionary directly from <em>Settings </em>→ <em>Spell Check </em>→ <em>Custom Dictionary </em>→ <em>Edit dictionary</em>.
|
||||
This opens the underlying note, which contains one word per line. You can
|
||||
add, remove, or modify entries as you like.</p>
|
||||
<aside class="admonition note">
|
||||
<p>Changes to the custom dictionary (whether from the editor or the context
|
||||
menu) take effect after restarting the application.</p>
|
||||
</aside>
|
||||
<h4>How the custom dictionary works</h4>
|
||||
<ul>
|
||||
<li>When you right-click a misspelled word and choose "Add to dictionary",
|
||||
the word is saved both to Electron's local spellchecker and to the synced
|
||||
dictionary note.</li>
|
||||
<li>On startup, Trilium loads all words from the dictionary note into the
|
||||
spellchecker session.</li>
|
||||
<li>If Trilium detects words in Electron's local dictionary but the dictionary
|
||||
note is empty (e.g. on first use), it performs a <strong>one-time import</strong> of
|
||||
those words into the note.</li>
|
||||
<li>Words that are in Electron's local dictionary but <em>not</em> in the note
|
||||
(e.g. you removed them manually) are cleaned up from the local dictionary
|
||||
on startup.</li>
|
||||
</ul>
|
||||
<h4>Known limitations<a id="known-limitations"></a></h4>
|
||||
<p>On Windows and macOS, Electron delegates "Add to dictionary" to the operating
|
||||
system's user dictionary. This means:</p>
|
||||
<ul>
|
||||
<li>Words added via the context menu are also written to the OS-level dictionary
|
||||
(e.g. <code spellcheck="false">%APPDATA%\Microsoft\Spelling\<language>\default.dic</code> on
|
||||
Windows).</li>
|
||||
<li><strong>Removing a word</strong> from the Trilium dictionary note prevents
|
||||
it from being loaded into the spellchecker on next startup, but does <em>not</em> remove
|
||||
it from the OS dictionary. The word may still be accepted by the OS spellchecker
|
||||
until you remove it from the OS dictionary manually.</li>
|
||||
</ul>
|
||||
<h2>Web browser</h2>
|
||||
<p>When accessing Trilium through a web browser, spell checking is handled
|
||||
entirely by the browser itself. Trilium does not control the browser's
|
||||
spellchecker — language selection, dictionaries, and all other settings
|
||||
are managed through your browser's preferences.</p>
|
||||
<p>The Spell Check settings page in Trilium will indicate that these options
|
||||
apply only to desktop builds.</p>
|
||||
<h2>Frequently asked questions</h2>
|
||||
<h3>Do I need to restart after every change?</h3>
|
||||
<p>Yes. Spell check language selection and the custom dictionary are loaded
|
||||
once at startup. Any changes require a restart to take effect.</p>
|
||||
<h3>Can I use multiple spell check languages at the same time?</h3>
|
||||
<p>Yes. Select as many languages as you need from the checklist. The spellchecker
|
||||
will accept words from any of the selected languages.</p>
|
||||
<h3>My custom words disappeared after syncing to a new device — what happened?</h3>
|
||||
<p>On the first launch of a new device, Trilium may import existing local
|
||||
dictionary words into the note. If the note already has words from another
|
||||
device (via sync), those are preserved. Make sure sync completes before
|
||||
restarting the application on a new device.</p>
|
||||
<h3>I removed a word from the dictionary note but it's still accepted</h3>
|
||||
<p>This is likely due to the OS-level dictionary retaining the word (see
|
||||
<a
|
||||
href="#known-limitations">Known limitations</a>above). You can manually remove it from your operating
|
||||
system's user dictionary.</p>
|
||||
@@ -313,6 +313,7 @@
|
||||
"shared-notes-title": "Shared Notes",
|
||||
"bulk-action-title": "Bulk Action",
|
||||
"backend-log-title": "Backend Log",
|
||||
"custom-dictionary-title": "Custom Dictionary",
|
||||
"user-hidden-title": "User Hidden",
|
||||
"launch-bar-templates-title": "Launch Bar Templates",
|
||||
"base-abstract-launcher-title": "Base Abstract Launcher",
|
||||
|
||||
171
apps/server/src/services/custom_dictionary.spec.ts
Normal file
171
apps/server/src/services/custom_dictionary.spec.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import becca from "../becca/becca.js";
|
||||
import { buildNote } from "../test/becca_easy_mocking.js";
|
||||
import customDictionary from "./custom_dictionary.js";
|
||||
|
||||
vi.mock("./log.js", () => ({
|
||||
default: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock("./sql.js", () => ({
|
||||
default: {
|
||||
transactional: (cb: Function) => cb(),
|
||||
execute: () => {},
|
||||
replace: () => {},
|
||||
getMap: () => {},
|
||||
getValue: () => null,
|
||||
upsert: () => {}
|
||||
}
|
||||
}));
|
||||
|
||||
function mockSession(localWords: string[] = []) {
|
||||
return {
|
||||
listWordsInSpellCheckerDictionary: vi.fn().mockResolvedValue(localWords),
|
||||
addWordToSpellCheckerDictionary: vi.fn(),
|
||||
removeWordFromSpellCheckerDictionary: vi.fn()
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("custom_dictionary", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
becca.reset();
|
||||
buildNote({
|
||||
id: "_customDictionary",
|
||||
title: "Custom Dictionary",
|
||||
type: "code",
|
||||
content: ""
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadForSession", () => {
|
||||
it("does nothing when note is empty and no local words", async () => {
|
||||
const session = mockSession();
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
|
||||
expect(session.removeWordFromSpellCheckerDictionary).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("imports local words when note is empty (one-time import)", async () => {
|
||||
const session = mockSession(["hello", "world"]);
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
// Words are saved to the note; they're already in the local dictionary so no re-add needed.
|
||||
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not remove or re-add local words after one-time import", async () => {
|
||||
const session = mockSession(["hello", "world"]);
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
// Words were imported from local, so they already exist — no remove, no re-add.
|
||||
expect(session.removeWordFromSpellCheckerDictionary).not.toHaveBeenCalled();
|
||||
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads note words into session when no local words exist", async () => {
|
||||
becca.reset();
|
||||
buildNote({
|
||||
id: "_customDictionary",
|
||||
title: "Custom Dictionary",
|
||||
type: "code",
|
||||
content: "apple\nbanana"
|
||||
});
|
||||
const session = mockSession();
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(2);
|
||||
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
|
||||
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("banana");
|
||||
});
|
||||
|
||||
it("only adds note words not already in local dictionary", async () => {
|
||||
becca.reset();
|
||||
buildNote({
|
||||
id: "_customDictionary",
|
||||
title: "Custom Dictionary",
|
||||
type: "code",
|
||||
content: "apple\nbanana"
|
||||
});
|
||||
// "banana" is already local, so only "apple" needs adding.
|
||||
const session = mockSession(["banana", "cherry"]);
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(1);
|
||||
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
|
||||
});
|
||||
|
||||
it("only removes local words not in the note", async () => {
|
||||
becca.reset();
|
||||
buildNote({
|
||||
id: "_customDictionary",
|
||||
title: "Custom Dictionary",
|
||||
type: "code",
|
||||
content: "apple\nbanana"
|
||||
});
|
||||
// "cherry" is not in the note, so it should be removed. "banana" should stay.
|
||||
const session = mockSession(["banana", "cherry"]);
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledTimes(1);
|
||||
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledWith("cherry");
|
||||
});
|
||||
|
||||
it("handles note with whitespace and blank lines", async () => {
|
||||
becca.reset();
|
||||
buildNote({
|
||||
id: "_customDictionary",
|
||||
title: "Custom Dictionary",
|
||||
type: "code",
|
||||
content: " apple \n\n banana \n\n"
|
||||
});
|
||||
const session = mockSession();
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(2);
|
||||
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
|
||||
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("banana");
|
||||
});
|
||||
|
||||
it("does not re-add words removed from the note but present locally", async () => {
|
||||
becca.reset();
|
||||
buildNote({
|
||||
id: "_customDictionary",
|
||||
title: "Custom Dictionary",
|
||||
type: "code",
|
||||
content: "apple\nbanana"
|
||||
});
|
||||
// "cherry" was previously in the note but user removed it;
|
||||
// it still lingers in Electron's local dictionary.
|
||||
const session = mockSession(["apple", "banana", "cherry"]);
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
// "apple" and "banana" are already local — no re-add needed.
|
||||
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
|
||||
// "cherry" should be removed from local dictionary.
|
||||
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledTimes(1);
|
||||
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledWith("cherry");
|
||||
});
|
||||
|
||||
it("handles missing dictionary note gracefully", async () => {
|
||||
becca.reset(); // no note created
|
||||
const session = mockSession(["hello"]);
|
||||
|
||||
await customDictionary.loadForSession(session);
|
||||
|
||||
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
113
apps/server/src/services/custom_dictionary.ts
Normal file
113
apps/server/src/services/custom_dictionary.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { Session } from "electron";
|
||||
|
||||
import becca from "../becca/becca.js";
|
||||
import cls from "./cls.js";
|
||||
import log from "./log.js";
|
||||
|
||||
const DICTIONARY_NOTE_ID = "_customDictionary";
|
||||
|
||||
/**
|
||||
* Reads the custom dictionary words from the hidden note.
|
||||
*/
|
||||
function getWords(): Set<string> {
|
||||
const note = becca.getNote(DICTIONARY_NOTE_ID);
|
||||
if (!note) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const content = note.getContent();
|
||||
if (typeof content !== "string" || !content.trim()) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return new Set(
|
||||
content.split("\n")
|
||||
.map((w) => w.trim())
|
||||
.filter((w) => w.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given words to the custom dictionary note, one per line.
|
||||
*/
|
||||
function saveWords(words: Set<string>) {
|
||||
cls.init(() => {
|
||||
const note = becca.getNote(DICTIONARY_NOTE_ID);
|
||||
if (!note) {
|
||||
log.error("Custom dictionary note not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...words].sort((a, b) => a.localeCompare(b));
|
||||
note.setContent(sorted.join("\n"));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single word to the custom dictionary note.
|
||||
*/
|
||||
function addWord(word: string) {
|
||||
const words = getWords();
|
||||
words.add(word);
|
||||
saveWords(words);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all words from Electron's local spellchecker dictionary
|
||||
* so they are not re-imported on subsequent startups.
|
||||
*/
|
||||
function clearFromLocalDictionary(session: Session, localWords: string[]) {
|
||||
for (const word of localWords) {
|
||||
session.removeWordFromSpellCheckerDictionary(word);
|
||||
}
|
||||
log.info(`Cleared ${localWords.length} words from local spellchecker dictionary.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the custom dictionary into Electron's spellchecker session,
|
||||
* performing a one-time import of locally stored words on first use.
|
||||
*/
|
||||
async function loadForSession(session: Session) {
|
||||
const note = becca.getNote(DICTIONARY_NOTE_ID);
|
||||
if (!note) {
|
||||
log.error("Custom dictionary note not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const noteWords = getWords();
|
||||
const localWords = await session.listWordsInSpellCheckerDictionary();
|
||||
|
||||
let merged = noteWords;
|
||||
|
||||
// One-time import: if the note is empty but there are local words, import them.
|
||||
if (noteWords.size === 0 && localWords.length > 0) {
|
||||
log.info(`Importing ${localWords.length} words from local spellchecker dictionary.`);
|
||||
merged = new Set(localWords);
|
||||
saveWords(merged);
|
||||
}
|
||||
|
||||
// Remove local words that are not in the note (e.g. user removed them manually).
|
||||
const staleWords = localWords.filter((w) => !merged.has(w));
|
||||
if (staleWords.length > 0) {
|
||||
clearFromLocalDictionary(session, staleWords);
|
||||
}
|
||||
|
||||
// Add note words that aren't already in the local dictionary.
|
||||
const localWordsSet = new Set(localWords);
|
||||
for (const word of merged) {
|
||||
if (!localWordsSet.has(word)) {
|
||||
session.addWordToSpellCheckerDictionary(word);
|
||||
}
|
||||
}
|
||||
|
||||
if (merged.size > 0) {
|
||||
log.info(`Loaded ${merged.size} custom dictionary words into spellchecker.`);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getWords,
|
||||
saveWords,
|
||||
addWord,
|
||||
loadForSession
|
||||
};
|
||||
@@ -93,6 +93,12 @@ function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenS
|
||||
{ type: "label", name: "fullContentWidth" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_customDictionary",
|
||||
title: t("hidden-subtree.custom-dictionary-title"),
|
||||
type: "code",
|
||||
icon: "bx-book"
|
||||
},
|
||||
{
|
||||
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
|
||||
id: "_userHidden",
|
||||
|
||||
@@ -8,9 +8,7 @@ import dataDir from "./data_dir.js";
|
||||
import cls from "./cls.js";
|
||||
import config, { LOGGING_DEFAULT_RETENTION_DAYS } from "./config.js";
|
||||
|
||||
if (!fs.existsSync(dataDir.LOG_DIR)) {
|
||||
fs.mkdirSync(dataDir.LOG_DIR, 0o700);
|
||||
}
|
||||
fs.mkdirSync(dataDir.LOG_DIR, { recursive: true, mode: 0o700 });
|
||||
|
||||
let logFile: fs.WriteStream | undefined;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type App, type BrowserWindow, type BrowserWindowConstructorOptions, default as electron, ipcMain, type IpcMainEvent, type WebContents } from "electron";
|
||||
import { type App, type BrowserWindow, type BrowserWindowConstructorOptions, default as electron, ipcMain, type IpcMainEvent, type Session, type WebContents } from "electron";
|
||||
import fs from "fs/promises";
|
||||
import { t } from "i18next";
|
||||
import path from "path";
|
||||
@@ -6,6 +6,7 @@ import url from "url";
|
||||
|
||||
import app_info from "./app_info.js";
|
||||
import cls from "./cls.js";
|
||||
import customDictionary from "./custom_dictionary.js";
|
||||
import keyboardActionsService from "./keyboard_actions.js";
|
||||
import log from "./log.js";
|
||||
import optionService from "./options.js";
|
||||
@@ -18,6 +19,7 @@ import { formatDownloadTitle, isMac, isWindows } from "./utils.js";
|
||||
let mainWindow: BrowserWindow | null;
|
||||
let setupWindow: BrowserWindow | null;
|
||||
let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus.
|
||||
const loadedSpellcheckSessions = new WeakSet<Session>();
|
||||
|
||||
function trackWindowFocus(win: BrowserWindow) {
|
||||
// We need to get the last focused window from allWindows. If the last window is closed, we return the previous window.
|
||||
@@ -69,6 +71,11 @@ electron.ipcMain.on("create-extra-window", (event, arg) => {
|
||||
createExtraWindow(arg.extraWindowHash);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("add-word-to-dictionary", (event, word: string) => {
|
||||
event.sender.session.addWordToSpellCheckerDictionary(word);
|
||||
customDictionary.addWord(word);
|
||||
});
|
||||
|
||||
interface PrintOpts {
|
||||
notePath: string;
|
||||
printToPdf: boolean;
|
||||
@@ -375,12 +382,22 @@ async function configureWebContents(webContents: WebContents, spellcheckEnabled:
|
||||
});
|
||||
|
||||
if (spellcheckEnabled) {
|
||||
setupSpellcheckForSession(webContents.session);
|
||||
}
|
||||
}
|
||||
|
||||
function setupSpellcheckForSession(session: Session) {
|
||||
if (!loadedSpellcheckSessions.has(session)) {
|
||||
loadedSpellcheckSessions.add(session);
|
||||
|
||||
const languageCodes = optionService
|
||||
.getOption("spellCheckLanguageCode")
|
||||
.split(",")
|
||||
.map((code) => code.trim());
|
||||
|
||||
webContents.session.setSpellCheckerLanguages(languageCodes);
|
||||
session.setSpellCheckerLanguages(languageCodes);
|
||||
customDictionary.loadForSession(session)
|
||||
.catch(err => log.error(`Failed to load custom dictionary for spellcheck: ${err}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
docs/Developer Guide/!!!meta.json
vendored
2
docs/Developer Guide/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.101.3",
|
||||
"appVersion": "0.102.2",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# attachments
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `attachmentId` | Text | Non-null | | Unique ID (e.g. `qhC1vzU4nwSE`) |
|
||||
| `ownerId` | Text | Non-null | | The unique ID of a row in <a class="reference-link" href="notes.md">notes</a>. |
|
||||
| `role` | Text | Non-null | | The role of the attachment: `image` for images that are attached to a note, `file` for uploaded files. |
|
||||
| `mime` | Text | Non-null | | The MIME type of the attachment (e.g. `image/png`) |
|
||||
| `title` | Text | Non-null | | The title of the attachment. |
|
||||
| `isProtected` | Integer | Non-null | 0 | `1` if the entity is [protected](../../../Concepts/Protected%20entities.md), `0` otherwise. |
|
||||
| `position` | Integer | Non-null | 0 | Not sure where the position is relevant for attachments (saw it with values of 10 and 0). |
|
||||
| `attachmentId` | Text | Non-null | | Unique ID (e.g. `qhC1vzU4nwSE`) |
|
||||
| `ownerId` | Text | Non-null | | The unique ID of a row in <a class="reference-link" href="notes.md">notes</a>. |
|
||||
| `role` | Text | Non-null | | The role of the attachment: `image` for images that are attached to a note, `file` for uploaded files. |
|
||||
| `mime` | Text | Non-null | | The MIME type of the attachment (e.g. `image/png`) |
|
||||
| `title` | Text | Non-null | | The title of the attachment. |
|
||||
| `isProtected` | Integer | Non-null | 0 | `1` if the entity is [protected](../../../Concepts/Protected%20entities.md), `0` otherwise. |
|
||||
| `position` | Integer | Non-null | 0 | Not sure where the position is relevant for attachments (saw it with values of 10 and 0). |
|
||||
| `blobId` | Text | Nullable | `null` | The corresponding `blobId` from the <a class="reference-link" href="blobs.md">blobs</a> table. |
|
||||
| `dateModified` | Text | Non-null | | Localized modification date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `utcDateScheduledForErasure` | Text | Nullable | `null` | |
|
||||
| `isDeleted` | Integer | Non-null | | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||
| `deleteId` | Text | Nullable | `null` | |
|
||||
| `dateModified` | Text | Non-null | | Localized modification date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `utcDateScheduledForErasure` | Text | Nullable | `null` | |
|
||||
| `isDeleted` | Integer | Non-null | | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||
| `deleteId` | Text | Nullable | `null` | |
|
||||
@@ -1,12 +1,12 @@
|
||||
# branches
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `branchId` | Text | Non-null | | The ID of the branch, in the form of `a_b` where `a` is the `parentNoteId` and `b` is the `noteId`. |
|
||||
| `noteId` | Text | Non-null | | The ID of the [note](notes.md). |
|
||||
| `parentNoteId` | Text | Non-null | | The ID of the parent [note](notes.md) the note belongs to. |
|
||||
| `notePosition` | Integer | Non-null | | The position of the branch within the same level of hierarchy, the value is usually a multiple of 10. |
|
||||
| `prefix` | Text | Nullable | | The [branch prefix](../../../Concepts/Branch%20prefixes.md) if any, or `NULL` otherwise. |
|
||||
| `isExpanded` | Integer | Non-null | 0 | Whether the branch should appear expanded (its children shown) to the user. |
|
||||
| `isDeleted` | Integer | Non-null | 0 | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||
| `deleteId` | Text | Nullable | `null` | |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `branchId` | Text | Non-null | | The ID of the branch, in the form of `a_b` where `a` is the `parentNoteId` and `b` is the `noteId`. |
|
||||
| `noteId` | Text | Non-null | | The ID of the [note](notes.md). |
|
||||
| `parentNoteId` | Text | Non-null | | The ID of the parent [note](notes.md) the note belongs to. |
|
||||
| `notePosition` | Integer | Non-null | | The position of the branch within the same level of hierarchy, the value is usually a multiple of 10. |
|
||||
| `prefix` | Text | Nullable | | The [branch prefix](../../../Concepts/Branch%20prefixes.md) if any, or `NULL` otherwise. |
|
||||
| `isExpanded` | Integer | Non-null | 0 | Whether the branch should appear expanded (its children shown) to the user. |
|
||||
| `isDeleted` | Integer | Non-null | 0 | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||
| `deleteId` | Text | Nullable | `null` | |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
@@ -1,15 +1,15 @@
|
||||
# entity_changes
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `id` | Integer | Non-null | | A sequential numeric index of the entity change. |
|
||||
| `entityName` | Text | Non-null | | The type of entity being changed (`attributes`, `branches`, `note_reordering`, etc.) |
|
||||
| `entityId` | Text | Non-null | | The ID of the entity being changed. |
|
||||
| `hash` | Text | Nullable (\*) | | TODO: Describe how the hash is calculated |
|
||||
| `isErased` | Integer (1 or 0) | Nullable (\*) | | TODO: What does this do? |
|
||||
| `changeId` | Text | Nullable (\*) | | TODO: What does this do? |
|
||||
| `componentId` | Text | Nullable (\*) | | The ID of the UI component that caused this change. <br> <br>Examples: `date-note`, `F-PoZMI0vc`, `NA` (catch all) |
|
||||
| `instanceId` | Text | Nullable (\*) | | The ID of the [instance](#root/pOsGYCXsbNQG/tC7s2alapj8V/Gzjqa934BdH4/c5xB8m4g2IY6) that created this change. |
|
||||
| `isSynced` | Integer (1 or 0) | Non-null | | TODO: What does this do? |
|
||||
| `utcDateChanged` | Text | Non-null | | Date of the entity change in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `id` | Integer | Non-null | | A sequential numeric index of the entity change. |
|
||||
| `entityName` | Text | Non-null | | The type of entity being changed (`attributes`, `branches`, `note_reordering`, etc.) |
|
||||
| `entityId` | Text | Non-null | | The ID of the entity being changed. |
|
||||
| `hash` | Text | Nullable (\*) | | TODO: Describe how the hash is calculated |
|
||||
| `isErased` | Integer (1 or 0) | Nullable (\*) | | TODO: What does this do? |
|
||||
| `changeId` | Text | Nullable (\*) | | TODO: What does this do? |
|
||||
| `componentId` | Text | Nullable (\*) | | The ID of the UI component that caused this change. <br> <br>Examples: `date-note`, `F-PoZMI0vc`, `NA` (catch all) |
|
||||
| `instanceId` | Text | Nullable (\*) | | The ID of the [instance](#root/pOsGYCXsbNQG/tC7s2alapj8V/Gzjqa934BdH4/c5xB8m4g2IY6) that created this change. |
|
||||
| `isSynced` | Integer (1 or 0) | Non-null | | TODO: What does this do? |
|
||||
| `utcDateChanged` | Text | Non-null | | Date of the entity change in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
|
||||
Nullable (\*) means all new values are non-null, old rows may contain null values.
|
||||
@@ -1,9 +1,9 @@
|
||||
# etapi_tokens
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `etapiTokenId` | Text | Non-null | | A unique ID of the token (e.g. `aHmLr5BywvfJ`). |
|
||||
| `name` | Text | Non-null | | The name of the token, as is set by the user. |
|
||||
| `tokenHash` | Text | Non-null | | The token itself. |
|
||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `isDeleted` | Integer | Non-null | 0 | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||
| `etapiTokenId` | Text | Non-null | | A unique ID of the token (e.g. `aHmLr5BywvfJ`). |
|
||||
| `name` | Text | Non-null | | The name of the token, as is set by the user. |
|
||||
| `tokenHash` | Text | Non-null | | The token itself. |
|
||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `isDeleted` | Integer | Non-null | 0 | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||
@@ -1,15 +1,15 @@
|
||||
# notes
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `noteId` | Text | Non-null | | The unique ID of the note (e.g. `2LJrKqIhr0Pe`). |
|
||||
| `noteId` | Text | Non-null | | The unique ID of the note (e.g. `2LJrKqIhr0Pe`). |
|
||||
| `title` | Text | Non-null | `"note"` | The title of the note, as defined by the user. |
|
||||
| `isProtected` | Integer | Non-null | 0 | `1` if the entity is [protected](../../../Concepts/Protected%20entities.md), `0` otherwise. |
|
||||
| `isProtected` | Integer | Non-null | 0 | `1` if the entity is [protected](../../../Concepts/Protected%20entities.md), `0` otherwise. |
|
||||
| `type` | Text | Non-null | `"text"` | The type of note (i.e. `text`, `file`, `code`, `relationMap`, `mermaid`, `canvas`). |
|
||||
| `mime` | Text | Non-null | `"text/html"` | The MIME type of the note (e.g. `text/html`).. Note that it can be an empty string in some circumstances, but not null. |
|
||||
| `blobId` | Text | Nullable | `null` | The corresponding ID from <a class="reference-link" href="blobs.md">blobs</a>. Although it can theoretically be `NULL`, haven't found any such note yet. |
|
||||
| `isDeleted` | Integer | Nullable | 0 | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||
| `deleteId` | Text | Non-null | `null` | |
|
||||
| `dateCreated` | Text | Non-null | | Localized creation date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
||||
| `dateModified` | Text | Non-null | | Localized modification date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `isDeleted` | Integer | Nullable | 0 | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||
| `deleteId` | Text | Non-null | `null` | |
|
||||
| `dateCreated` | Text | Non-null | | Localized creation date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
||||
| `dateModified` | Text | Non-null | | Localized modification date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
@@ -1,7 +1,7 @@
|
||||
# options
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `name` | Text | Non-null | | The name of option (e.g. `maxContentWidth`) |
|
||||
| `value` | Text | Non-null | | The value of the option. |
|
||||
| `isSynced` | Integer | Non-null | 0 | `0` if the option is not synchronized and thus can differ between clients, `1` if the option is synchronized. |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `name` | Text | Non-null | | The name of option (e.g. `maxContentWidth`) |
|
||||
| `value` | Text | Non-null | | The value of the option. |
|
||||
| `isSynced` | Integer | Non-null | 0 | `0` if the option is not synchronized and thus can differ between clients, `1` if the option is synchronized. |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
@@ -1,6 +1,6 @@
|
||||
# recent_notes
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `noteId` | Text | Non-null | | Unique ID of the note (e.g. `yRRTLlqTbGoZ`). |
|
||||
| `notePath` | Text | Non-null | | The path (IDs) to the [note](notes.md) from root to the note itself, separated by slashes. |
|
||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `noteId` | Text | Non-null | | Unique ID of the note (e.g. `yRRTLlqTbGoZ`). |
|
||||
| `notePath` | Text | Non-null | | The path (IDs) to the [note](notes.md) from root to the note itself, separated by slashes. |
|
||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
@@ -1,15 +1,15 @@
|
||||
# revisions
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `revisionId` | Text | Non-null | | Unique ID of the revision (e.g. `0GjgUqnEudI8`). |
|
||||
| `noteId` | Text | Non-null | | ID of the [note](notes.md) this revision belongs to. |
|
||||
| `revisionId` | Text | Non-null | | Unique ID of the revision (e.g. `0GjgUqnEudI8`). |
|
||||
| `noteId` | Text | Non-null | | ID of the [note](notes.md) this revision belongs to. |
|
||||
| `type` | Text | Non-null | `""` | The type of note (i.e. `text`, `file`, `code`, `relationMap`, `mermaid`, `canvas`). |
|
||||
| `mime` | Text | Non-null | `""` | The MIME type of the note (e.g. `text/html`). |
|
||||
| `title` | Text | Non-null | | The title of the note, as defined by the user. |
|
||||
| `isProtected` | Integer | Non-null | 0 | `1` if the entity is [protected](../../../Concepts/Protected%20entities.md), `0` otherwise. |
|
||||
| `title` | Text | Non-null | | The title of the note, as defined by the user. |
|
||||
| `isProtected` | Integer | Non-null | 0 | `1` if the entity is [protected](../../../Concepts/Protected%20entities.md), `0` otherwise. |
|
||||
| `blobId` | Text | Nullable | `null` | The corresponding ID from <a class="reference-link" href="blobs.md">blobs</a>. Although it can theoretically be `NULL`, haven't found any such note yet. |
|
||||
| `utcDateLastEdited` | Text | Non-null | | **Not sure how it differs from modification date.** |
|
||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `dateLastEdited` | Text | Non-null | | **Not sure how it differs from modification date.** |
|
||||
| `dateCreated` | Text | Non-null | | Localized creatino date (e.g. `2023-08-12 15:10:04.045+0300`) |
|
||||
| `utcDateLastEdited` | Text | Non-null | | **Not sure how it differs from modification date.** |
|
||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||
| `dateLastEdited` | Text | Non-null | | **Not sure how it differs from modification date.** |
|
||||
| `dateCreated` | Text | Non-null | | Localized creatino date (e.g. `2023-08-12 15:10:04.045+0300`) |
|
||||
@@ -3,6 +3,6 @@ Contains user sessions for authentication purposes. The table is almost a direct
|
||||
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `id` | Text | Non-null | | Unique, non-sequential ID of the session, directly as indicated by `express-session` |
|
||||
| `data` | Text | Non-null | | The session information, in stringified JSON format. |
|
||||
| `expires` | Integer | Non-null | | The expiration date of the session, extracted from the session information. Used to rapidly clean up expired sessions. |
|
||||
| `id` | Text | Non-null | | Unique, non-sequential ID of the session, directly as indicated by `express-session` |
|
||||
| `data` | Text | Non-null | | The session information, in stringified JSON format. |
|
||||
| `expires` | Integer | Non-null | | The expiration date of the session, extracted from the session information. Used to rapidly clean up expired sessions. |
|
||||
@@ -7,11 +7,11 @@ Relevant files:
|
||||
|
||||
| Column Name | Data Type | Nullity | Default value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `tmpID` | Integer | | | A sequential ID of the user. Since only one user is supported by Trilium, this value is always zero. |
|
||||
| `username` | Text | | | The user name as returned from the OAuth operation. |
|
||||
| `email` | Text | | | The email as returned from the OAuth operation. |
|
||||
| `userIDEncryptedDataKey` | Text | | | An encrypted hash of the user subject identifier from the OAuth operation. |
|
||||
| `userIDVerificationHash` | Text | | | A salted hash of the subject identifier from the OAuth operation. |
|
||||
| `salt` | Text | | | The verification salt. |
|
||||
| `derivedKey` | Text | | | A random secure token. |
|
||||
| `isSetup` | Text | | `"false"` | Indicates that the user has been saved (`"true"`). |
|
||||
| `tmpID` | Integer | | | A sequential ID of the user. Since only one user is supported by Trilium, this value is always zero. |
|
||||
| `username` | Text | | | The user name as returned from the OAuth operation. |
|
||||
| `email` | Text | | | The email as returned from the OAuth operation. |
|
||||
| `userIDEncryptedDataKey` | Text | | | An encrypted hash of the user subject identifier from the OAuth operation. |
|
||||
| `userIDVerificationHash` | Text | | | A salted hash of the subject identifier from the OAuth operation. |
|
||||
| `salt` | Text | | | The verification salt. |
|
||||
| `derivedKey` | Text | | | A random secure token. |
|
||||
| `isSetup` | Text | | `"false"` | Indicates that the user has been saved (`"true"`). |
|
||||
@@ -6,7 +6,7 @@
|
||||
| Affected file | Affected method | Changed in | Reason for change |
|
||||
| --- | --- | --- | --- |
|
||||
| `packages/ckeditor5-mention/src/mentionui.ts` | `createRegExp()` | `6db05043be24bacf9bd51ea46408232b01a1b232` (added back) | Allows triggering the autocomplete for labels and attributes in the attribute editor. |
|
||||
| `init()` | `55a63a1934efb9a520fcc2d69f3ce55ac22aca39` | Allows dismissing @-mention permanently after pressing ESC, otherwise it would automatically show up as soon as a space was entered. | |
|
||||
| `init()` | `55a63a1934efb9a520fcc2d69f3ce55ac22aca39` | Allows dismissing @-mention permanently after pressing ESC, otherwise it would automatically show up as soon as a space was entered. | |
|
||||
|
||||
## Checking the old repo
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Documentation
|
||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/7hmsAGuPacge/Documentation_image.png" width="205" height="162">
|
||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/2E07SO1IRJxo/Documentation_image.png" width="205" height="162">
|
||||
|
||||
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
|
||||
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.
|
||||
|
||||
2
docs/Release Notes/!!!meta.json
vendored
2
docs/Release Notes/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.102.1",
|
||||
"appVersion": "0.102.2",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
||||
41
docs/User Guide/!!!meta.json
vendored
41
docs/User Guide/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.102.1",
|
||||
"appVersion": "0.102.2",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
@@ -9351,6 +9351,41 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "oBo3iHIZnbG2",
|
||||
"notePath": [
|
||||
"pOsGYCXsbNQG",
|
||||
"KSZ04uQ2D1St",
|
||||
"iPIMuisry3hd",
|
||||
"oBo3iHIZnbG2"
|
||||
],
|
||||
"title": "Spell Check",
|
||||
"notePosition": 200,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "label",
|
||||
"name": "iconClass",
|
||||
"value": "bx bx-check-double",
|
||||
"isInheritable": false,
|
||||
"position": 30
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"name": "shareAlias",
|
||||
"value": "spellcheck",
|
||||
"isInheritable": false,
|
||||
"position": 40
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Spell Check.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "BFvAtE74rbP6",
|
||||
@@ -9361,7 +9396,7 @@
|
||||
"BFvAtE74rbP6"
|
||||
],
|
||||
"title": "Table of contents",
|
||||
"notePosition": 200,
|
||||
"notePosition": 210,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -9433,7 +9468,7 @@
|
||||
"NdowYOC1GFKS"
|
||||
],
|
||||
"title": "Tables",
|
||||
"notePosition": 210,
|
||||
"notePosition": 220,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
|
||||
2
docs/User Guide/User Guide.md
vendored
2
docs/User Guide/User Guide.md
vendored
@@ -15,7 +15,7 @@ Trilium is an open-source solution for note-taking and organizing a personal kno
|
||||
|
||||
* <a class="reference-link" href="User%20Guide/Installation%20%26%20Setup/Desktop%20Installation.md">Desktop Installation</a>
|
||||
* <a class="reference-link" href="User%20Guide/Installation%20%26%20Setup/Server%20Installation.md">Server Installation</a>
|
||||
* <a class="reference-link" href="User%20Guide/Scripting/Script%20API/Frontend%20API">Frontend API</a> or <a class="reference-link" href="User%20Guide/Scripting/Script%20API/Backend%20API.dat">[missing note]</a>
|
||||
* <a class="reference-link" href="User%20Guide/Scripting/Script%20API/Frontend%20API">Frontend API</a> or <a class="reference-link" href="User%20Guide/Scripting/Script%20API/Backend%20API.dat">Backend API</a>
|
||||
* [ETAPI reference](User%20Guide/Advanced%20Usage/ETAPI%20\(REST%20API\)/API%20Reference.dat)
|
||||
|
||||
## External links
|
||||
|
||||
@@ -87,4 +87,4 @@ Development versions are version pre-release versions, generally meant for testi
|
||||
|
||||
## Credits
|
||||
|
||||
Some parts of the code are based on the [Joplin Notes browser extension](https://github.com/laurent22/joplin/tree/master/Clipper).
|
||||
Some parts of the code are based on the [Joplin Notes browser extension](https://github.com/laurent22/joplin/tree/master/Clipper).
|
||||
69
docs/User Guide/User Guide/Note Types/Text/Spell Check.md
vendored
Normal file
69
docs/User Guide/User Guide/Note Types/Text/Spell Check.md
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
# Spell Check
|
||||
Trilium supports spell checking for your notes. How it works depends on whether you're using the **desktop application** (Electron) or accessing Trilium through a **web browser**.
|
||||
|
||||
## Desktop
|
||||
|
||||
The desktop app uses Chromium's built-in spellchecker. You can configure it from _Options_ → _Spell Check_.
|
||||
|
||||
### Enabling spell check
|
||||
|
||||
Toggle _Check spelling_ to enable or disable the spellchecker. A restart is required for changes to take effect — use the restart button at the bottom of the section.
|
||||
|
||||
### Choosing languages
|
||||
|
||||
When spell check is enabled, a _Spell Check Languages_ section appears listing all languages available on your system. Select one or more languages by checking the boxes. The spellchecker will accept words that are valid in _any_ of the selected languages.
|
||||
|
||||
The available languages depend on your operating system's installed language packs. For example, on Windows you can add languages through _Options_ → _Time & Language_ → _Language & Region_ → _Add a language_.
|
||||
|
||||
> [!NOTE]
|
||||
> The changes take effect only after restarting the application.
|
||||
|
||||
### Custom dictionary
|
||||
|
||||
> [!TIP]
|
||||
> This function is available starting with Trilium v0.103.0.
|
||||
|
||||
Words you add to the dictionary (e.g. via the right-click context menu → "Add to dictionary") are stored in a **synced note** inside Trilium. This means your custom dictionary automatically syncs across all your devices.
|
||||
|
||||
You can view and edit the dictionary directly from _Settings_ → _Spell Check_ → _Custom Dictionary_ → _Edit dictionary_. This opens the underlying note, which contains one word per line. You can add, remove, or modify entries as you like.
|
||||
|
||||
> [!NOTE]
|
||||
> Changes to the custom dictionary (whether from the editor or the context menu) take effect after restarting the application.
|
||||
|
||||
#### How the custom dictionary works
|
||||
|
||||
* When you right-click a misspelled word and choose "Add to dictionary", the word is saved both to Electron's local spellchecker and to the synced dictionary note.
|
||||
* On startup, Trilium loads all words from the dictionary note into the spellchecker session.
|
||||
* If Trilium detects words in Electron's local dictionary but the dictionary note is empty (e.g. on first use), it performs a **one-time import** of those words into the note.
|
||||
* Words that are in Electron's local dictionary but _not_ in the note (e.g. you removed them manually) are cleaned up from the local dictionary on startup.
|
||||
|
||||
#### Known limitations
|
||||
|
||||
On Windows and macOS, Electron delegates "Add to dictionary" to the operating system's user dictionary. This means:
|
||||
|
||||
* Words added via the context menu are also written to the OS-level dictionary (e.g. `%APPDATA%\Microsoft\Spelling\<language>\default.dic` on Windows).
|
||||
* **Removing a word** from the Trilium dictionary note prevents it from being loaded into the spellchecker on next startup, but does _not_ remove it from the OS dictionary. The word may still be accepted by the OS spellchecker until you remove it from the OS dictionary manually.
|
||||
|
||||
## Web browser
|
||||
|
||||
When accessing Trilium through a web browser, spell checking is handled entirely by the browser itself. Trilium does not control the browser's spellchecker — language selection, dictionaries, and all other settings are managed through your browser's preferences.
|
||||
|
||||
The Spell Check settings page in Trilium will indicate that these options apply only to desktop builds.
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### Do I need to restart after every change?
|
||||
|
||||
Yes. Spell check language selection and the custom dictionary are loaded once at startup. Any changes require a restart to take effect.
|
||||
|
||||
### Can I use multiple spell check languages at the same time?
|
||||
|
||||
Yes. Select as many languages as you need from the checklist. The spellchecker will accept words from any of the selected languages.
|
||||
|
||||
### My custom words disappeared after syncing to a new device — what happened?
|
||||
|
||||
On the first launch of a new device, Trilium may import existing local dictionary words into the note. If the note already has words from another device (via sync), those are preserved. Make sure sync completes before restarting the application on a new device.
|
||||
|
||||
### I removed a word from the dictionary note but it's still accepted
|
||||
|
||||
This is likely due to the OS-level dictionary retaining the word (see [Known limitations](#known-limitations) above). You can manually remove it from your operating system's user dictionary.
|
||||
Reference in New Issue
Block a user