Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/about-dialog-overhaul

This commit is contained in:
Adorian Doran
2026-04-18 20:36:13 +03:00
116 changed files with 2762 additions and 1070 deletions

View File

@@ -66,12 +66,20 @@ runs:
if: ${{ inputs.os == 'linux' }}
shell: ${{ inputs.shell }}
run: |
sudo apt-get update && sudo apt-get install rpm flatpak-builder elfutils
sudo apt-get update && sudo apt-get install rpm flatpak-builder elfutils libfuse2
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
FLATPAK_ARCH=$(if [[ ${{ inputs.arch }} = 'arm64' ]]; then echo 'aarch64'; else echo 'x86_64'; fi)
FLATPAK_VERSION='24.08'
flatpak install --user --no-deps --arch $FLATPAK_ARCH --assumeyes runtime/org.freedesktop.Platform/$FLATPAK_ARCH/$FLATPAK_VERSION runtime/org.freedesktop.Sdk/$FLATPAK_ARCH/$FLATPAK_VERSION org.electronjs.Electron2.BaseApp/$FLATPAK_ARCH/$FLATPAK_VERSION
- name: Install appimagetool
if: ${{ inputs.os == 'linux' }}
shell: ${{ inputs.shell }}
run: |
APPIMAGETOOL_ARCH=$(if [[ ${{ inputs.arch }} = 'arm64' ]]; then echo 'aarch64'; else echo 'x86_64'; fi)
wget -q "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${APPIMAGETOOL_ARCH}.AppImage" -O /usr/local/bin/appimagetool
chmod +x /usr/local/bin/appimagetool
- name: Update build info
shell: ${{ inputs.shell }}
run: pnpm run chore:update-build-info
@@ -90,6 +98,14 @@ runs:
TARGET_ARCH: ${{ inputs.arch }}
run: pnpm run --filter desktop electron-forge:make --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
- name: Build AppImage
if: ${{ inputs.os == 'linux' }}
shell: ${{ inputs.shell }}
env:
TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
APPIMAGE_EXTRACT_AND_RUN: "1"
run: bash apps/desktop/scripts/build-appimage.sh ${{ inputs.arch }}
# Add DMG signing step
- name: Sign DMG
if: inputs.os == 'macos'

View File

@@ -0,0 +1,44 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

50
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr *)'

2
.nvmrc
View File

@@ -1 +1 @@
24.14.1
24.15.0

View File

@@ -16,7 +16,7 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.33.0",
"devDependencies": {
"@redocly/cli": "2.26.0",
"@redocly/cli": "2.28.0",
"archiver": "7.0.1",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",

View File

@@ -52,6 +52,7 @@
"dompurify": "3.4.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.2",
"htmldiff-js": "1.0.5",
"i18next": "26.0.4",
"i18next-http-backend": "3.0.4",
"jquery": "4.0.0",
@@ -66,7 +67,7 @@
"mind-elixir": "5.10.0",
"panzoom": "9.4.4",
"preact": "10.29.1",
"react-i18next": "17.0.2",
"react-i18next": "17.0.3",
"react-window": "2.2.7",
"reveal.js": "6.0.1",
"rrule": "2.8.1",

View File

@@ -281,6 +281,7 @@ export type CommandMappings = {
backInNoteHistory: CommandData;
forwardInNoteHistory: CommandData;
forceSaveRevision: CommandData;
saveNamedRevision: CommandData;
scrollToActiveNote: CommandData;
quickSearch: CommandData;
collapseTree: CommandData;

View File

@@ -1,6 +1,7 @@
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
import bundleService from "../services/bundle.js";
import dialog from "../services/dialog.js";
import dateNoteService from "../services/date_notes.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
@@ -216,4 +217,21 @@ export default class Entrypoints extends Component {
toastService.showMessage(t("entrypoints.note-revision-created"));
}
async saveNamedRevisionCommand() {
const noteId = appContext.tabManager.getActiveContextNoteId();
if (!noteId) return;
const name = await dialog.prompt({
title: t("entrypoints.save-named-revision-title"),
message: t("entrypoints.save-named-revision-message"),
defaultValue: ""
});
// null means the user cancelled
if (name === null) return;
await server.post(`notes/${noteId}/revision`, { description: name || undefined });
toastService.showMessage(t("entrypoints.note-revision-created"));
}
}

View File

@@ -66,7 +66,15 @@ class FAttribute {
}
get isAutoLink() {
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
if (this.type === "relation") {
return ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
if (this.type === "label") {
return this.name === "internalBookmark";
}
return false;
}
get toString() {

View File

@@ -2,7 +2,6 @@
:root {
--print-font-size: 11pt;
--ck-content-color-image-caption-background: transparent !important;
}
@page {
@@ -11,9 +10,12 @@
html,
body {
--print-font-family: var(--detail-font-family, sans-serif);
width: 100%;
height: 100%;
color: black;
font-family: var(--print-font-family);
}
.note-list-widget.full-height,
@@ -26,6 +28,12 @@ body {
}
body[data-note-type="text"] .ck-content {
--ck-content-font-family: var(--print-font-family);
--ck-content-font-size: var(--print-font-size);
--ck-content-font-color: black;
--ck-content-line-height: 1.5;
--ck-content-color-image-caption-background: transparent;
font-size: var(--print-font-size);
text-align: justify;
}
@@ -154,4 +162,4 @@ span[style] {
.page-break::after {
display: none !important;
}
/* #endregion */
/* #endregion */

View File

@@ -31,6 +31,13 @@ async function main() {
if (!noteId) return;
await import("./print.css");
// Load the user's font preferences so that --detail-font-family is available.
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = "api/fonts";
document.head.appendChild(fontLink);
const note = await froca.getNote(noteId);
const bodyWrapper = document.createElement("div");
@@ -105,6 +112,9 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
// Check custom CSS.
await loadCustomCss(note);
// Wait for all fonts (including those from custom CSS) to finish loading.
await document.fonts.ready;
}
load().then(() => requestAnimationFrame(() => onReady({
@@ -130,6 +140,7 @@ function CollectionRenderer({ note, onReady, onProgressChanged }: RendererProps)
media="print"
onReady={async (data: PrintReport) => {
await loadCustomCss(note);
await document.fonts.ready;
onReady(data);
}}
onProgressChanged={onProgressChanged}

View File

@@ -7,6 +7,10 @@ async function renderAttribute(attribute: FAttribute, renderIsInheritable: boole
const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : "";
const $attr = $("<span>");
if (attribute.isAutoLink) {
return $attr;
}
if (attribute.type === "label") {
$attr.append(document.createTextNode(`#${attribute.name}${isInheritable}`));
@@ -15,9 +19,6 @@ async function renderAttribute(attribute: FAttribute, renderIsInheritable: boole
$attr.append(document.createTextNode(formatValue(attribute.value)));
}
} else if (attribute.type === "relation") {
if (attribute.isAutoLink) {
return $attr;
}
// when the relation has just been created, then it might not have a value
if (attribute.value) {

View File

@@ -60,6 +60,8 @@ export interface ViewScope {
*/
tocPreviousVisible?: boolean;
tocCollapsedHeadings?: Set<string>;
/** When set, scrolls to a bookmark anchor within the note after navigation. */
bookmark?: string;
}
interface CreateLinkOptions {
@@ -244,7 +246,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
hoistedNoteId = value;
} else if (name === "searchString") {
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
} else if (["viewMode", "attachmentId"].includes(name)) {
} else if (["viewMode", "attachmentId", "bookmark"].includes(name)) {
(viewScope as any)[name] = value;
} else if (name === "popup") {
openInPopup = true;
@@ -432,6 +434,13 @@ async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | n
const title = await getReferenceLinkTitle(href);
$el.text(title);
if (viewScope?.bookmark) {
$el.append($("<small>").append(
$("<span>").addClass("bx bx-bookmark"),
document.createTextNode(viewScope.bookmark)
));
}
if (note) {
const icon = await getLinkIcon(noteId, viewScope.viewMode);
@@ -457,8 +466,8 @@ async function getReferenceLinkTitle(href: string) {
return attachment ? attachment.title : "[missing attachment]";
}
return note.title;
return note.title;
}
function getReferenceLinkTitleSync(href: string) {
@@ -481,8 +490,12 @@ function getReferenceLinkTitleSync(href: string) {
return attachment ? attachment.title : "[missing attachment]";
}
return note.title;
if (viewScope?.bookmark) {
return `${note.title} - ${viewScope.bookmark}`;
}
return note.title;
}
if (glob.device !== "print") {

View File

@@ -835,6 +835,7 @@ table.promoted-attributes-in-tooltip th {
text-align: start;
color: var(--main-text-color) !important;
max-width: 500px;
white-space: pre-line;
box-shadow: 10px 10px 93px -25px #aaaaaa;
}
@@ -960,15 +961,19 @@ table.promoted-attributes-in-tooltip th {
background-color: var(--active-item-background-color);
}
.help-button {
.help-button,
.custom-title-bar-button {
float: inline-end;
background: none;
font-weight: 900;
color: orange;
border: 0;
cursor: pointer;
}
.help-button {
color: orange;
}
.multiplicity {
font-size: 1.3em;
}
@@ -1147,11 +1152,90 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */
}
.modal-header .help-button {
.modal-header .help-button,
.modal-header .custom-title-bar-button {
padding: 6px;
margin: 0 12px;
}
.modal-content-with-sidebar {
flex-direction: row !important;
}
.modal-content-with-sidebar > .modal-sidebar {
display: flex;
flex-direction: column;
border-right: 1px solid var(--main-border-color);
flex-shrink: 0;
min-height: 0;
}
.modal-content-with-sidebar .modal-sidebar-header {
padding: 0.75rem 1rem;
flex-shrink: 0;
text-align: center;
border-bottom: 1px solid var(--main-border-color);
}
.modal-content-with-sidebar .modal-sidebar-header h5 {
margin: 0;
font-size: 1em;
}
.modal-content-with-sidebar > .modal-main > .modal-header > .modal-title {
visibility: hidden;
flex-grow: 1;
width: 0;
padding: 0;
margin: 0;
overflow: hidden;
}
.modal-content-with-sidebar > .modal-main > .modal-header {
flex-wrap: nowrap;
}
.modal-content-with-sidebar > .modal-main {
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.modal-content-with-sidebar > .modal-main > .modal-body {
overflow: auto;
flex-grow: 1;
min-height: 0;
}
body.mobile .modal-content-with-sidebar {
flex-direction: column !important;
}
body.mobile .modal-content-with-sidebar > .modal-sidebar {
border-right: none;
border-bottom: 1px solid var(--main-border-color);
height: 30vh;
flex-shrink: 0;
overflow: hidden;
order: 1;
}
body.mobile .modal-content-with-sidebar > .modal-main {
order: 0;
}
body.mobile .modal-content-with-sidebar .modal-sidebar-header {
display: none;
}
body.mobile .modal-content-with-sidebar > .modal-main > .modal-header > .modal-title {
visibility: visible;
width: auto;
}
.ck-mentions .ck-button {
font-size: var(--detail-font-size) !important;
padding: 5px;

View File

@@ -714,6 +714,15 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
text-decoration: underline;
}
.ck-content a.reference-link small {
margin-left: 0.25em;
opacity: 0.5;
>span {
font-size: 0.7em;
}
}
/*
* Read-only text content
*/

View File

@@ -51,6 +51,8 @@
"link_title_mirrors": "link title mirrors the note's current title",
"link_title_arbitrary": "link title can be changed arbitrarily",
"link_title": "Link title",
"anchor": "Anchor (optional)",
"anchor_none": "None (link to note)",
"button_add_link": "Add link"
},
"branch_prefix": {
@@ -295,6 +297,7 @@
"confirm_delete_all": "Do you want to delete all revisions of this note?",
"no_revisions": "No revisions for this note yet...",
"restore_button": "Restore",
"highlight_changes": "Highlight changes",
"diff_on": "Show diff",
"diff_off": "Show content",
"diff_on_hint": "Click to show note source diff",
@@ -308,11 +311,39 @@
"revision_deleted": "Note revision has been deleted.",
"snapshot_interval": "Note Revision Snapshot Interval: {{seconds}}s.",
"maximum_revisions": "Note Revision Snapshot Limit: {{number}}.",
"save_revision_now": "Save a revision now",
"save_named_revision": "Save named revision...",
"snapshot_header": "Note revision snapshot",
"snapshot_interval_value": "Interval: {{seconds}}s",
"snapshot_limit_value": "Limit: {{number}}",
"settings": "Note Revision Settings",
"menu_tooltip": "Revision options",
"download_button": "Download",
"mime": "MIME: ",
"file_size": "File size:",
"preview_not_available": "Preview isn't available for this note type."
"preview_not_available": "Preview isn't available for this note type.",
"save_revision": "Save revision",
"save_revision_tooltip": "Manually save a snapshot of the current note",
"description_placeholder": "Name this revision",
"revision_saved": "Note revision has been saved.",
"edit_description": "Edit name",
"description_updated": "Revision name has been updated.",
"source_auto": "Auto-save",
"source_manual": "Manual save",
"source_etapi": "ETAPI",
"source_llm": "LLM",
"source_restore": "Restore",
"source_unknown": "Snapshot",
"date_today": "Today",
"date_yesterday": "Yesterday",
"date_this_week": "This week",
"date_this_month": "This month",
"source_description_auto": "Automatically saved by the system at regular intervals",
"source_description_manual": "Manually saved by the user",
"source_description_etapi": "Created via the External Trilium API",
"source_description_llm": "Created by the AI assistant",
"source_description_restore": "Saved before restoring a previous revision",
"source_description_unknown": "Source not available"
},
"sort_child_notes": {
"sort_children_by": "Sort children by...",
@@ -726,6 +757,7 @@
"print_note": "Print note",
"view_revisions": "Note revisions...",
"save_revision": "Save revision",
"save_named_revision": "Save named revision...",
"advanced": "Advanced",
"convert_into_attachment_failed": "Converting note '{{title}}' failed.",
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
@@ -870,6 +902,9 @@
"no_inherited_attributes": "No inherited attributes.",
"none": "none"
},
"auto_link_attribute_list": {
"title": "System Attributes"
},
"note_info_widget": {
"note_id": "Note ID",
"created": "Created",
@@ -1902,6 +1937,8 @@
},
"entrypoints": {
"note-revision-created": "Note revision has been created.",
"save-named-revision-title": "Save named revision",
"save-named-revision-message": "Enter a name for this revision (leave empty for no name):",
"note-executed": "Note executed.",
"sql-error": "Error occurred while executing SQL query: {{message}}"
},
@@ -2512,5 +2549,9 @@
"move_note": "Move note",
"clone_note": "Clone note"
}
},
"common": {
"save": "Save",
"cancel": "Cancel"
}
}

View File

@@ -615,7 +615,8 @@
"collections": "コレクション",
"ai-chat": "AI チャット",
"spreadsheet": "スプレッドシート",
"llm-chat": "AI チャット"
"llm-chat": "AI チャット",
"markdown": "Markdown"
},
"edited_notes": {
"no_edited_notes_found": "この日の編集されたノートはまだありません...",
@@ -2479,5 +2480,10 @@
},
"launcher_button_context_menu": {
"remove_from_launch_bar": "ランチャーバーから削除"
},
"display_mode": {
"source": "ソースビュー",
"split": "分割ビュー",
"preview": "プレビュー"
}
}

View File

@@ -89,13 +89,21 @@
},
"delete_notes": {
"delete_all_clones_description": "同時刪除所有克隆(可以在最近修改中撤消)",
"erase_notes_description": "通常(軟)刪除僅標記筆記為已刪除,可以在一段時間內透過最近修改對話方塊撤消。勾選此選項將立即擦除筆記,無法撤銷。",
"erase_notes_description": "立即刪除筆記,而非執行軟刪除。此操作無法撤銷,且會強制重新載入應用程式。",
"erase_notes_warning": "永久擦除筆記(無法撤銷),包括所有克隆。這將強制應用程式重新載入。",
"notes_to_be_deleted": "刪除以下筆記 ({{notesCount}})",
"notes_to_be_deleted": "刪除筆記 ({{notesCount}})",
"no_note_to_delete": "沒有筆記將被刪除(僅克隆)。",
"broken_relations_to_be_deleted": "將刪除以下關聯並斷開連接 ({{ relationCount}})",
"broken_relations_to_be_deleted": "斷開的關聯 ({{ relationCount}})",
"cancel": "取消",
"close": "關閉"
"close": "關閉",
"title": "刪除筆記",
"clones_label": "克隆",
"delete_clones_description_one": "同時刪除 {{count}} 個其他克隆。此操作可在最近修改中撤銷。",
"erase_notes_label": "永久擦除",
"table_note_with_relation": "有關聯的筆記",
"table_relation": "關聯",
"table_points_to": "指向 (已刪除)",
"delete": "刪除"
},
"export": {
"export_note_title": "匯出筆記",
@@ -206,7 +214,8 @@
"box_size_small": "小型(顯示大約 10 行)",
"box_size_medium": "中型 (顯示大約30行)",
"box_size_full": "完整顯示(完整文字框)",
"button_include": "內嵌筆記"
"button_include": "內嵌筆記",
"box_size_expandable": "可展開(預設為摺疊狀態)"
},
"info": {
"modalTitle": "資訊消息",
@@ -1430,7 +1439,7 @@
"expand-subtree": "展開子階層",
"collapse-subtree": "收摺子階層",
"sort-by": "排序方式…",
"recent-changes-in-subtree": "子階層中的最近改",
"recent-changes-in-subtree": "子階層中的最近改",
"convert-to-attachment": "轉換為附件",
"copy-note-path-to-clipboard": "複製筆記路徑至剪貼簿",
"protect-subtree": "保護子階層",
@@ -2334,5 +2343,14 @@
"history": "對話歷史",
"recent_chats": "最近的對話",
"no_chats": "無先前的對話記錄"
},
"revisions": {
"note_revisions": "筆記歷史版本",
"delete_all_revisions": "刪除此筆記的所有歷史版本",
"delete_all_button": "刪除所有歷史版本",
"help_title": "關於筆記歷史版本的說明",
"confirm_delete_all": "您要刪除此筆記的所有歷史版本嗎?",
"no_revisions": "尚無此筆記的歷史版本...",
"restore_button": "還原"
}
}

View File

@@ -269,6 +269,8 @@ declare namespace Fancytree {
lazy: boolean;
/** Alternative description used as hover banner */
tooltip: string;
/** `<li>` element wrapping this node. `null` if the node has not been rendered yet. */
li: HTMLLIElement | null;
/** Outer element of single nodes */
span: HTMLElement;
/** Outer element of single nodes for table extension */

View File

@@ -1,3 +1,10 @@
declare module "htmldiff-js" {
const HtmlDiff: {
execute(oldHtml: string, newHtml: string): string;
};
export default HtmlDiff;
}
// TODO: Use real @types/ but that one generates a lot of errors.
declare module "draggabilly" {
type DraggabillyEventData = {};

View File

@@ -5,6 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
import NoteAutocomplete from "../react/NoteAutocomplete";
import { useRef, useState, useEffect } from "preact/hooks";
import tree from "../../services/tree";
import froca from "../../services/froca";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import { logError } from "../../services/ws";
import FormGroup from "../react/FormGroup.js";
@@ -24,6 +25,9 @@ export default function AddLinkDialog() {
const [ linkTitle, setLinkTitle ] = useState("");
const [ linkType, setLinkType ] = useState<LinkType>();
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
const [ bookmarks, setBookmarks ] = useState<string[]>([]);
const [ selectedBookmark, setSelectedBookmark ] = useState("");
const [ noteTitle, setNoteTitle ] = useState("");
const [ shown, setShown ] = useState(false);
const hasSubmittedRef = useRef(false);
@@ -41,26 +45,34 @@ export default function AddLinkDialog() {
}, [ opts ]);
async function setDefaultLinkTitle(noteId: string) {
const noteTitle = await tree.getNoteTitle(noteId);
setLinkTitle(noteTitle);
}
function resetExternalLink() {
if (linkType === "external-link") {
setLinkType("reference-link");
}
const title = await tree.getNoteTitle(noteId);
setNoteTitle(title);
setLinkTitle(title);
}
useEffect(() => {
const resetExternalLink = () =>
setLinkType((prev) => prev === "external-link" ? "reference-link" : prev);
if (!suggestion) {
resetExternalLink();
setBookmarks([]);
setSelectedBookmark("");
return;
}
let cancelled = false;
if (suggestion.notePath) {
const noteId = tree.getNoteIdFromUrl(suggestion.notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
froca.getNote(noteId).then((note) => {
if (cancelled) return;
const bkms = note?.getLabels("internalBookmark").map((l) => l.value) ?? [];
setBookmarks(bkms);
setSelectedBookmark("");
});
}
resetExternalLink();
}
@@ -69,8 +81,18 @@ export default function AddLinkDialog() {
setLinkTitle(suggestion.externalLink);
setLinkType("external-link");
}
return () => { cancelled = true; };
}, [suggestion]);
useEffect(() => {
if (selectedBookmark) {
setLinkTitle(`${noteTitle} - ${selectedBookmark}`);
} else {
setLinkTitle(noteTitle);
}
}, [selectedBookmark, noteTitle]);
function onShown() {
const $autocompleteEl = refToJQuerySelector(autocompleteRef);
if (!opts?.text) {
@@ -114,8 +136,11 @@ export default function AddLinkDialog() {
hasSubmittedRef.current = false;
if (suggestion.notePath) {
// Handle note link
opts.addLink(suggestion.notePath, linkType === "reference-link" ? null : linkTitle);
// Handle note link, optionally with a bookmark anchor
const path = selectedBookmark
? `${suggestion.notePath}?bookmark=${encodeURIComponent(selectedBookmark)}`
: suggestion.notePath;
opts.addLink(path, linkType === "reference-link" ? null : linkTitle);
} else if (suggestion.externalLink) {
// Handle external link
opts.addLink(suggestion.externalLink, linkTitle, true);
@@ -123,6 +148,9 @@ export default function AddLinkDialog() {
}
setSuggestion(null);
setBookmarks([]);
setSelectedBookmark("");
setNoteTitle("");
setShown(false);
}}
show={shown}
@@ -138,6 +166,21 @@ export default function AddLinkDialog() {
/>
</FormGroup>
{bookmarks.length > 0 && (
<FormGroup label={t("add_link.anchor")} name="anchor">
<select
className="form-select"
value={selectedBookmark}
onChange={(e) => setSelectedBookmark((e.target as HTMLSelectElement).value)}
>
<option value="">{t("add_link.anchor_none")}</option>
{bookmarks.map((bk) => (
<option key={bk} value={bk}>{bk}</option>
))}
</select>
</FormGroup>
)}
{!opts?.hasSelection && (
<div className="add-link-title-settings">
{(linkType !== "external-link") && (

View File

@@ -3,13 +3,14 @@ import { t } from "../../services/i18n";
import FormGroup from "../react/FormGroup";
import NoteAutocomplete from "../react/NoteAutocomplete";
import FormList, { FormListHeader, FormListItem } from "../react/FormList";
import { useEffect, useState } from "preact/hooks";
import { useEffect, useRef, useState } from "preact/hooks";
import note_types from "../../services/note_types";
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
import { TreeCommandNames } from "../../menus/tree_context_menu";
import { Suggestion } from "../../services/note_autocomplete";
import SimpleBadge from "../react/Badge";
import { useTriliumEvent } from "../react/hooks";
import { refToJQuerySelector } from "../react/react_utils";
export interface ChooseNoteTypeResponse {
success: boolean;
@@ -30,6 +31,8 @@ export default function NoteTypeChooserDialogComponent() {
const [ shown, setShown ] = useState(false);
const [ parentNote, setParentNote ] = useState<Suggestion | null>();
const [ noteTypes, setNoteTypes ] = useState<MenuItem<TreeCommandNames>[]>([]);
const modalRef = useRef<HTMLDivElement>(null);
const autocompleteRef = useRef<HTMLInputElement>(null);
useTriliumEvent("chooseNoteType", ({ callback }) => {
setCallback(() => callback);
@@ -68,11 +71,17 @@ export default function NoteTypeChooserDialogComponent() {
return (
<Modal
modalRef={modalRef}
title={t("note_type_chooser.modal_title")}
className="note-type-chooser-dialog"
size="md"
zIndex={1100} // note type chooser needs to be higher than other dialogs from which it is triggered, e.g. "add link"
scrollable
onShown={() => {
refToJQuerySelector(autocompleteRef)
.trigger("focus")
.trigger("select");
}}
onHidden={() => {
callback?.({ success: false });
setShown(false);
@@ -82,6 +91,7 @@ export default function NoteTypeChooserDialogComponent() {
>
<FormGroup name="parent-note" label={t("note_type_chooser.change_path_prompt")}>
<NoteAutocomplete
inputRef={autocompleteRef}
onChange={setParentNote}
placeholder={t("note_type_chooser.search_placeholder")}
opts={{

View File

@@ -3,74 +3,209 @@ body.mobile .revisions-dialog {
height: 95vh;
}
.modal-header {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
font-size: 0.9em;
}
.modal-title {
flex-grow: 1;
width: 100%;
}
.modal-body {
height: fit-content !important;
flex-direction: column;
padding: 0;
}
.modal-footer {
font-size: 0.9em;
}
.revision-list {
height: fit-content !important;
max-height: 20vh;
border-bottom: 1px solid var(--main-border-color) !important;
padding: 0 1em;
flex-shrink: 0;
}
.modal-body > .revision-content-wrapper {
flex-grow: 1;
max-width: unset !important;
height: 100%;
margin: 0;
display: block !important;
}
.modal-body > .revision-content-wrapper > div:first-of-type {
flex-direction: column;
}
.revision-title {
font-size: 1rem;
}
.revision-title-buttons {
text-align: center;
display: flex;
gap: 0.25em;
.revision-toolbar-actions {
flex-wrap: wrap;
}
.revision-content {
padding: 0.5em;
height: fit-content;
}
}
body.desktop .revisions-dialog {
.revision-list {
width: 300px;
}
.modal-content-with-sidebar {
height: 80vh;
}
}
.revisions-dialog {
.revision-title-buttons {
.modal-body {
padding: 0;
display: flex;
flex-direction: column;
}
.modal-sidebar {
background-color: var(--card-background-color);
}
.modal-sidebar .dropdown-menu.static {
background-color: transparent !important;
border-radius: 0 !important;
}
.revision-toolbar {
flex-shrink: 0;
border-bottom: 1px solid var(--main-border-color);
padding: 8px 20px;
}
.revision-title {
font-size: 1.2em;
margin: 8px 0;
}
.revision-toolbar-actions {
display: flex;
align-items: center;
gap: 4px;
}
.revision-menu-header {
font-weight: bold;
font-size: 0.85em;
text-transform: uppercase;
opacity: 0.6;
}
.revision-content-wrapper {
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
}
.revision-content {
flex-grow: 1;
min-height: 0;
overflow: auto;
padding: 20px;
}
.no-items {
padding-block: 3em;
}
.revision-list {
flex: 1 1 0;
min-height: 0;
overflow: auto;
.dropdown-item {
min-height: 2.5em;
>div {
padding-left: 0.25em;
min-width: 0;
}
}
}
.revision-item-description {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.85em;
opacity: 0.7;
}
.revision-group-header {
font-size: 0.75em;
font-weight: bold;
text-transform: uppercase;
opacity: 0.5;
padding: 6px 12px 2px;
}
.revision-item-meta {
font-size: 0.85em;
opacity: 0.7;
}
.revision-description-icon {
opacity: 0.5;
flex-shrink: 0;
}
.revision-description-editor {
display: flex;
gap: 5px;
align-items: center;
margin: 3px 0;
input {
flex-grow: 1;
}
}
.revision-description-display {
display: flex;
align-items: center;
margin: 3px 0;
gap: 5px;
min-height: 24px;
}
.revision-description-text {
font-size: 0.9em;
&.empty {
opacity: 0.5;
font-style: italic;
}
}
.revision-diff-code {
font-family: var(--font-family-monospace, monospace);
font-size: 0.9rem;
white-space: pre-wrap;
word-break: break-all;
max-width: 100%;
padding: 0;
}
/* HTML diff styles (htmldiff-js) */
.revision-diff-content {
ins {
text-decoration: none;
&.diffins,
&.diffmod {
background-color: color-mix(in srgb, var(--bs-success) 25%, transparent);
}
}
del {
text-decoration: line-through;
&.diffdel,
&.diffmod {
background-color: color-mix(in srgb, var(--bs-danger) 25%, transparent);
}
}
/* Image diff styles */
ins img,
del img {
border: 3px solid;
border-radius: 4px;
position: relative;
}
del img {
border-color: var(--bs-danger);
opacity: 0.6;
}
ins img {
border-color: var(--bs-success);
}
}
.revision-content.type-file {
display: flex;
min-width: 0;

View File

@@ -1,8 +1,10 @@
import "./revisions.css";
import type { RevisionItem, RevisionPojo } from "@triliumnext/commons";
import { dayjs, type RevisionItem, type RevisionPojo } from "@triliumnext/commons";
import clsx from "clsx";
import { diffWords } from "diff";
import HtmlDiff from "htmldiff-js";
import { Fragment } from "preact";
import type { CSSProperties } from "preact/compat";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
@@ -20,11 +22,13 @@ import toast from "../../services/toast";
import utils from "../../services/utils";
import ActionButton from "../react/ActionButton";
import Button from "../react/Button";
import FormList, { FormListItem } from "../react/FormList";
import Dropdown from "../react/Dropdown";
import FormList, { FormDropdownDivider, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import { useTriliumEvent } from "../react/hooks";
import Modal from "../react/Modal";
import { RawHtmlBlock } from "../react/RawHtml";
import NoItems from "../react/NoItems";
import { RawHtmlBlock, SanitizedHtml } from "../react/RawHtml";
import PdfViewer from "../type_widgets/file/PdfViewer";
export default function RevisionsDialog() {
@@ -33,7 +37,7 @@ export default function RevisionsDialog() {
const [ revisions, setRevisions ] = useState<RevisionItem[]>();
const [ currentRevision, setCurrentRevision ] = useState<RevisionItem>();
const [ shown, setShown ] = useState(false);
const [ showDiff, setShowDiff ] = useState(false);
const [ showDiff, setShowDiff ] = useState(true);
const [ refreshCounter, setRefreshCounter ] = useState(0);
useTriliumEvent("showRevisions", async ({ noteId }) => {
@@ -54,114 +58,390 @@ export default function RevisionsDialog() {
}
}, [ note, refreshCounter ]);
const revisionsLoaded = revisions !== undefined;
const hasRevisions = !!revisions?.length;
if (revisions?.length && !currentRevision) {
setCurrentRevision(revisions[0]);
}
const onHidden = () => {
setShown(false);
setShowDiff(true);
setNote(undefined);
setCurrentRevision(undefined);
setRevisions(undefined);
};
if (revisionsLoaded && !hasRevisions) {
return (
<Modal
className="revisions-dialog"
size="md"
title={t("revisions.note_revisions")}
helpPageId="vZWERwf8U3nx"
header={note && (
<RevisionsMenu
note={note}
onRevisionSaved={() => {
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
}}
onAllDeleted={() => {
setRevisions([]);
setCurrentRevision(undefined);
}}
hasRevisions={false}
/>
)}
onHidden={onHidden}
show={shown}
>
<NoItems icon="bx bx-history" text={t("revisions.no_revisions")} />
</Modal>
);
}
return (
<Modal
className="revisions-dialog"
size="xl"
title={t("revisions.note_revisions")}
helpPageId="vZWERwf8U3nx"
bodyStyle={{ display: "flex", height: "80vh" }}
header={
!!revisions?.length && (
<>
{["text", "code", "mermaid"].includes(currentRevision?.type ?? "") && (
<FormToggle
currentValue={showDiff}
onChange={(newValue) => setShowDiff(newValue)}
switchOnName={t("revisions.diff_on")}
switchOffName={t("revisions.diff_off")}
switchOnTooltip={t("revisions.diff_on_hint")}
switchOffTooltip={t("revisions.diff_off_hint")}
/>
)}
&nbsp;
<Button
text={t("revisions.delete_all_revisions")}
size="small"
style={{ padding: "0 10px" }}
onClick={async () => {
const text = t("revisions.confirm_delete_all");
if (note && await dialog.confirm(text)) {
await server.remove(`notes/${note.noteId}/revisions`);
setRevisions([]);
setCurrentRevision(undefined);
toast.showMessage(t("revisions.revisions_deleted"));
}
}}
/>
</>
)
header={note && (
<RevisionsMenu
note={note}
onRevisionSaved={() => {
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
}}
onAllDeleted={() => {
setRevisions([]);
setCurrentRevision(undefined);
}}
hasRevisions={true}
/>
)}
sidebar={
<RevisionsList
revisions={revisions ?? []}
onSelect={(revisionId) => {
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
if (correspondingRevision) {
setCurrentRevision(correspondingRevision);
}
}}
currentRevision={currentRevision}
/>
}
footer={<RevisionFooter note={note} />}
footerStyle={{ paddingTop: 0, paddingBottom: 0 }}
onHidden={() => {
setShown(false);
setShowDiff(false);
setNote(undefined);
setCurrentRevision(undefined);
setRevisions(undefined);
}}
onHidden={onHidden}
show={shown}
>
<RevisionsList
revisions={revisions ?? []}
onSelect={(revisionId) => {
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
if (correspondingRevision) {
setCurrentRevision(correspondingRevision);
<RevisionToolbar
revisionItem={currentRevision}
showDiff={showDiff}
setShowDiff={setShowDiff}
setShown={setShown}
onRevisionDeleted={() => {
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
}}
onDescriptionUpdated={(revisionId, description) => {
setRevisions(prev => prev?.map(r =>
r.revisionId === revisionId ? { ...r, description } : r
));
if (currentRevision?.revisionId === revisionId) {
setCurrentRevision({ ...currentRevision, description });
}
}}
currentRevision={currentRevision}
/>
<div className="revision-content-wrapper" style={{
flexGrow: "1",
marginInlineStart: "20px",
display: "flex",
flexDirection: "column",
maxWidth: "calc(100% - 150px)",
minWidth: 0
}}>
<div className="revision-content-wrapper">
<RevisionPreview
noteContent={noteContent}
revisionItem={currentRevision}
showDiff={showDiff}
setShown={setShown}
onRevisionDeleted={() => {
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
}} />
/>
</div>
</Modal>
);
}
function RevisionsMenu({ note, onRevisionSaved, onAllDeleted, hasRevisions }: {
note: FNote,
onRevisionSaved: () => void,
onAllDeleted: () => void,
hasRevisions: boolean
}) {
let revisionsNumberLimit: number | string = parseInt(note.getLabelValue("versioningLimit") ?? "", 10);
if (!Number.isInteger(revisionsNumberLimit)) {
revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0;
}
if (revisionsNumberLimit === -1) {
revisionsNumberLimit = "∞";
}
return (
<Dropdown
text={<span className="bx bx-dots-horizontal-rounded" />}
hideToggleArrow
buttonClassName="custom-title-bar-button"
noSelectButtonStyle
buttonProps={{ title: t("revisions.menu_tooltip") }}
dropdownContainerClassName="mobile-bottom-menu"
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
>
<FormListItem
icon="bx bx-save"
onClick={async () => {
await server.post(`notes/${note.noteId}/revision`);
toast.showMessage(t("revisions.revision_saved"));
onRevisionSaved();
}}
>
{t("revisions.save_revision_now")}
</FormListItem>
<FormListItem
icon="bx bx-purchase-tag"
onClick={async () => {
const name = await dialog.prompt({
title: t("entrypoints.save-named-revision-title"),
message: t("entrypoints.save-named-revision-message"),
defaultValue: ""
});
if (name === null) return;
await server.post(`notes/${note.noteId}/revision`, { description: name || undefined });
toast.showMessage(t("revisions.revision_saved"));
onRevisionSaved();
}}
>
{t("revisions.save_named_revision")}
</FormListItem>
<FormDropdownDivider />
<FormListItem disabled className="revision-menu-header">
{t("revisions.snapshot_header")}
</FormListItem>
<FormListItem disabled>
{t("revisions.snapshot_interval_value", { seconds: options.getInt("revisionSnapshotTimeInterval") })}
</FormListItem>
<FormListItem disabled>
{t("revisions.snapshot_limit_value", { number: revisionsNumberLimit })}
</FormListItem>
<FormListItem
icon="bx bx-cog"
onClick={() => appContext.tabManager.openContextWithNote("_optionsOther", { activate: true })}
>
{t("revisions.settings")}
</FormListItem>
{hasRevisions && (
<>
<FormDropdownDivider />
<FormListItem
icon="bx bx-trash"
onClick={async () => {
if (await dialog.confirm(t("revisions.confirm_delete_all"))) {
await server.remove(`notes/${note.noteId}/revisions`);
onAllDeleted();
toast.showMessage(t("revisions.revisions_deleted"));
}
}}
>
{t("revisions.delete_all_revisions")}
</FormListItem>
</>
)}
</Dropdown>
);
}
const REVISION_SOURCE_ICONS: Record<string, string> = {
auto: "bx bx-time-five",
manual: "bx bx-save",
etapi: "bx bx-code-alt",
llm: "bx bx-bot",
restore: "bx bx-history"
};
const DEFAULT_REVISION_ICON = "bx bx-file";
function getRevisionSourceTitle(source?: string): string {
return t(`revisions.source_description_${source ?? "unknown"}`);
}
type DateGroup = "today" | "yesterday" | "this_week" | "this_month" | "older";
function getDateGroup(dateStr: string): DateGroup {
const date = dayjs(dateStr);
const now = dayjs();
if (date.isSame(now, "day")) return "today";
if (date.isSame(now.subtract(1, "day"), "day")) return "yesterday";
if (date.isSame(now, "week")) return "this_week";
if (date.isSame(now, "month")) return "this_month";
return "older";
}
function getDateGroupLabel(group: DateGroup, dateStr: string): string {
if (group === "older") return dayjs(dateStr).format("MMMM YYYY");
return t(`revisions.date_${group}`);
}
function formatRevisionDate(dateStr: string, group: DateGroup): string {
const date = dayjs(dateStr);
switch (group) {
case "today":
case "yesterday":
return date.format("HH:mm");
case "this_week":
return date.format("dddd · HH:mm");
default:
return date.isSame(dayjs(), "year")
? date.format("MMM D · HH:mm")
: date.format("MMM D, YYYY · HH:mm");
}
}
function buildRevisionTooltip(item: RevisionItem): string {
const dateLine = item.dateCreated
? `${dayjs(item.dateCreated).format("YYYY-MM-DD HH:mm")} (${dayjs(item.dateCreated).fromNow()})`
: "";
return [
item.description,
getRevisionSourceTitle(item.source),
dateLine,
item.contentLength && utils.formatSize(item.contentLength)
].filter(Boolean).join("\n");
}
function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: RevisionItem[], onSelect: (val: string) => void, currentRevision?: RevisionItem }) {
let lastGroup: DateGroup | "" = "";
return (
<FormList onSelect={onSelect} fullHeight wrapperClassName="revision-list">
{revisions.map((item) =>
<FormListItem
key={item.revisionId}
value={item.revisionId}
active={currentRevision && item.revisionId === currentRevision.revisionId}
>
{item.dateCreated && item.dateCreated.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
</FormListItem>
)}
{revisions.map((item) => {
const group = item.dateCreated ? getDateGroup(item.dateCreated) : "" as DateGroup;
const showHeader = group !== lastGroup;
lastGroup = group;
return (
<Fragment key={item.revisionId}>
{showHeader && (
<div className="revision-group-header">{item.dateCreated ? getDateGroupLabel(group, item.dateCreated) : ""}</div>
)}
<FormListItem
key={item.revisionId}
value={item.revisionId}
icon={REVISION_SOURCE_ICONS[item.source ?? ""] ?? DEFAULT_REVISION_ICON}
title={buildRevisionTooltip(item)}
active={currentRevision && item.revisionId === currentRevision.revisionId}
>
<div>
<div className="revision-item-date">
{item.dateCreated && formatRevisionDate(item.dateCreated, group)}
</div>
{item.description && (
<div className="revision-item-description">
{item.description}
</div>
)}
</div>
</FormListItem>
</Fragment>
);
})}
</FormList>);
}
function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevisionDeleted }: {
function RevisionToolbar({ revisionItem, showDiff, setShowDiff, setShown, onRevisionDeleted, onDescriptionUpdated }: {
revisionItem?: RevisionItem,
showDiff: boolean,
setShowDiff: Dispatch<StateUpdater<boolean>>,
setShown: Dispatch<StateUpdater<boolean>>,
onRevisionDeleted?: () => void,
onDescriptionUpdated?: (revisionId: string, description: string) => void,
}) {
const canShowDiff = ["text", "code", "mermaid"].includes(revisionItem?.type ?? "");
const canInteract = revisionItem && (!revisionItem.isProtected || protected_session_holder.isProtectedSessionAvailable());
const [ editingDescription, setEditingDescription ] = useState(false);
const [ descriptionDraft, setDescriptionDraft ] = useState("");
useEffect(() => {
setEditingDescription(false);
}, [revisionItem]);
return (
<div className="revision-toolbar">
{revisionItem && (
<div className="revision-toolbar-actions">
{canShowDiff && (
<FormToggle
currentValue={showDiff}
onChange={(newValue) => setShowDiff(newValue)}
switchOnName={t("revisions.highlight_changes")}
switchOffName={t("revisions.highlight_changes")}
/>
)}
<div style="flex-grow: 1" />
{canInteract && (
<>
<ActionButton
icon="bx bx-trash"
text={t("revisions.delete_button")}
onClick={async () => {
if (await dialog.confirm(t("revisions.confirm_delete"))) {
await server.remove(`revisions/${revisionItem.revisionId}`);
toast.showMessage(t("revisions.revision_deleted"));
onRevisionDeleted?.();
}
}} frame />
<ActionButton
icon="bx bx-download"
text={t("revisions.download_button")}
onClick={() => {
if (revisionItem.revisionId) {
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId);
}
}}
frame />
<Button
icon="bx bx-history"
text={t("revisions.restore_button")}
onClick={async () => {
if (await dialog.confirm(t("revisions.confirm_restore"))) {
await server.post(`revisions/${revisionItem.revisionId}/restore`);
setShown(false);
toast.showMessage(t("revisions.revision_restored"));
}
}}/>
</>
)}
</div>
)}
{revisionItem && (
<RevisionDescription
revisionItem={revisionItem}
editing={editingDescription}
draft={descriptionDraft}
onEdit={() => {
setDescriptionDraft(revisionItem.description || "");
setEditingDescription(true);
}}
onDraftChange={setDescriptionDraft}
onSave={async () => {
await server.patch(`revisions/${revisionItem.revisionId}`, { description: descriptionDraft });
setEditingDescription(false);
toast.showMessage(t("revisions.description_updated"));
onDescriptionUpdated?.(revisionItem.revisionId!, descriptionDraft);
}}
onCancel={() => setEditingDescription(false)}
/>
)}
</div>
);
}
function RevisionPreview({noteContent, revisionItem, showDiff }: {
noteContent?: string,
revisionItem?: RevisionItem,
showDiff: boolean,
setShown: Dispatch<StateUpdater<boolean>>,
onRevisionDeleted?: () => void
}) {
const [ fullRevision, setFullRevision ] = useState<RevisionPojo>();
@@ -174,54 +454,60 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
}, [revisionItem]);
return (
<>
<div style="flex-grow: 0; display: flex; justify-content: space-between;">
<h3 className="revision-title" style="margin: 3px; flex-grow: 100;">{revisionItem?.title ?? t("revisions.no_revisions")}</h3>
{(revisionItem && <div className="revision-title-buttons">
{(!revisionItem.isProtected || protected_session_holder.isProtectedSessionAvailable()) &&
<>
<Button
icon="bx bx-history"
text={t("revisions.restore_button")}
onClick={async () => {
if (await dialog.confirm(t("revisions.confirm_restore"))) {
await server.post(`revisions/${revisionItem.revisionId}/restore`);
setShown(false);
toast.showMessage(t("revisions.revision_restored"));
}
}}/>
&nbsp;
<Button
icon="bx bx-trash"
text={t("revisions.delete_button")}
onClick={async () => {
if (await dialog.confirm(t("revisions.confirm_delete"))) {
await server.remove(`revisions/${revisionItem.revisionId}`);
toast.showMessage(t("revisions.revision_deleted"));
onRevisionDeleted?.();
}
}} />
&nbsp;
<Button
kind="primary"
icon="bx bx-download"
text={t("revisions.download_button")}
onClick={() => {
if (revisionItem.revisionId) {
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId);}
}
}/>
</>
}
</div>)}
<div
className={clsx("revision-content use-tn-links selectable-text", `type-${revisionItem?.type}`)}
style={{ wordBreak: "break-word" }}
>
<h3 className="revision-title">{revisionItem?.title}</h3>
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
</div>
);
}
function RevisionDescription({ revisionItem, editing, draft, onEdit, onDraftChange, onSave, onCancel }: {
revisionItem: RevisionItem,
editing: boolean,
draft: string,
onEdit: () => void,
onDraftChange: (val: string) => void,
onSave: () => void,
onCancel: () => void
}) {
if (editing) {
return (
<div className="revision-description-editor">
<span className="bx bx-purchase-tag revision-description-icon" />
<input
type="text"
className="form-control form-control-sm"
placeholder={t("revisions.description_placeholder")}
value={draft}
onInput={(e) => onDraftChange((e.target as HTMLInputElement).value)}
onKeyDown={(e) => {
if (e.key === "Enter") onSave();
if (e.key === "Escape") onCancel();
}}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
/>
<ActionButton icon="bx bx-check" text={t("common.save")} onClick={onSave} />
<ActionButton icon="bx bx-x" text={t("common.cancel")} onClick={onCancel} />
</div>
<div
className={clsx("revision-content use-tn-links selectable-text", `type-${revisionItem?.type}`)}
style={{ overflow: "auto", wordBreak: "break-word" }}
>
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
</div>
</>
);
}
return (
<div className="revision-description-display">
<span className="bx bx-purchase-tag revision-description-icon" />
<span className={clsx("revision-description-text", { empty: !revisionItem.description })}>
{revisionItem.description || t("revisions.description_placeholder")}
</span>
<ActionButton
icon="bx bx-edit-alt"
text={t("revisions.edit_description")}
onClick={onEdit}
/>
</div>
);
}
@@ -250,7 +536,7 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
case "text":
return <RevisionContentText content={content} />;
case "code":
return <pre style={CODE_STYLE}>{content}</pre>;
return <div className="revision-diff-code">{content}</div>;
case "image":
switch (revisionItem.mime) {
case "image/svg+xml": {
@@ -299,69 +585,33 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
itemContent: string | Buffer<ArrayBufferLike> | undefined,
itemType: string
}) {
const contentRef = useRef<HTMLDivElement>(null);
if (!noteContent || typeof itemContent !== "string") {
return <div className="revision-diff-content">{t("revisions.diff_not_available")}</div>;
}
useEffect(() => {
if (!noteContent || typeof itemContent !== "string") {
if (contentRef.current) {
contentRef.current.textContent = t("revisions.diff_not_available");
}
return;
}
let processedNoteContent = noteContent;
let processedItemContent = itemContent;
if (itemType === "text") {
processedNoteContent = utils.formatHtml(noteContent);
processedItemContent = utils.formatHtml(itemContent);
}
const diff = diffWords(processedNoteContent, processedItemContent);
const diffHtml = diff.map(part => {
let diffHtml: string;
if (itemType === "text") {
// Use proper HTML-aware diff for rich text content
diffHtml = HtmlDiff.execute(noteContent, itemContent);
} else {
// Use word diff for code/mermaid (plain text)
const diff = diffWords(noteContent, itemContent);
diffHtml = diff.map(part => {
if (part.added) {
return `<span class="revision-diff-added">${utils.escapeHtml(part.value)}</span>`;
} else if (part.removed) {
return `<span class="revision-diff-removed">${utils.escapeHtml(part.value)}</span>`;
}
return utils.escapeHtml(part.value);
}).join("");
}
if (contentRef.current) {
contentRef.current.innerHTML = diffHtml;
}
}, [noteContent, itemContent, itemType]);
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }} />;
return <SanitizedHtml
className={clsx("revision-diff-content", itemType === "text" ? "ck-content" : "revision-diff-code")}
html={diffHtml}
/>;
}
function RevisionFooter({ note }: { note?: FNote }) {
if (!note) {
return <></>;
}
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "", 10);
if (!Number.isInteger(revisionsNumberLimit)) {
revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0;
}
if (revisionsNumberLimit === -1) {
revisionsNumberLimit = "∞";
}
return <>
<span class="revisions-snapshot-interval flex-grow-1 my-0 py-0">
{t("revisions.snapshot_interval", { seconds: options.getInt("revisionSnapshotTimeInterval") })}
</span>
<span class="maximum-revisions-for-current-note flex-grow-1 my-0 py-0">
{t("revisions.maximum_revisions", { number: revisionsNumberLimit })}
</span>
<ActionButton
icon="bx bx-cog" text={t("revisions.settings")}
onClick={() => appContext.tabManager.openContextWithNote("_optionsOther", { activate: true })}
/>
</>;
}
function FilePreview({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
return (

View File

@@ -87,7 +87,8 @@
font-weight: 600;
}
.inherited-attributes-widget {
.inherited-attributes-widget,
.auto-link-attributes-widget {
display: inline;
> div {

View File

@@ -26,6 +26,7 @@ import LinkButton from "../react/LinkButton";
import { ParentComponent } from "../react/react_utils";
import { ContentLanguagesModal, NoteTypeCodeNoteList, NoteTypeOptionsModal, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor";
import AutoLinkAttributesTab from "../ribbon/AutoLinkAttributesTab";
import InheritedAttributesTab from "../ribbon/InheritedAttributesTab";
import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab";
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
@@ -401,6 +402,11 @@ function AttributesPane({ note, noteContext, attributesShown, setAttributesShown
<span class="attributes-panel-label">{t("inherited_attribute_list.title")}</span>
<InheritedAttributesTab {...context} emptyListString="inherited_attribute_list.none" />
{glob.isDev && <div>
<span class="attributes-panel-label">{t("auto_link_attribute_list.title")}</span>
<AutoLinkAttributesTab {...context} />
</div>}
<AttributeEditor
{...context}
api={api}

View File

@@ -1534,8 +1534,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const hoistedNotePath = await treeService.resolveNotePath(this.noteContext.hoistedNoteId);
if (!forceUpdate && this.lastFilteredHoistedNotePath === hoistedNotePath) {
// no need to re-filter if the hoisting did not change
// (helps with flickering on simple note change with large subtrees)
// Hoisting did not change, so skip the expensive re-filter (avoids flickering on
// simple note changes with large subtrees). The hidden-node class must still be
// reapplied — the <li> may have been recreated by a lazy reload (e.g. via
// `getNodeFromPath` → `parentNode.load(true)` after an import into root).
this.toggleHiddenNode(this.noteContext.hoistedNoteId !== "root");
return;
}
@@ -1568,8 +1571,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
toggleHiddenNode(show: boolean) {
const hiddenNode = this.getNodesByNoteId("_hidden")[0];
// TODO: Check how .li exists here.
$((hiddenNode as any).li).toggleClass("hidden-node-is-hidden", !show);
if (hiddenNode?.li) {
$(hiddenNode.li).toggleClass("hidden-node-is-hidden", !show);
}
}
async frocaReloadedEvent() {

View File

@@ -77,13 +77,19 @@ export interface ModalProps {
* If true, the modal will not focus itself after becoming visible.
*/
noFocus?: boolean;
/**
* Content to display as a full-height sidebar on the left side of the modal.
* When set, the modal layout switches to a horizontal split with the sidebar
* spanning the entire height alongside the header, body and footer.
*/
sidebar?: ComponentChildren;
/**
* Indicates if the dialog will be displayed as a full page on mobile devices.
*/
isFullPageOnMobile?: boolean;
}
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus, isFullPageOnMobile }: ModalProps) {
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus, sidebar, isFullPageOnMobile }: ModalProps) {
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
const modalInstanceRef = useRef<BootstrapModal>();
const elementToFocus = useRef<Element | null>();
@@ -147,48 +153,63 @@ export default function Modal({ children, className, size, title, customTitleBar
return (
<div className={`modal fade mx-auto ${className}`} tabIndex={-1} style={dialogStyle} role="dialog" ref={modalRef}>
{(show || keepInDom) && <div className={clsx("modal-dialog", `modal-${size}`, {"modal-dialog-scrollable": scrollable, "modal-dialog-full-page-on-mobile": isFullPageOnMobile})} style={documentStyle} role="document">
<div className="modal-content">
<div className="modal-header">
{!title || typeof title === "string" ? (
<h5 className="modal-title">{title ?? <>&nbsp;</>}</h5>
{(show || keepInDom) && <div className={clsx("modal-dialog", `modal-${size}`, {"modal-dialog-scrollable": scrollable, "modal-dialog-full-page-on-mobile": isFullPageOnMobile, "modal-content-with-sidebar": sidebar})} style={documentStyle} role="document">
<div className={clsx("modal-content", sidebar && "modal-content-with-sidebar")}>
{sidebar && <div className="modal-sidebar">
{title && <div className="modal-sidebar-header">
<h5>{title}</h5>
</div>}
{sidebar}
</div>}
<ModalMain sidebar={!!sidebar}>
<div className="modal-header">
{!title || typeof title === "string" ? (
<h5 className="modal-title">{title ?? <>&nbsp;</>}</h5>
) : (
title
)}
{header}
{helpPageId && (
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
)}
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
<button type="button"
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
title={titleBarButton.title}
onClick={titleBarButton.onClick} />
))}
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")} />
</div>
{onSubmit ? (
<form ref={formRef} onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}>
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>{children}</ModalInner>
</form>
) : (
title
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>
{children}
</ModalInner>
)}
{header}
{helpPageId && (
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
)}
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
<button type="button"
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
title={titleBarButton.title}
onClick={titleBarButton.onClick} />
))}
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")} />
</div>
{onSubmit ? (
<form ref={formRef} onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}>
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>{children}</ModalInner>
</form>
) : (
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>
{children}
</ModalInner>
)}
</ModalMain>
</div>
</div>}
</div>
);
}
function ModalMain({ sidebar, children }: { sidebar: boolean; children: ComponentChildren }) {
if (sidebar) {
return <div className="modal-main">{children}</div>;
}
return <>{children}</>;
}
function ModalInner({ children, footer, footerAlignment, bodyStyle, footerStyle: _footerStyle }: Pick<ModalProps, "children" | "footer" | "footerAlignment" | "bodyStyle" | "footerStyle">) {
// Memoize footer style
const footerStyle = useMemo<CSSProperties>(() => {

View File

@@ -0,0 +1,69 @@
import { useEffect, useState } from "preact/hooks";
import FAttribute from "../../entities/fattribute";
import attributes from "../../services/attributes";
import froca from "../../services/froca";
import { useTriliumEvent } from "../react/hooks";
import { joinElements } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
type AutoLinkAttributesTabArgs = Pick<TabContext, "note" | "componentId">;
export default function AutoLinkAttributesTab({ note, componentId }: AutoLinkAttributesTabArgs) {
const [autoLinkAttributes, setAutoLinkAttributes] = useState<FAttribute[]>();
function refresh() {
if (!note) return;
const attrs = note.getAttributes().filter((attr) => attr.isAutoLink && attr.noteId === note.noteId);
setAutoLinkAttributes(attrs);
}
useEffect(refresh, [note]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
refresh();
}
});
if (!autoLinkAttributes?.length) {
return null;
}
return (
<div className="auto-link-attributes-widget">
<div className="auto-link-attributes-container selectable-text">
{joinElements(autoLinkAttributes.map((attribute) => (
<AutoLinkAttribute key={attribute.attributeId} attribute={attribute} />
)), " ")}
</div>
</div>
);
}
function AutoLinkAttribute({ attribute }: { attribute: FAttribute }) {
const [html, setHtml] = useState<string>("");
useEffect(() => {
renderAutoLink(attribute).then(setHtml);
}, [attribute]);
return <span dangerouslySetInnerHTML={{ __html: html }} />;
}
async function renderAutoLink(attribute: FAttribute) {
if (attribute.type === "label") {
return `#${escapeHtml(attribute.name)}=${escapeHtml(attribute.value)}`;
}
const note = await froca.getNote(attribute.value);
if (!note) return "";
const link = `<a href="#root/${attribute.value}" class="reference-link">${escapeHtml(note.title)}</a>`;
return `~${escapeHtml(attribute.name)}=${link}`;
}
function escapeHtml(text: string) {
const el = document.createElement("span");
el.textContent = text;
return el.innerHTML;
}

View File

@@ -153,6 +153,7 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
<CommandItem command="showRevisions" icon="bx bx-history" text={t("note_actions.view_revisions")} />
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
<CommandItem command="saveNamedRevision" icon="bx bx-purchase-tag" disabled={isInOptionsOrHelp} text={t("note_actions.save_named_revision")} />
<FormDropdownDivider />

View File

@@ -61,6 +61,17 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
onContentChange(newContent) {
contentRef.current = newContent;
watchdogRef.current?.editor?.setData(newContent);
// Scroll to bookmark anchor if navigated with ?bookmark=...
const viewScope = noteContext?.viewScope;
if (viewScope?.bookmark) {
requestAnimationFrame(() => {
const el = watchdogRef.current?.editor?.editing.view.getDomRoot()
?.querySelector(`[id="${CSS.escape(viewScope.bookmark!)}"]`);
el?.scrollIntoView({ behavior: "smooth", block: "center" });
viewScope.bookmark = undefined;
});
}
},
dataSaved(savedData) {
// Store back the saved data in order to retrieve it in case the CKEditor crashes.
@@ -263,6 +274,7 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
// We are not using CKEditor's built-in watch dog content, instead we are using the data we store regularly in the spaced update (see `dataSaved`).
editor.setData(contentRef.current);
parentComponent?.triggerEvent("textEditorRefreshed", { ntxId, editor });
}}
/>}

View File

@@ -6,7 +6,7 @@ import "@triliumnext/ckeditor5";
import clsx from "clsx";
import { Ref } from "preact";
import { useEffect, useLayoutEffect, useMemo } from "preact/hooks";
import { useEffect, useLayoutEffect, useMemo, useRef as usePreactRef } from "preact/hooks";
import appContext from "../../../components/app_context";
import FNote from "../../../entities/fnote";
@@ -24,6 +24,17 @@ import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./util
export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) {
const blob = useNoteBlob(note);
const { isRtl } = useNoteLanguage(note);
const readOnlyContentRef = usePreactRef<HTMLDivElement>(null);
// Scroll to bookmark anchor if navigated with ?bookmark=...
useEffect(() => {
const viewScope = noteContext?.viewScope;
if (!viewScope?.bookmark || !readOnlyContentRef.current) return;
const el = readOnlyContentRef.current.querySelector(`[id="${CSS.escape(viewScope.bookmark)}"]`);
el?.scrollIntoView({ behavior: "smooth", block: "center" });
viewScope.bookmark = undefined;
}, [blob]);
return (
<>
@@ -31,6 +42,7 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
html={blob?.content ?? ""}
ntxId={ntxId}
dir={isRtl ? "rtl" : "ltr"}
contentRef={readOnlyContentRef}
/>
<TouchBar>

View File

@@ -133,6 +133,15 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
defaultProtocol: "https://",
allowedProtocols: ALLOWED_PROTOCOLS
},
bookmark: {
toolbar: [
"bookmarkPreview",
"|",
"editBookmark",
"copyAnchorLink",
"removeBookmark"
]
},
emoji: {
definitionsUrl: window.glob.isDev
? new URL(import.meta.url).origin + emojiDefinitionsUrl

View File

@@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
:POWERSHELL
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:TRILIUM_DATA_DIR -Value './trilium-data'; ./trilium.exe"
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:TRILIUM_DATA_DIR -Value './trilium-data'; Set-Item -Path Env:TRILIUM_ELECTRON_DATA_DIR -Value './trilium-electron-data'; Set-Item -Path Env:ELECTRON_NO_ATTACH_CONSOLE -Value '1'; ./trilium.exe"
GOTO END
:BATCH
@@ -17,6 +17,8 @@ chcp 65001
SET DIR=%~dp0
SET DIR=%DIR:~0,-1%
SET TRILIUM_DATA_DIR=%DIR%\trilium-data
SET TRILIUM_ELECTRON_DATA_DIR=%DIR%\trilium-electron-data
SET ELECTRON_NO_ATTACH_CONSOLE=1
cd "%DIR%"
start trilium.exe
GOTO END

View File

@@ -2,6 +2,7 @@
DIR=`dirname "$0"`
export TRILIUM_DATA_DIR="$DIR/trilium-data"
export TRILIUM_ELECTRON_DATA_DIR="$DIR/trilium-electron-data"
exec "$DIR/trilium"

View File

@@ -11,16 +11,16 @@
"url": "https://triliumnotes.org"
},
"scripts": {
"dev": "cross-env TRILIUM_PORT=37742 TRILIUM_DATA_DIR=data tsx ../../scripts/electron-start.mts src/main.ts",
"dev": "cross-env TRILIUM_PORT=37742 TRILIUM_DATA_DIR=data TRILIUM_ELECTRON_DATA_DIR=data-electron-37742 tsx ../../scripts/electron-start.mts src/main.ts",
"start-no-dir": "cross-env TRILIUM_PORT=37743 tsx ../../scripts/electron-start.mts src/main.ts",
"build": "tsx scripts/build.ts",
"start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist",
"start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_ELECTRON_DATA_DIR=data-electron-37841 TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist",
"start-prod-no-dir": "pnpm build && cross-env TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist",
"electron-forge:make": "pnpm build && electron-forge make dist",
"electron-forge:make-flatpak": "pnpm build && DEBUG=* electron-forge make dist --targets=@electron-forge/maker-flatpak",
"electron-forge:package": "pnpm build && electron-forge package dist",
"electron-forge:start": "pnpm build && electron-forge start dist",
"e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e ELECTRON_IS_DEV=0 playwright test"
"e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e TRILIUM_ELECTRON_DATA_DIR=data-e2e-electron-8082 ELECTRON_IS_DEV=0 playwright test"
},
"dependencies": {
"@electron/remote": "2.1.3",

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
#
# Build an AppImage from the packaged Electron app.
#
# Usage: ./build-appimage.sh [arch]
# arch: x64 or arm64 (default: x64)
#
# Prerequisites:
# - The Electron app must already be packaged via `electron-forge make` or `electron-forge package`
# - appimagetool must be available in PATH
#
# Environment variables:
# TRILIUM_ARTIFACT_NAME_HINT: If set, used as the base name for the output file
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
FORGE_DIR="$DESKTOP_DIR/electron-forge"
ARCH="${1:-x64}"
EXECUTABLE_NAME="trilium"
PRODUCT_NAME="Trilium Notes"
# Map architecture names
case "$ARCH" in
x64) APPIMAGE_ARCH="x86_64" ;;
arm64) APPIMAGE_ARCH="aarch64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
# Find the packaged app directory
PACKAGED_DIR="$DESKTOP_DIR/dist/out/$PRODUCT_NAME-linux-$ARCH"
if [ ! -d "$PACKAGED_DIR" ]; then
echo "Error: Packaged app not found at $PACKAGED_DIR"
echo "Run 'electron-forge make' or 'electron-forge package' first."
exit 1
fi
echo "Building AppImage from: $PACKAGED_DIR"
# Create AppDir structure
APPDIR="$DESKTOP_DIR/dist/out/$PRODUCT_NAME.AppDir"
rm -rf "$APPDIR"
mkdir -p "$APPDIR"
# Copy the packaged app contents into the AppDir
cp -a "$PACKAGED_DIR"/. "$APPDIR/"
# Create the AppRun entry point
cat > "$APPDIR/AppRun" << 'APPRUN_EOF'
#!/bin/bash
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/trilium" "$@"
APPRUN_EOF
chmod +x "$APPDIR/AppRun"
# Create the .desktop file
cat > "$APPDIR/$EXECUTABLE_NAME.desktop" << DESKTOP_EOF
[Desktop Entry]
Name=$PRODUCT_NAME
Comment=Build your personal knowledge base with Trilium Notes
GenericName=Note Taking Application
Exec=$EXECUTABLE_NAME %U
Icon=$EXECUTABLE_NAME
Type=Application
StartupNotify=true
StartupWMClass=$PRODUCT_NAME
Categories=Office;Utility;
DESKTOP_EOF
# Copy the icon (AppImage expects it at the root of AppDir)
if [ -f "$FORGE_DIR/app-icon/png/256x256.png" ]; then
cp "$FORGE_DIR/app-icon/png/256x256.png" "$APPDIR/$EXECUTABLE_NAME.png"
elif [ -f "$APPDIR/icon.png" ]; then
cp "$APPDIR/icon.png" "$APPDIR/$EXECUTABLE_NAME.png"
else
echo "Warning: No icon found"
fi
# Determine output filename
UPLOAD_DIR="$DESKTOP_DIR/upload"
mkdir -p "$UPLOAD_DIR"
if [ -n "${TRILIUM_ARTIFACT_NAME_HINT:-}" ]; then
OUTPUT_NAME="${TRILIUM_ARTIFACT_NAME_HINT//\//-}.AppImage"
else
VERSION=$(node -e "console.log(require('$DESKTOP_DIR/package.json').version)")
OUTPUT_NAME="TriliumNotes-v${VERSION}-linux-${ARCH}.AppImage"
fi
OUTPUT_PATH="$UPLOAD_DIR/$OUTPUT_NAME"
# Build the AppImage
echo "Creating AppImage: $OUTPUT_PATH"
ARCH="$APPIMAGE_ARCH" appimagetool "$APPDIR" "$OUTPUT_PATH"
# Clean up the AppDir
rm -rf "$APPDIR"
echo "AppImage created successfully: $OUTPUT_PATH"

View File

@@ -10,7 +10,7 @@ import electronDebug from "electron-debug";
import electronDl from "electron-dl";
import { PRODUCT_NAME } from "./app-info";
import port from "@triliumnext/server/src/services/port.js";
import { join } from "path";
import { join, resolve } from "path";
import { deferred, LOCALES } from "../../../packages/commons/src";
async function main() {
@@ -101,10 +101,16 @@ async function main() {
/**
* Returns a unique user data directory for Electron so that single instance locks between legitimately different instances such as different port or data directory can still act independently, but we are focusing the main window otherwise.
*
* When running in portable mode, set TRILIUM_ELECTRON_DATA_DIR (e.g. via the trilium-portable script)
* so that no Electron files are written to the system's roaming profile (e.g. %APPDATA% on Windows).
*/
function getUserData() {
const name = `${app.getName()}-${port}`;
return join(app.getPath("appData"), name);
if (process.env.TRILIUM_ELECTRON_DATA_DIR) {
return resolve(process.env.TRILIUM_ELECTRON_DATA_DIR);
}
return join(app.getPath("appData"), `${app.getName()}-${port}`);
}
async function onReady() {

View File

@@ -32,9 +32,9 @@
"dependencies": {
"@ai-sdk/anthropic": "3.0.69",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/openai": "3.0.52",
"@ai-sdk/openai": "3.0.53",
"@modelcontextprotocol/sdk": "^1.12.1",
"ai": "6.0.159",
"ai": "6.0.161",
"better-sqlite3": "12.9.0",
"html-to-text": "9.0.5",
"js-yaml": "4.1.1",

View File

@@ -1,5 +1,5 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import { beforeAll, describe, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";

View File

@@ -48,6 +48,8 @@ CREATE TABLE IF NOT EXISTS "revisions" (`revisionId` TEXT NOT NULL PRIMARY KEY,
type TEXT DEFAULT '' NOT NULL,
mime TEXT DEFAULT '' NOT NULL,
`title` TEXT NOT NULL,
`description` TEXT DEFAULT '' NOT NULL,
`source` TEXT DEFAULT 'auto' NOT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
blobId TEXT DEFAULT NULL,
`utcDateLastEdited` TEXT NOT NULL,

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

View File

@@ -1,13 +1,121 @@
<figure class="image">
<img style="aspect-ratio:2089/1515;" src="2_Note Revisions_image.png"
width="2089" height="1515">
</figure>
<p>Trilium supports seamless versioning of notes by storing snapshots ("revisions")
of notes at regular intervals.</p>
<h2>Note Revisions Snapshot Interval</h2>
<h2>Displaying the revisions</h2>
<ul>
<li>On the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
press the <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_8YBEPzcpUgxw">note context menu</a> and
select <em>Note revisions…</em>
</li>
<li>On the old layout, press directly the
<img class="image_resized" style="aspect-ratio:27/25;width:2.32%;"
src="1_Note Revisions_image.png" width="27" height="25">button in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_8YBEPzcpUgxw">Note buttons</a>&nbsp;area.</li>
</ul>
<h2>Interaction</h2>
<aside class="admonition note">
<p>This documentation matches the redesign of the note revisions dialog on
v0.103.0, older versions have a similar dialog but with some differences.</p>
</aside>
<ul>
<li>The full list of revisions are displayed on the left in reverse chronological
order.
<ul>
<li>The revisions are grouped by the date the revision was taken.</li>
<li
>This list does not contain the <em>current state</em> of the note, so it
is possible to have notes with no revisions/snapshots saved.</li>
</ul>
</li>
<li>The icon of a revision indicates the <em>source</em> of that revision (e.g.
a
<img class="image_resized" style="aspect-ratio:20/21;width:2.49%;" src="Note Revisions_image.png"
width="20" height="21">icon for a manually saved revision).</li>
<li>Pressing the […] on the top-right of the dialog displays multiple options,
including:
<ul>
<li>Saving a new revision now.</li>
<li>Checking the interval and limit for this note (see below).</li>
<li>Deleting all the revisions of this note.</li>
</ul>
</li>
<li>For supported notes (text, code), changes are highlighted. This behavior
can be toggled via the <em>Highlight changes</em> at the top of the dialog.
<ul>
<li>The highlighted changes are relative to the <strong>current state of the note</strong>,
not to the revision prior to this one.</li>
</ul>
</li>
<li>For any given revision, the buttons on the top-right allow operating on
it:
<ul>
<li>Deleting the revision.</li>
<li>Downloading the revision locally.</li>
<li>Restoring the revision, which replaces the current content of the note
with the one from the revision. Another revision is saved containing the
current content of the note.</li>
</ul>
</li>
</ul>
<h2>Named revisions</h2>
<p>Named revisions are a new feature of Trilium v0.103.0 which allows adding
a short description of what the changes in the snapshot contain.</p>
<p>In the list of note revisions:</p>
<ul>
<li>The name of the revision is displayed underneath the time of the revision
in the sidebar, as well as at the top of the dialog where it is displayed
in full.</li>
<li>Clicking on the edit button near the name of the revision allows it to
be changed.</li>
</ul>
<p>To create a named revision, either:</p>
<ul>
<li>Go to the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_8YBEPzcpUgxw">Note buttons</a>,
select <em>Save named revision…</em>, enter the name of revision and confirm.</li>
<li
>Use the corresponding <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/_help_A9Oc6YKKc65v">keyboard shortcut</a> or
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_F1r9QtzQLZqm">Jump to...</a>&nbsp;command
with the same name.</li>
<li>Save a revision normally, and adjust the name afterwards from the note
revision list.</li>
</ul>
<h2>When revisions are saved</h2>
<p>Revisions are saved:</p>
<ul>
<li>Automatically at a fixed interval. This behavior can be configured (see
below).</li>
<li>Manually, by:
<ul>
<li>Going to the press the <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_8YBEPzcpUgxw">note context menu</a> and
select <em>Save revision.</em>
</li>
<li>Using the <em>Force Save Revision</em> <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/_help_A9Oc6YKKc65v">keyboard shortcut</a>.</li>
<li
>In the <em>Revisions</em> dialog, pressing the […] button in the top-right
and selecting <em>Save a revision now</em>.</li>
</ul>
</li>
</ul>
<p>Additionally, revisions can also come from somewhere else, and this is
indicated via the icon of the revision:</p>
<ul>
<li>Generated externally, by&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_pgxEVkzLl1OP">ETAPI (REST API)</a>.</li>
<li
>A modification created by&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GBBMSlVSOIGP">AI</a>.</li>
<li
>A revision is restored, causing the existing note content to be saved
as a revision to prevent potential data loss.</li>
</ul>
<h4>Snapshot interval</h4>
<p>Time interval of taking note snapshot is configurable in the Options -&gt;
Other dialog. This provides a tradeoff between more revisions and more
Other dialog. This provides a trade-off between more revisions and more
data to store.</p>
<p>To turn off note versioning for a particular note (or subtree), add
<p>To turn off note versioning for a particular note (or sub-tree), add
<code
spellcheck="false">disableVersioning</code> <a href="#root/_help_zEY4DaJG4YT5">label</a>to the note.</p>
<h2>Note Revision Snapshots Limit</h2>
spellcheck="false">disableVersioning</code> <a href="#root/_help_zEY4DaJG4YT5">label</a> to the note.</p>
<h4>Maximum revisions</h4>
<p>The limit on the number of note snapshots can be configured in the Options
-&gt; Other dialog. The note revision snapshot number limit refers to the
maximum number of revisions that can be saved for each note. Where -1 means
@@ -15,10 +123,5 @@
for a single note through the <code spellcheck="false">versioningLimit=X</code> label.</p>
<p>The note limit will not take effect immediately; it will only apply when
the note is modified.</p>
<p>You can click the <strong>Erase excess revision snapshots now</strong> button
to apply the changes immediately.</p>
<p>Note revisions can be accessed through the button on the right of ribbon
toolbar.</p>
<p>
<img src="Note Revisions_note-revisi.png">
</p>
<p>You can click the <em>Erase excess revision snapshots now</em> button to
apply the changes immediately.</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -165,10 +165,16 @@ class="admonition note">
class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>.</li>
</ul>
<p>For example, to change the font of the document from the one defined by
the theme or the user to a serif one:</p><pre><code class="language-text-x-trilium-auto">body {
--main-font-family: serif !important;
--detail-font-family: var(--main-font-family) !important;
the theme or the user to a serif one:</p><pre><code class="language-text-x-trilium-auto">body{
--print-font-family: serif;
--print-font-size: 11pt;
}</code></pre>
<aside class="admonition important">
<p>When altering <code spellcheck="false">--print-font-family</code>, make
sure the change is done at <code spellcheck="false">body</code> level and
not <code spellcheck="false">:root</code>, since otherwise it won't be picked
up due to specificity rules.</p>
</aside>
<p>To remark:</p>
<ul>
<li>Multiple CSS notes can be add by using multiple <code spellcheck="false">~printCss</code> relations.</li>

View File

@@ -1,8 +1,8 @@
<p>Split view is a feature of&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_s1aBHPd79XYj">Mermaid Diagrams</a>&nbsp;and&nbsp;
<p>Split view is a feature of&nbsp;<a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>&nbsp;and&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6RM1Q7ppFVoj">Markdown</a>&nbsp;notes which displays both the source code on one side
class="reference-link" href="#root/_help_6RM1Q7ppFVoj">Markdown</a>&nbsp;notes which displays both the source code on one side
and the preview of the content on the other.</p>
<p><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_s1aBHPd79XYj">Mermaid Diagrams</a>&nbsp;also
<p><a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>&nbsp;also
allow changing between a horizontal or a vertical split, to accommodate
for the various sizes of diagrams.</p>
<h2>Display modes and interaction</h2>
@@ -20,12 +20,12 @@
<li><em>Preview</em> which displays only the rendering of the diagram or text
in full screen, especially useful for read-only notes.</li>
</ul>
<p>These buttons can be found near the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_8YBEPzcpUgxw">Note buttons</a>&nbsp;section
on the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
or in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;on
<p>These buttons can be found near the&nbsp;<a class="reference-link" href="#root/_help_8YBEPzcpUgxw">Note buttons</a>&nbsp;section
on the&nbsp;<a class="reference-link" href="#root/_help_IjZS7iK5EXtb">New Layout</a>,
or in the&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;on
the old layout.</p>
<p>The display node is stored at note level.</p>
<h2>Relation to read-only notes</h2>
<p>If a note is marked as <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_CoFPLs3dRlXc">read-only</a>,
the source view will not be editable. While in preview mode, marking a
note as read-only has no effect since the preview itself is not editable.</p>
<p>If a note is marked as <a href="#root/_help_CoFPLs3dRlXc">read-only</a>, the
source view will not be editable. While in preview mode, marking a note
as read-only has no effect since the preview itself is not editable.</p>

View File

@@ -70,6 +70,19 @@
this:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_DATA_DIR=/home/myuser/data/my-trilium-data trilium</code></pre>
<p>You can then save the above command as a shell script on your path for
convenience.</p>
<h2>Electron user data directory (desktop only)</h2>
<p>When running the desktop application, Electron stores internal data (caches,
spell-check dictionaries, session storage, etc.) separately from the Trilium
data directory. By default this goes to the system's application data folder
(e.g. <code spellcheck="false">%APPDATA%</code> on Windows), which may be
undesirable in corporate environments with roaming profiles or when running
in portable mode.</p>
<p>To keep Electron data out of the system's roaming profile, set the
<code
spellcheck="false">TRILIUM_ELECTRON_DATA_DIR</code>environment variable to an explicit path.
The <code spellcheck="false">trilium-portable</code> script does this automatically,
pointing it to <code spellcheck="false">trilium-electron-data/</code> next
to the application.</p>
<h2>Fine-grained directory/path location</h2>
<p>Apart from the data directory, some of the subdirectories of it can be
moved elsewhere by changing an environment variable:</p>
@@ -129,5 +142,13 @@
</td>
<td>Path to&nbsp;<a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a>&nbsp;file.</td>
</tr>
<tr>
<td><code spellcheck="false">TRILIUM_ELECTRON_DATA_DIR</code>
</td>
<td>System appData</td>
<td>Directory for Electron internal data (caches, spell-check dictionaries,
etc.). Set this in portable mode to avoid writing to the system profile
(desktop only).</td>
</tr>
</tbody>
</table>

View File

@@ -23,7 +23,9 @@
<li><code spellcheck="false">trilium-portable</code>: Launches Trilium in
portable mode, where the <a href="#root/_help_tAassRL4RSQL">data directory</a> is
created within the application's directory, making it easy to move the
entire setup.</li>
entire setup. Electron's internal data (caches, dictionaries, etc.) is
also stored within the data directory, so no files are written to the system's
roaming profile.</li>
<li><code spellcheck="false">trilium-safe-mode</code>: Boots Trilium in "safe
mode," disabling any startup scripts that might cause the application to
crash.</li>

View File

@@ -9,8 +9,7 @@
note where to place the new one and select:</p>
<ul>
<li><em>Insert note after</em>, to put the new note underneath the one selected.</li>
<li
><em>Insert child note</em>, to insert the note as a child of the selected
<li><em>Insert child note</em>, to insert the note as a child of the selected
note.</li>
</ul>
<p>
@@ -21,8 +20,7 @@
<li>When adding a <a href="#root/_help_QEAPj01N5f7w">link</a> in a&nbsp;<a class="reference-link"
href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;note, type the desired title of
the new note and press Enter. Afterwards the type of the note will be asked.</li>
<li
>Similarly, when creating a new tab, type the desired title and press Enter.</li>
<li>Similarly, when creating a new tab, type the desired title and press Enter.</li>
</ul>
<h2>Changing the type of a note</h2>
<p>It is possible to change the type of a note after it has been created
@@ -32,96 +30,94 @@
edit the <a href="#root/_help_4FahAwuGTAwC">source of a note</a>.</p>
<h2>Supported note types</h2>
<p>The following note types are supported by Trilium:</p>
<figure class="table">
<table>
<thead>
<tr>
<th>Note Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a>
</td>
<td>The default note type, which allows for rich text formatting, images,
admonitions and right-to-left support.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>
</td>
<td>Uses a mono-space font and can be used to store larger chunks of code
or plain text than a text note, and has better syntax highlighting.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a>
</td>
<td>Stores the information about a search (the search text, criteria, etc.)
for later use. Can be used for quick filtering of a large amount of notes,
for example. The search can easily be triggered.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_iRwzGnHPzonm">Relation Map</a>
</td>
<td>Allows easy creation of notes and relations between them. Can be used
for mainly relational data such as a family tree.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_bdUJEHsAPYQR">Note Map</a>
</td>
<td>Displays the relationships between the notes, whether via relations or
their hierarchical structure.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>
</td>
<td>Used in&nbsp;<a class="reference-link" href="#root/_help_CdNpE2pqjmI6">Scripting</a>,
it displays the HTML content of another note. This allows displaying any
kind of content, provided there is a script behind it to generate it.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>
</td>
<td>Displays the children of the note either as a grid, a list, or for a more
specialized case: a calendar.&nbsp;&nbsp;
<br>
<br>Generally useful for easy reading of short notes.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>
</td>
<td>Displays diagrams such as bar charts, flow charts, state diagrams, etc.
Requires a bit of technical knowledge since the diagrams are written in
a specialized format.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_grjYqerjn243">Canvas</a>
</td>
<td>Allows easy drawing of sketches, diagrams, handwritten content. Uses the
same technology behind <a href="https://excalidraw.com">excalidraw.com</a>.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_1vHRoWCEjj0L">Web View</a>
</td>
<td>Displays the content of an external web page, similar to a browser.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a>
</td>
<td>Easy for brainstorming ideas, by placing them in a hierarchical layout.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a>
</td>
<td>Displays the children of the note as a geographical map, one use-case
would be to plan vacations. It even has basic support for tracks. Notes
can also be created from it.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a>
</td>
<td>Represents an uploaded file such as PDFs, images, video or audio files.</td>
</tr>
</tbody>
</table>
</figure>
<table>
<thead>
<tr>
<th>Note Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a>
</td>
<td>The default note type, which allows for rich text formatting, images,
admonitions and right-to-left support.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>
</td>
<td>Uses a mono-space font and can be used to store larger chunks of code
or plain text than a text note, and has better syntax highlighting.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a>
</td>
<td>Stores the information about a search (the search text, criteria, etc.)
for later use. Can be used for quick filtering of a large amount of notes,
for example. The search can easily be triggered.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_iRwzGnHPzonm">Relation Map</a>
</td>
<td>Allows easy creation of notes and relations between them. Can be used
for mainly relational data such as a family tree.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_bdUJEHsAPYQR">Note Map</a>
</td>
<td>Displays the relationships between the notes, whether via relations or
their hierarchical structure.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>
</td>
<td>Used in&nbsp;<a class="reference-link" href="#root/_help_CdNpE2pqjmI6">Scripting</a>,
it displays the HTML content of another note. This allows displaying any
kind of content, provided there is a script behind it to generate it.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>
</td>
<td>Displays the children of the note either as a grid, a list, or for a more
specialized case: a calendar.&nbsp;&nbsp;
<br>
<br>Generally useful for easy reading of short notes.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>
</td>
<td>Displays diagrams such as bar charts, flow charts, state diagrams, etc.
Requires a bit of technical knowledge since the diagrams are written in
a specialized format.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_grjYqerjn243">Canvas</a>
</td>
<td>Allows easy drawing of sketches, diagrams, handwritten content. Uses the
same technology behind <a href="https://excalidraw.com">excalidraw.com</a>.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_1vHRoWCEjj0L">Web View</a>
</td>
<td>Displays the content of an external web page, similar to a browser.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a>
</td>
<td>Easy for brainstorming ideas, by placing them in a hierarchical layout.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a>
</td>
<td>Displays the children of the note as a geographical map, one use-case
would be to plan vacations. It even has basic support for tracks. Notes
can also be created from it.</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a>
</td>
<td>Represents an uploaded file such as PDFs, images, video or audio files.</td>
</tr>
</tbody>
</table>

View File

@@ -5,8 +5,7 @@
create a <em>File</em> note type directly:</p>
<ul>
<li>Drag a file into the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
<li
>Right click a note and select <em>Import into note</em> and point it to
<li>Right click a note and select <em>Import into note</em> and point it to
one of the supported files.</li>
</ul>
<h2>Supported file types</h2>
@@ -83,30 +82,28 @@
href="#root/_help_BlN9DFI679QC">Ribbon</a>.
<ul>
<li><em>Download</em>, which will download the file for local use.</li>
<li
><em>Open</em>, will will open the file with the system-default application.</li>
<li
>Upload new revision to replace the file with a new one.</li>
<li><em>Open</em>, will will open the file with the system-default application.</li>
<li>Upload new revision to replace the file with a new one.</li>
</ul>
</li>
<li>It is <strong>not</strong> possible to change the note type of a <em>File</em> note.</li>
<li
>Convert into an <a href="#root/_help_0vhv7lsOLy82">attachment</a> from the <a href="#root/_help_8YBEPzcpUgxw">note menu</a>.</li>
</li>
<li>It is <strong>not</strong> possible to change the note type of a <em>File</em> note.</li>
<li>Convert into an <a href="#root/_help_0vhv7lsOLy82">attachment</a> from the <a href="#root/_help_8YBEPzcpUgxw">note menu</a>.</li>
</ul>
<h2>Relation with other notes</h2>
<ul>
<li>
<p>Files are also displayed in the&nbsp;<a class="reference-link" href="#root/_help_0ESUbbAxVnoK">Note List</a>&nbsp;based
on their type:</p>
<p>
<img class="image_resized" style="aspect-ratio:853/315;width:50%;" src="4_File_image.png"
width="853" height="315">
</p>
<img class="image_resized" style="aspect-ratio:853/315;width:50%;"
src="4_File_image.png" width="853" height="315">
</li>
<li>
<p>Non-image files can be embedded into text notes as read-only widgets via
the&nbsp;<a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>&nbsp;functionality.</p>
</li>
<li>
<p>Image files can be embedded into text notes like normal images via&nbsp;
<a
class="reference-link" href="#root/_help_0Ofbk1aSuVRu">Image references</a>.</p>
</li>
<li>Non-image files can be embedded into text notes as read-only widgets via
the&nbsp;<a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>&nbsp;functionality.</li>
<li
>Image files can be embedded into text notes like normal images via&nbsp;
<a
class="reference-link" href="#root/_help_0Ofbk1aSuVRu">Image references</a>.</li>
</ul>

View File

@@ -1,13 +1,12 @@
<p>Trilium has always supported Markdown through its <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/mHbBMPDPkVV5/_help_Oau6X9rCuegd">import feature</a>,
<p>Trilium has always supported Markdown through its <a href="#root/_help_Oau6X9rCuegd">import feature</a>,
however the file was either transformed to a&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a>&nbsp;note
(converted to Trilium's internal HTML format) or saved as a&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;note
href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;note (converted to Trilium's internal
HTML format) or saved as a&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note
with only syntax highlight.</p>
<p>This note type is a split view, meaning that both the source code and
a preview of the document are displayed side-by-side. See&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_SL5f1Auq7sVN">Note types with split view</a>&nbsp;for
more information.</p>
href="#root/_help_SL5f1Auq7sVN">Note types with split view</a>&nbsp;for more
information.</p>
<h2>Rationale</h2>
<p>The goal of this note type is to fill a gap: rendering Markdown but not
altering its structure or its whitespace which would inevitably change
@@ -33,81 +32,77 @@
<p>The following features are supported by Trilium's Markdown format and
will show up in the preview pane:</p>
<ul>
<li>All standard and GitHub-flavored syntax (basic formatting, tables, blockquotes)</li>
<li
>Code blocks with syntax highlight (e.g. <code spellcheck="false">```js</code>)
and automatic syntax highlight</li>
<li><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/_help_NwBbFdNZ9h7O">Block quotes &amp; admonitions</a>
</li>
<li><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/_help_YfYAtQBcfo5V">Math Equations</a>
</li>
<li><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_s1aBHPd79XYj">Mermaid Diagrams</a>&nbsp;using
<li>
<p>All standard and GitHub-flavored syntax (basic formatting, tables, blockquotes)</p>
</li>
<li>
<p>Code blocks with syntax highlight (e.g. <code spellcheck="false">```js</code>)
and automatic syntax highlight</p>
</li>
<li>
<p><a class="reference-link" href="#root/_help_NwBbFdNZ9h7O">Block quotes &amp; admonitions</a>
</p>
</li>
<li>
<p><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
</p>
</li>
<li>
<p><a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>&nbsp;using
<code
spellcheck="false">```mermaid</code>
</li>
<li>
<p><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/_help_nBAXQFj20hS1">Include Note</a>&nbsp;(no
builtin Markdown syntax, but HTML syntax works just fine):</p><pre><code class="language-text-x-trilium-auto">&lt;section class="include-note" data-note-id="vJDjQm0VK8Na" data-box-size="expandable"&gt;
&amp;nbsp;
</p>
</li>
<li>
<p><a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>&nbsp;(no
builtin Markdown syntax, but HTML syntax works just fine):</p><pre><code class="language-text-x-trilium-auto">&lt;section class="include-note" data-note-id="vJDjQm0VK8Na" data-box-size="expandable"&gt;
&amp;nbsp;
&lt;/section&gt;n</code></pre>
</li>
<li>
<p><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/QEAPj01N5f7w/_help_hrZ1D00cLbal">Internal (reference) links</a>&nbsp;via
its HTML syntax, or through a <em>Wikilinks</em>-like format (only&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_m1lbrzyKDaRB">Note ID</a>):</p><pre><code class="language-text-x-trilium-auto">[[Hg8TS5ZOxti6]]</code></pre>
</li>
</li>
<li>
<p><a class="reference-link" href="#root/_help_hrZ1D00cLbal">Internal (reference) links</a>&nbsp;via
its HTML syntax, or through a <em>Wikilinks</em>-like format (only&nbsp;
<a
class="reference-link" href="#root/_help_m1lbrzyKDaRB">Note ID</a>):</p><pre><code class="language-text-x-trilium-auto">[[Hg8TS5ZOxti6]]</code></pre>
</li>
</ul>
<h2>Creating Markdown notes</h2>
<p>There are two ways to create a Markdown note:</p>
<ol>
<li>Create a new note (e.g. in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>)
<li>Create a new note (e.g. in the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>)
and select the type <em>Markdown</em>, just like all the other note types.</li>
<li
>Create a note of type&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;and
select as the language either <em>Markdown </em>or <em>GitHub-Flavored Markdown</em>.
<li>Create a note of type&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;and
select as the language either <em>Markdown</em> or <em>GitHub-Flavored Markdown</em>.
This maintains compatibility with your existing notes prior to the introduction
of this feature.</li>
</ol>
<aside class="admonition note">
<p>There is no distinction between the new Markdown note type and code notes
of type Markdown; internally both are represented as&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;notes
with the proper MIME type (e.g. <code spellcheck="false">text/x-markdown</code>).</p>
href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;notes with the proper MIME type
(e.g. <code spellcheck="false">text/x-markdown</code>).</p>
</aside>
<h2>Import/export</h2>
<ul>
<li>
<p>By default, when importing a single Markdown file it automatically gets
converted to a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a>&nbsp;note.
To avoid that and have it imported as a Markdown note instead:</p>
<li>By default, when importing a single Markdown file it automatically gets
converted to a&nbsp;<a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;note.
To avoid that and have it imported as a Markdown note instead:
<ul>
<li>
<p>Right click the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
select <em>Import into note</em>.</p>
</li>
<li>
<p>Select the file normally.</p>
</li>
<li>
<p>Uncheck <em>Import HTML, Markdown and TXT as text notes if it's unclear from the metadata</em>.</p>
</li>
<li>Right click the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
select <em>Import into note</em>.</li>
<li>Select the file normally.</li>
<li>Uncheck <em>Import HTML, Markdown and TXT as text notes if it's unclear from the metadata</em>.</li>
</ul>
</li>
<li>
<p>When exporting Markdown files, the extension is preserved and the content
remains the same as in the source view.</p>
</li>
<li>
<p>Once exported as a Trilium ZIP, the ZIP will preserve the Markdown type
without converting to text notes thanks to the meta-information in it.</p>
</li>
<li>When exporting Markdown files, the extension is preserved and the content
remains the same as in the source view.</li>
<li>Once exported as a Trilium ZIP, the ZIP will preserve the Markdown type
without converting to text notes thanks to the meta-information in it.</li>
</ul>
<h2>Conversion between text notes and Markdown notes</h2>
<p>Currently there is no built-in functionality to convert a&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a>&nbsp;note
into a Markdown note or vice-versa. We do have plans to address this in
the future.</p>
href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;note into a Markdown note or vice-versa.
We do have plans to address this in the future.</p>
<p>This can be achieved manually, for a single note:</p>
<ol>
<li>Export the file as Markdown, with single format.</li>
@@ -135,6 +130,6 @@
<p>This feature of synchronizing the scroll is based on blocks but it's provided
on a best-effort basis since our underlying Markdown library doesn't support
this feature natively, so we had to implement our own algorithm. Feel free
to <a href="#root/pOsGYCXsbNQG/BgmBlOIl72jZ/_help_wy8So3yZZlH9">report issues</a>,
but always provide a sample Markdown file to be able to reproduce it.</p>
to <a href="#root/_help_wy8So3yZZlH9">report issues</a>, but always provide a
sample Markdown file to be able to reproduce it.</p>
</aside>

View File

@@ -12,8 +12,8 @@
the diagram.</p>
<p>This note type is a split view, meaning that both the source code and
a preview of the document are displayed side-by-side. See&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_SL5f1Auq7sVN">Note types with split view</a>&nbsp;for
more information.</p>
href="#root/_help_SL5f1Auq7sVN">Note types with split view</a>&nbsp;for more
information.</p>
<h2>Sample diagrams</h2>
<p>Starting with v0.103.0, Mermaid diagrams no longer start with a sample
flowchart, but instead a pane at the bottom will show all the supported
@@ -52,34 +52,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&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;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&nbsp;<a class="reference-link"
href="#root/_help_0Ofbk1aSuVRu">Image references</a>&nbsp;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&nbsp;<a class="reference-link"
href="#root/_help_0Ofbk1aSuVRu">Image references</a>&nbsp;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

View File

@@ -13,13 +13,11 @@
<ol>
<li>HTML language for the legacy/vanilla method, with what needs to be displayed
(for example <code spellcheck="false">&lt;p&gt;Hello world.&lt;/p&gt;</code>).</li>
<li
>JSX for the Preact-based approach (see below).</li>
</ol>
<li>JSX for the Preact-based approach (see below).</li>
</ol>
</li>
<li>Create a&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</li>
<li
>Assign the <code spellcheck="false">renderNote</code> <a href="#root/_help_zEY4DaJG4YT5">relation</a> to
<li>Assign the <code spellcheck="false">renderNote</code> <a href="#root/_help_zEY4DaJG4YT5">relation</a> to
point at the previously created code note.</li>
</ol>
<h2>Legacy scripting using jQuery</h2>
@@ -48,9 +46,10 @@ $dateEl.text(new Date());</code></pre>
need to provide a HTML anymore.</p>
<p>Here are the steps to creating a simple render note:</p>
<ol>
<li>Create a note of type&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</li>
<li
>
<li>
<p>Create a note of type&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</p>
</li>
<li>
<p>Create a child&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note
with JSX as the language.
<br>As an example, use the following content:</p><pre><code class="language-text-x-trilium-auto">export default function() {
@@ -60,17 +59,20 @@ $dateEl.text(new Date());</code></pre>
&lt;/&gt;
);
}</code></pre>
</li>
<li>In the parent render note, define a <code spellcheck="false">~renderNote</code> relation
pointing to the newly created child.</li>
<li>Refresh the render note and it should display a “Hello world” message.</li>
</li>
<li>
<p>In the parent render note, define a <code spellcheck="false">~renderNote</code> relation
pointing to the newly created child.</p>
</li>
<li>
<p>Refresh the render note and it should display a “Hello world” message.</p>
</li>
</ol>
<h2>Refreshing the note</h2>
<p>It's possible to refresh the note via:</p>
<ul>
<li>The corresponding button in&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>.</li>
<li
>The “Render active note” <a href="#root/_help_A9Oc6YKKc65v">keyboard shortcut</a> (not
<li>The “Render active note” <a href="#root/_help_A9Oc6YKKc65v">keyboard shortcut</a> (not
assigned by default).</li>
</ul>
<h2>Examples</h2>

View File

@@ -64,9 +64,8 @@
yet:</p>
<ul>
<li>Trilium-specific formulas (e.g. to obtain the title of a note).</li>
<li
>User-defined formulas</li>
<li>Cross-workbook calculation</li>
<li>User-defined formulas</li>
<li>Cross-workbook calculation</li>
</ul>
<p>If you would like us to work on these features, consider <a href="https://triliumnotes.org/en/support-us">supporting us</a>.</p>
<h2>Known limitations</h2>
@@ -81,8 +80,7 @@
</ul>
</li>
<li>There is currently no export functionality, as stated previously.</li>
<li
>There is no dedicated mobile support. Mobile support is currently experimental
<li>There is no dedicated mobile support. Mobile support is currently experimental
in Univer and when it becomes stable, we could potentially integrate it
into Trilium as well.</li>
</ul>

View File

@@ -20,171 +20,168 @@
<p>Fore more information see&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>.</p>
<h2>Features and formatting</h2>
<p>Here's a list of various features supported by text notes:</p>
<figure
class="table">
<table>
<thead>
<tr>
<th>Dedicated article</th>
<th>Feature</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="reference-link" href="#root/_help_Gr6xFaF6ioJ5">General formatting</a>
</td>
<td>
<ul>
<li>Headings (section titles, paragraph)</li>
<li>Font size</li>
<li>Bold, italic, underline, strike-through</li>
<li>Superscript, subscript</li>
<li>Font color &amp; background color</li>
<li>Remove formatting</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_S6Xx8QIWTV66">Lists</a>
</td>
<td>
<ul>
<li>Bulleted lists</li>
<li>Numbered lists</li>
<li>To-do lists</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_NwBbFdNZ9h7O">Block quotes &amp; admonitions</a>
</td>
<td>
<ul>
<li>Block quotes</li>
<li>Admonitions</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_NdowYOC1GFKS">Tables</a>
</td>
<td>
<ul>
<li>Basic tables</li>
<li>Merging cells</li>
<li>Styling tables and cells.</li>
<li>Table captions</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_UYuUB1ZekNQU">Developer-specific formatting</a>
</td>
<td>
<ul>
<li>Inline code</li>
<li>Code blocks</li>
<li>Keyboard shortcuts</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_AgjCISero73a">Footnotes</a>
</td>
<td>
<ul>
<li>Footnotes</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_mT0HEkOsz6i1">Images</a>
</td>
<td>
<ul>
<li>Images</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_QEAPj01N5f7w">Links</a>
</td>
<td>
<ul>
<li>External links</li>
<li>Internal Trilium links</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>
</td>
<td>
<ul>
<li>Include note</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_CohkqWQC1iBv">Insert buttons</a>
</td>
<td>
<ul>
<li>Symbols</li>
<li><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
</li>
<li>Mermaid diagrams</li>
<li>Horizontal ruler</li>
<li>Page break</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_dEHYtoWWi8ct">Other features</a>
</td>
<td>
<ul>
<li>Indentation
<ul>
<li>Markdown import</li>
</ul>
</li>
<li><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_gLt3vA97tMcp">Premium features</a>
</td>
<td>
<ul>
<li><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
</li>
<li><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
</li>
<li><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</figure>
<h2>Read-Only vs. Editing Mode</h2>
<p>Text notes are usually opened in edit mode. However, they may open in
read-only mode if the note is too big or the note is explicitly marked
as read-only. For more information, see&nbsp;<a class="reference-link"
href="#root/_help_CoFPLs3dRlXc">Read-Only Notes</a>.</p>
<h2>Keyboard shortcuts</h2>
<p>There are numerous keyboard shortcuts to format the text without having
to use the mouse. For a reference of all the key combinations, see&nbsp;
<a
class="reference-link" href="#root/_help_A9Oc6YKKc65v">Keyboard Shortcuts</a>. In addition, see&nbsp;<a class="reference-link"
href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting</a>&nbsp;as an alternative
to the keyboard shortcuts.</p>
<h2>Technical details</h2>
<p>For the text editing functionality, Trilium uses a commercial product
(with an open-source base) called&nbsp;<a class="reference-link" href="#root/_help_MI26XDLSAlCD">CKEditor</a>.
This brings the benefit of having a powerful WYSIWYG (What You See Is What
You Get) editor.</p>
<table>
<thead>
<tr>
<th>Dedicated article</th>
<th>Feature</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="reference-link" href="#root/_help_Gr6xFaF6ioJ5">General formatting</a>
</td>
<td>
<ul>
<li>Headings (section titles, paragraph)</li>
<li>Font size</li>
<li>Bold, italic, underline, strike-through</li>
<li>Superscript, subscript</li>
<li>Font color &amp; background color</li>
<li>Remove formatting</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_S6Xx8QIWTV66">Lists</a>
</td>
<td>
<ul>
<li>Bulleted lists</li>
<li>Numbered lists</li>
<li>To-do lists</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_NwBbFdNZ9h7O">Block quotes &amp; admonitions</a>
</td>
<td>
<ul>
<li>Block quotes</li>
<li>Admonitions</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_NdowYOC1GFKS">Tables</a>
</td>
<td>
<ul>
<li>Basic tables</li>
<li>Merging cells</li>
<li>Styling tables and cells.</li>
<li>Table captions</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_UYuUB1ZekNQU">Developer-specific formatting</a>
</td>
<td>
<ul>
<li>Inline code</li>
<li>Code blocks</li>
<li>Keyboard shortcuts</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_AgjCISero73a">Footnotes</a>
</td>
<td>
<ul>
<li>Footnotes</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_mT0HEkOsz6i1">Images</a>
</td>
<td>
<ul>
<li>Images</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_QEAPj01N5f7w">Links</a>
</td>
<td>
<ul>
<li>External links</li>
<li>Internal Trilium links</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>
</td>
<td>
<ul>
<li>Include note</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_CohkqWQC1iBv">Insert buttons</a>
</td>
<td>
<ul>
<li>Symbols</li>
<li><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
</li>
<li>Mermaid diagrams</li>
<li>Horizontal ruler</li>
<li>Page break</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_dEHYtoWWi8ct">Other features</a>
</td>
<td>
<ul>
<li>Indentation
<ul>
<li>Markdown import</li>
</ul>
</li>
<li><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_gLt3vA97tMcp">Premium features</a>
</td>
<td>
<ul>
<li><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
</li>
<li><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
</li>
<li><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
<h2>Read-Only vs. Editing Mode</h2>
<p>Text notes are usually opened in edit mode. However, they may open in
read-only mode if the note is too big or the note is explicitly marked
as read-only. For more information, see&nbsp;<a class="reference-link"
href="#root/_help_CoFPLs3dRlXc">Read-Only Notes</a>.</p>
<h2>Keyboard shortcuts</h2>
<p>There are numerous keyboard shortcuts to format the text without having
to use the mouse. For a reference of all the key combinations, see&nbsp;
<a
class="reference-link" href="#root/_help_A9Oc6YKKc65v">Keyboard Shortcuts</a>. In addition, see&nbsp;<a class="reference-link"
href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting</a>&nbsp;as an alternative
to the keyboard shortcuts.</p>
<h2>Technical details</h2>
<p>For the text editing functionality, Trilium uses a commercial product
(with an open-source base) called&nbsp;<a class="reference-link" href="#root/_help_MI26XDLSAlCD">CKEditor</a>.
This brings the benefit of having a powerful WYSIWYG (What You See Is What
You Get) editor.</p>

View File

@@ -0,0 +1,70 @@
<aside class="admonition note">
<p>This feature used to be called <em>Bookmarks</em> (as it is the official
name in the editor we are using), but in order not to collide with the
concept of&nbsp;<a class="reference-link" href="#root/_help_u3YFHC9tQlpm">Bookmarks</a>,
we have renamed it to <em>Anchors.</em>
</p>
</aside>
<p>Anchors allows creating <a href="#root/_help_QEAPj01N5f7w">links</a> to a certain
part of a note, such as referencing a particular heading or section within
a note.</p>
<p>This feature was introduced in TriliumNext v0.94.0 and augmented in v0.130.0
to support linking across notes.</p>
<h2>Interaction</h2>
<ul>
<li>To create a anchor:
<ul>
<li>Place the cursor at the desired position where to place the anchor.</li>
<li>Look for the
<img src="Anchors_plus.png"
width="15" height="16">button in the&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>,
and then press the
<img src="1_Anchors_plus.png"
width="12" height="15">button.</li>
<li>Alternatively, use&nbsp;<a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>&nbsp;and
look for <em>anchor</em>.</li>
</ul>
</li>
<li>To place a link to a anchor:
<ul>
<li>Place the cursor at the desired position of the link.</li>
<li>From the <a href="#root/_help_QEAPj01N5f7w">link</a> pane, select the <em>Anchors</em> section
and select the desired anchor.</li>
</ul>
</li>
</ul>
<h2>Linking across notes</h2>
<p>Trilium v0.103.0 introduces cross-note Anchors, which makes it possible
to create&nbsp;<a class="reference-link" href="#root/_help_hrZ1D00cLbal">Internal (reference) links</a>&nbsp;which
point to a specific anchor in that document.</p>
<h3>Compatibility with documents from previous versions</h3>
<p>For notes created prior to Trilium v0.103.0, you might notice that the
Anchors might not be identified. This limitation is intentional in order
not to have to re-process all the notes, looking for anchors.</p>
<p>To fix this, simply go that note and make any change (e.g. inserting a
space), this will trigger the recalculation of the links.</p>
<h3>Linking to anchors through the <em>Add link</em> dialog</h3>
<ol>
<li>Create an anchor in the target note using the same process as described
above.</li>
<li>In another note, press <kbd>Ctrl</kbd>+<kbd>L</kbd> to insert an internal
link. Select the target note containing Anchors.</li>
<li>If the target note contains Anchors, a section will appear underneath
the note selector with the list of Anchors.</li>
<li>Add the link normally.</li>
</ol>
<p>Clicking on a reference link pointing to a anchor will automatically scroll
to the desired section.</p>
<h3>Linking to anchors through the bookmark toolbar</h3>
<ol>
<li>Create an anchor in the target note using the same process as described
above.</li>
<li>Click on the anchor to reveal the anchor's floating toolbar.</li>
<li>Click on the <em>Copy anchor reference link</em> button.</li>
<li>Go to the note where to insert the link and press <kbd>Ctrl</kbd>+<kbd>V</kbd>.</li>
</ol>
<aside class="admonition note">
<p>Use this method only to insert&nbsp;<a class="reference-link" href="#root/_help_hrZ1D00cLbal">Internal (reference) links</a>&nbsp;between
two documents. To link to an anchor on the same note, use the <em>Insert link</em> dialog
(<kbd>Ctrl</kbd>+<kbd>K</kbd>) and select the <em>Anchors</em> item instead.</p>
</aside>

View File

@@ -1,31 +0,0 @@
<p>Bookmarks allows creating <a href="#root/_help_QEAPj01N5f7w">links</a> to a certain
part of a note, such as referencing a particular heading.</p>
<p>Technically, bookmarks are HTML anchors.</p>
<p>This feature was introduced in TriliumNext 0.94.0.</p>
<h2>Interaction</h2>
<ul>
<li>To create a bookmark:
<ul>
<li>Place the cursor at the desired position where to place the bookmark.</li>
<li>Look for the
<img src="Bookmarks_plus.png"
width="15" height="16">button in the&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>,
and then press the
<img src="1_Bookmarks_plus.png"
width="12" height="15">button.</li>
</ul>
</li>
<li>To place a link to a bookmark:
<ul>
<li>Place the cursor at the desired position of the link.</li>
<li>From the <a href="#root/_help_QEAPj01N5f7w">link</a> pane, select the <em>Bookmarks</em> section
and select the desired bookmark.</li>
</ul>
</li>
</ul>
<h2>Limitations</h2>
<ul>
<li>Currently it's not possible to create a link to a bookmark from a different
note. This functionality will be added after the internal links feature
is enhanced to support bookmarks.</li>
</ul>

View File

@@ -4,7 +4,7 @@
reveal special inserable items and blocks such as symbols, Math expressions
and separators.</p>
<h2>Bookmarks</h2>
<p>See the dedicated&nbsp;<a class="reference-link" href="#root/_help_oSuaNgyyKnhu">Bookmarks</a>&nbsp;section.</p>
<p>See the dedicated&nbsp;<a class="reference-link" href="#root/_help_oSuaNgyyKnhu">Anchors</a>&nbsp;section.</p>
<h2>Emoji</h2>
<figure class="image image-style-align-right image_resized" style="width:42.4%;">
<img style="aspect-ratio:366/410;" src="Insert buttons_plus.png"

View File

@@ -100,6 +100,7 @@
"reset-zoom-level": "Reset zoom level",
"copy-without-formatting": "Copy selected text without formatting",
"force-save-revision": "Force creating / saving new note revision of the active note",
"save-named-revision": "Save a named revision of the active note",
"toggle-book-properties": "Toggle Collection Properties",
"toggle-classic-editor-toolbar": "Toggle the Formatting tab for the editor with fixed toolbar",
"export-as-pdf": "Export the current note as a PDF",
@@ -200,7 +201,8 @@
"zoom-in": "Zoom In",
"reset-zoom-level": "Reset Zoom Level",
"copy-without-formatting": "Copy Without Formatting",
"force-save-revision": "Force Save Revision"
"force-save-revision": "Force Save Revision",
"save-named-revision": "Save Named Revision"
},
"login": {
"title": "Login",

View File

@@ -382,7 +382,8 @@
"migration": {
"old_version": "現在のバージョンからの直接的な移行はサポートされていません。まず最新のv0.60.4にアップグレードしてから、このバージョンにアップグレードしてください。",
"error_message": "バージョン {{version}} への移行中にエラーが発生しました: {{stack}}",
"wrong_db_version": "データベースのバージョン({{version}})は、アプリケーションが想定しているバージョン({{targetVersion}}よりも新しく、互換性のないバージョンによって作成された可能性があります。この問題を解決するには、Triliumを最新バージョンにアップグレードしてください。"
"wrong_db_version": "データベースのバージョン({{version}})は、アプリケーションが想定しているバージョン({{targetVersion}}よりも新しく、互換性のないバージョンによって作成された可能性があります。この問題を解決するには、Triliumを最新バージョンにアップグレードしてください。",
"invalid_db_version": "データベースのバージョン番号が無効です。これは通常、データベース内の 'dbVersion' オプションが破損していることを示しています。バックアップから復元してください。"
},
"modals": {
"error_title": "エラー"

View File

@@ -45,7 +45,7 @@
"show-note-source": "顯示筆記來源對話方塊",
"show-options": "打開選項頁面",
"show-revisions": "顯示筆記歷史版本對話方塊",
"show-recent-changes": "顯示最近改對話方塊",
"show-recent-changes": "顯示最近改對話方塊",
"show-sql-console": "打開 SQL 控制台頁面",
"show-backend-log": "打開後端日誌頁面",
"text-note-operations": "文字筆記操作",
@@ -261,7 +261,7 @@
"show-note-source": "顯示筆記原始碼",
"show-options": "顯示選項",
"show-revisions": "顯示歷史版本",
"show-recent-changes": "顯示最近改",
"show-recent-changes": "顯示最近改",
"show-sql-console": "顯示 SQL 控制台",
"show-backend-log": "顯示後端日誌",
"show-help": "顯示說明",

View File

@@ -85,6 +85,13 @@
"reload-frontend-app": "ئالدى تەرەپ ئەپىنى قايتا يۈكلەش",
"open-dev-tools": "تەتقىقاتچى قوراللىرىنى ئېچىش",
"find-in-text": "تېكىست ئىچىدىن ئىزدەش",
"toggle-left-note-tree-panel": "سول تەرەپ (خاتىرە دەرىخى) تاختىسىنى ئالماشتۇرۇش"
"toggle-left-note-tree-panel": "سول تەرەپ (خاتىرە دەرىخى) تاختىسىنى ئالماشتۇرۇش",
"toggle-full-screen": "پۈتۈن ئېكران شەكلىگە ئالماشتۇرۇش",
"zoom-out": "كىچىكلىتىش",
"zoom-in": "چوڭايتىش",
"note-navigation": "خاتىرە يولباشچىسى",
"reset-zoom-level": "چوڭ-كىچىكلىك دەرىجىسىنى ئەسلىگە كەلتۈرۈش",
"copy-without-formatting": "تاللانغان تېكىستنى فارماٹسىز كۆچۈرۈش",
"force-save-revision": "نۆۋەتتىكى خاتىرىنىڭ يېڭى نەشرىنى مەجبۇرىي قۇرۇش/ساقلاش"
}
}

View File

@@ -119,7 +119,15 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
}
isAutoLink() {
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
if (this.type === "relation") {
return ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
if (this.type === "label") {
return this.name === "internalBookmark";
}
return false;
}
get note() {

View File

@@ -1,4 +1,4 @@
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow, RevisionSource } from "@triliumnext/commons";
import { dayjs, getNoteIcon } from "@triliumnext/commons";
import cloningService from "../../services/cloning.js";
@@ -1543,7 +1543,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
return !(this.noteId in this.becca.notes) || this.isBeingDeleted;
}
saveRevision(): BRevision {
saveRevision(opts: { description?: string; source?: RevisionSource } = {}): BRevision {
return sql.transactional(() => {
let noteContent = this.getContent();
@@ -1552,6 +1552,8 @@ class BNote extends AbstractBeccaEntity<BNote> {
noteId: this.noteId,
// title and text should be decrypted now
title: this.title,
description: opts.description || "",
source: opts.source || "auto",
type: this.type,
mime: this.mime,
isProtected: this.isProtected,

View File

@@ -7,7 +7,7 @@ import becca from "../becca.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js";
import BAttachment from "./battachment.js";
import type { AttachmentRow, NoteType, RevisionPojo, RevisionRow } from "@triliumnext/commons";
import type { AttachmentRow, NoteType, RevisionPojo, RevisionRow, RevisionSource } from "@triliumnext/commons";
import eraseService from "../../services/erase.js";
interface ContentOpts {
@@ -31,7 +31,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
return "revisionId";
}
static get hashedProperties() {
return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"];
return ["revisionId", "noteId", "title", "description", "source", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"];
}
revisionId?: string;
@@ -39,6 +39,8 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
type!: NoteType;
mime!: string;
title!: string;
description!: string;
source!: RevisionSource;
dateLastEdited?: string;
utcDateLastEdited?: string;
contentLength?: number;
@@ -61,6 +63,8 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
this.mime = row.mime;
this.isProtected = !!row.isProtected;
this.title = row.title;
this.description = row.description || "";
this.source = row.source || "auto";
this.blobId = row.blobId;
this.dateLastEdited = row.dateLastEdited;
this.dateCreated = row.dateCreated;
@@ -193,6 +197,8 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
mime: this.mime,
isProtected: this.isProtected,
title: this.title,
description: this.description,
source: this.source,
blobId: this.blobId,
dateLastEdited: this.dateLastEdited,
dateCreated: this.dateCreated,

View File

@@ -73,6 +73,8 @@ function mapRevisionToPojo(revision: BRevision) {
mime: revision.mime,
isProtected: revision.isProtected,
title: revision.title,
description: revision.description,
source: revision.source,
blobId: revision.blobId,
dateLastEdited: revision.dateLastEdited,
dateCreated: revision.dateCreated,

View File

@@ -192,7 +192,8 @@ function register(router: Router) {
eu.route<{ noteId: string }>(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
note.saveRevision();
const description = typeof req.body?.description === "string" ? req.body.description : "";
note.saveRevision({ description, source: "etapi" });
return res.sendStatus(204);
});

View File

@@ -6,6 +6,15 @@
// Migrations should be kept in descending order, so the latest migration is first.
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
// Add description column to revisions table for manual revision comments
{
version: 238,
sql: /*sql*/`
ALTER TABLE revisions ADD COLUMN description TEXT DEFAULT '' NOT NULL;
ALTER TABLE revisions ADD COLUMN source TEXT DEFAULT 'auto' NOT NULL;
`,
ignoreErrors: true
},
// Clean up obsolete keyboard shortcut options from renamed actions
{
version: 237,

View File

@@ -351,7 +351,12 @@ function forceSaveRevision(req: Request<{ noteId: string }>) {
throw new ValidationError(`Note revision of a protected note cannot be created outside of a protected session.`);
}
note.saveRevision();
const description = typeof req.body?.description === "string" ? req.body.description : "";
const revision = note.saveRevision({ description, source: "manual" });
return {
revisionId: revision.revisionId
};
}
function convertNoteToAttachment(req: Request<{ noteId: string }>) {

View File

@@ -111,6 +111,18 @@ function eraseRevision(req: Request<{ revisionId: string }>) {
eraseService.eraseRevisions([req.params.revisionId]);
}
function updateRevisionDescription(req: Request<{ revisionId: string }>) {
const revision = becca.getRevisionOrThrow(req.params.revisionId);
const { description } = req.body;
if (typeof description !== "string") {
return [400, "Description must be a string."];
}
revision.description = description;
revision.save();
}
function eraseAllExcessRevisions() {
const allNoteIds = sql.getRows("SELECT noteId FROM notes WHERE SUBSTRING(noteId, 1, 1) != '_'") as { noteId: string }[];
allNoteIds.forEach((row) => {
@@ -125,7 +137,7 @@ function restoreRevision(req: Request<{ revisionId: string }>) {
const note = revision.getNote();
sql.transactional(() => {
note.saveRevision();
note.saveRevision({ source: "restore" });
for (const oldNoteAttachment of note.getAttachments()) {
oldNoteAttachment.markAsDeleted();
@@ -222,5 +234,6 @@ export default {
eraseAllRevisions,
eraseAllExcessRevisions,
eraseRevision,
restoreRevision
restoreRevision,
updateRevisionDescription
};

View File

@@ -186,6 +186,7 @@ function register(app: express.Application) {
apiRoute(GET, "/api/revisions/:revisionId", revisionsApiRoute.getRevision);
apiRoute(GET, "/api/revisions/:revisionId/blob", revisionsApiRoute.getRevisionBlob);
apiRoute(DEL, "/api/revisions/:revisionId", revisionsApiRoute.eraseRevision);
apiRoute(PATCH, "/api/revisions/:revisionId", revisionsApiRoute.updateRevisionDescription);
apiRoute(PST, "/api/revisions/:revisionId/restore", revisionsApiRoute.restoreRevision);
route(GET, "/api/revisions/:revisionId/image/:filename", [auth.checkApiAuthOrElectron], imageRoute.returnImageFromRevision);

View File

@@ -77,7 +77,7 @@ function getAttributeNames(type: string, nameLike: string) {
}
}
names = names.filter((name) => !["internalLink", "imageLink", "includeNoteLink", "relationMapLink"].includes(name));
names = names.filter((name) => !["internalLink", "imageLink", "includeNoteLink", "relationMapLink", "internalBookmark"].includes(name));
names.sort((a, b) => {
const aPrefix = a.toLowerCase().startsWith(nameLike);

View File

@@ -50,4 +50,26 @@ describe("sanitize", () => {
</figure>`;
expect(html_sanitizer.sanitize(dirty)).toBe(clean);
});
describe("bookmark anchors", () => {
it("preserves id attribute on empty <a> tags (CKEditor bookmarks)", () => {
const dirty = `<a id="my-bookmark"></a>`;
expect(html_sanitizer.sanitize(dirty)).toBe(dirty);
});
it("preserves id attribute on <a> tags with bookmark class", () => {
const dirty = `<a id="chapter-1" class="ck-bookmark"></a>`;
expect(html_sanitizer.sanitize(dirty)).toBe(dirty);
});
it("strips id attribute from non-anchor tags to prevent DOM clobbering", () => {
const dirty = `<div id="loginForm">content</div>`;
expect(html_sanitizer.sanitize(dirty)).toBe(`<div>content</div>`);
});
it("strips id attribute from <img> tags to prevent DOM clobbering", () => {
const dirty = `<img id="someId" src="test.png" />`;
expect(html_sanitizer.sanitize(dirty)).toBe(`<img src="test.png" />`);
});
});
});

View File

@@ -42,6 +42,7 @@ function sanitize(dirtyHtml: string) {
allowedTags: allowedTags as string[],
allowedAttributes: {
"*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"],
a: ["id"], // CKEditor bookmark anchors use <a id="name"></a>
input: ["type", "checked"],
img: ["width", "height"],
code: [ "spellcheck" ]

View File

@@ -828,6 +828,14 @@ function getDefaultKeyboardActions() {
defaultShortcuts: [],
description: t("keyboard_actions.force-save-revision"),
scope: "window"
},
{
actionName: "saveNamedRevision",
friendlyName: t("keyboard_action_names.save-named-revision"),
iconClass: "bx bx-purchase-tag",
defaultShortcuts: [],
description: t("keyboard_actions.save-named-revision"),
scope: "window"
}
];

View File

@@ -116,7 +116,7 @@ export const noteTools = defineTools({
return { error: `Cannot update content for note type: ${note.type}` };
}
note.saveRevision();
note.saveRevision({ source: "llm" });
setNoteContentFromLlm(note, content);
return {
success: true,
@@ -158,7 +158,7 @@ export const noteTools = defineTools({
newContent = existingContent + (existingContent.endsWith("\n") ? "" : "\n") + content;
}
note.saveRevision();
note.saveRevision({ source: "llm" });
note.setContent(newContent);
return {
success: true,

View File

@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { findBookmarks } from "./notes.js";
describe("findBookmarks", () => {
it("extracts bookmark IDs from empty anchor tags", () => {
const content = `<p>Hello</p><a id="chapter-1"></a><p>World</p>`;
expect(findBookmarks(content)).toEqual(["chapter-1"]);
});
it("extracts multiple bookmarks", () => {
const content = `<a id="intro"></a><p>Text</p><a id="conclusion"></a>`;
expect(findBookmarks(content)).toEqual(["intro", "conclusion"]);
});
it("returns empty array when no bookmarks exist", () => {
const content = `<p>No bookmarks here</p>`;
expect(findBookmarks(content)).toEqual([]);
});
it("ignores anchor tags with href (regular links, not bookmarks)", () => {
const content = `<a href="#root/abc123" id="some-id">link</a>`;
expect(findBookmarks(content)).toEqual([]);
});
it("handles bookmarks with various valid ID characters", () => {
const content = `<a id="my_bookmark-2.0"></a>`;
expect(findBookmarks(content)).toEqual(["my_bookmark-2.0"]);
});
it("does not produce duplicates", () => {
const content = `<a id="same"></a><a id="same"></a>`;
expect(findBookmarks(content)).toEqual(["same"]);
});
it("matches self-closing bookmark anchors (CKEditor empty elements)", () => {
const content = `<p>Text</p><a id="my-bookmark"></a><p>More</p>`;
// CKEditor may also output without closing tag
const contentNoClose = `<p>Text</p><a id="my-bookmark"><p>More</p>`;
expect(findBookmarks(content)).toEqual(["my-bookmark"]);
expect(findBookmarks(contentNoClose)).toEqual(["my-bookmark"]);
});
});

View File

@@ -454,6 +454,54 @@ function findImageLinks(content: string, foundLinks: FoundLink[]) {
return content.replace(/src="[^"]*\/api\/images\//g, 'src="api/images/');
}
/**
* Extracts bookmark IDs from CKEditor bookmark anchors (`<a id="..."></a>` without href).
* Bookmarks are stored as labels on the note so they can be looked up without parsing content.
*/
export function findBookmarks(content: string): string[] {
const re = /<a\s+id="([^"]+)"[^>]*>(<\/a>)?/g;
const bookmarks: string[] = [];
let match;
while ((match = re.exec(content))) {
// Skip anchors that also have an href (those are regular links, not bookmarks)
if (match[0].includes("href=")) {
continue;
}
const id = match[1];
if (!bookmarks.includes(id)) {
bookmarks.push(id);
}
}
return bookmarks;
}
function saveBookmarks(note: BNote, content: string) {
const foundBookmarks = findBookmarks(content);
const existingBookmarks = note.getOwnedLabels("internalBookmark");
for (const bookmarkId of foundBookmarks) {
const existing = existingBookmarks.find((l) => l.value === bookmarkId);
if (!existing) {
new BAttribute({
noteId: note.noteId,
type: "label",
name: "internalBookmark",
value: bookmarkId
}).save();
}
}
// Remove bookmarks that are no longer in the content
const unusedBookmarks = existingBookmarks.filter((l) => !foundBookmarks.includes(l.value));
for (const unused of unusedBookmarks) {
unused.markAsDeleted();
}
}
function findInternalLinks(content: string, foundLinks: FoundLink[]) {
const re = /href="[^"]*#root[a-zA-Z0-9_\/]*\/([a-zA-Z0-9_]+)\/?"/g;
let match;
@@ -695,6 +743,7 @@ function saveLinks(note: BNote, content: string | Buffer) {
content = findImageLinks(content, foundLinks);
content = findInternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks);
saveBookmarks(note, content);
({ forceFrontendReload, content } = checkImageAttachments(note, content));
} else if (note.type === "relationMap" && typeof content === "string") {

View File

@@ -16,7 +16,7 @@
"packageManager": "pnpm@10.33.0",
"devDependencies": {
"@wxt-dev/auto-icons": "1.1.1",
"wxt": "0.20.21"
"wxt": "0.20.22"
},
"dependencies": {
"cash-dom": "8.1.5"

View File

@@ -13,7 +13,7 @@
"preact": "10.29.1",
"preact-iso": "2.11.1",
"preact-render-to-string": "6.6.7",
"react-i18next": "17.0.2"
"react-i18next": "17.0.3"
},
"devDependencies": {
"@preact/preset-vite": "2.10.5",

View File

@@ -1,5 +1,5 @@
# Documentation
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/1ysfcELr4Xua/Documentation_image.png" width="205" height="162">
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/88cjtiwxfR49/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/README-ja.md vendored
View File

@@ -63,7 +63,7 @@ Trilium Notes
* ートは任意の深さのツリーに配置できます。1つのートをツリー内の複数の場所に配置できます[クローン](https://docs.triliumnotes.org/user-guide/concepts/notes/cloning)を参照)
* 豊富な WYSIWYG ノートエディター 例:
表、画像、[数式](https://docs.triliumnotes.org/user-guide/note-types/text) とマークダウン
表、画像、[数式](https://docs.triliumnotes.org/user-guide/note-types/text) と markdown
[自動フォーマット](https://docs.triliumnotes.org/user-guide/note-types/text/markdown-formatting)
など
* 構文ハイライト表示を含む

31
docs/README-ug.md vendored
View File

@@ -285,23 +285,24 @@ pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
### تەتقىقاتچى ھۆججەتلىرى
Please view the [documentation
guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
for details. If you have more questions, feel free to reach out via the links
described in the "Discuss with us" section above.
تەپسىلاتلار ئۈچۈن [ھۆججەت
يېتەكچىسى](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)گە
قاراڭ. ئەگەر تېخىمۇ كۆپ سوئاللىرىڭىز بولسا، ئۈستىدىكى "بىز بىلەن ئالاقىلىشىڭ"
بۆلىكىدە تەمىنلەنگەن ئۇلىنىشلار ئارقىلىق بىز بىلەن ئالاقىلىشىڭنى قارشى ئالىمىز.
## 👏 Shoutouts
## 👏 مىننەتدارلىق
* [zadam](https://github.com/zadam) for the original concept and implementation
of the application.
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
application icon.
* [nriver](https://github.com/nriver) for his work on internationalization.
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
widget.
* [Dosu](https://dosu.dev/) for providing us with the automated responses to
GitHub issues and discussions.
* ئەپنىڭ ئەسلى ئۇقۇم لاھىيەسى ۋە ئەمەلگە ئاشۇرۇلۇشىغا تۆھپە قوشقان
[zadam](https://github.com/zadam).
* ئەپ سىنبەلگىسىنى لاھىيەلىگەن [Sarah
Hussein](https://github.com/Sarah-Hussein).
* خەلقئارالاشتۇرۇش خىزمىتىگە تۆھپە قوشقان [nriver](https://github.com/nriver).
* Canvas جەھەتتىكى ئەسلى ئىجادىي خىزمەتلىرى ئۈچۈن [Thomas
Frei](https://github.com/thfrei).
* ئەسلى گرامماتىكا گەۋدىلەندۈرۈش كىچىك زاپچاسلارنى ئاپتورى
[antoniotejada](https://github.com/nriver).
* GitHub مەسىلىلىرى ۋە مۇنازىرىلىرىگە ئاپتوماتىك جاۋاب قايتۇرۇش بىلەن تەمىنلىگەن
[Dosu](https://dosu.dev/).
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
Trilium would not be possible without the technologies behind it:

View File

@@ -3984,42 +3984,42 @@
"name": "internalLink",
"value": "s1aBHPd79XYj",
"isInheritable": false,
"position": 30
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "6RM1Q7ppFVoj",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "CoFPLs3dRlXc",
"isInheritable": false,
"position": 50
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "8YBEPzcpUgxw",
"isInheritable": false,
"position": 60
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "IjZS7iK5EXtb",
"isInheritable": false,
"position": 70
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 80
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "CoFPLs3dRlXc",
"isInheritable": false,
"position": 60
},
{
"type": "label",
@@ -4539,18 +4539,76 @@
"value": "bx bx-history",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "IjZS7iK5EXtb",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "8YBEPzcpUgxw",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "pgxEVkzLl1OP",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "GBBMSlVSOIGP",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "A9Oc6YKKc65v",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "F1r9QtzQLZqm",
"isInheritable": false,
"position": 100
}
],
"format": "markdown",
"dataFileName": "Note Revisions.md",
"attachments": [
{
"attachmentId": "1TA1nUFZzprY",
"title": "note-revisions.png",
"attachmentId": "BHquVQR30ess",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Note Revisions_note-revisi.png"
"dataFileName": "Note Revisions_image.png"
},
{
"attachmentId": "eoYsKZfMMvlg",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Note Revisions_image.png"
},
{
"attachmentId": "w1kmtyCISdjQ",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Note Revisions_image.png"
}
]
},
@@ -7188,6 +7246,93 @@
],
"dirFileName": "Text",
"children": [
{
"isClone": false,
"noteId": "oSuaNgyyKnhu",
"notePath": [
"pOsGYCXsbNQG",
"KSZ04uQ2D1St",
"iPIMuisry3hd",
"oSuaNgyyKnhu"
],
"title": "Anchors",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "u3YFHC9tQlpm",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "QEAPj01N5f7w",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "nRhnJkTT8cPs",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "ZlN4nump6EbW",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "hrZ1D00cLbal",
"isInheritable": false,
"position": 50
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-bookmark",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "shareAlias",
"value": "bookmarks",
"isInheritable": false,
"position": 30
}
],
"format": "markdown",
"dataFileName": "Anchors.md",
"attachments": [
{
"attachmentId": "2cn9iY3Qgyjs",
"title": "plus.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Anchors_plus.png"
},
{
"attachmentId": "JaiAT3dHDIyy",
"title": "plus.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Anchors_plus.png"
}
]
},
{
"isClone": false,
"noteId": "NwBbFdNZ9h7O",
@@ -7198,7 +7343,7 @@
"NwBbFdNZ9h7O"
],
"title": "Block quotes & admonitions",
"notePosition": 10,
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -7262,72 +7407,6 @@
}
]
},
{
"isClone": false,
"noteId": "oSuaNgyyKnhu",
"notePath": [
"pOsGYCXsbNQG",
"KSZ04uQ2D1St",
"iPIMuisry3hd",
"oSuaNgyyKnhu"
],
"title": "Bookmarks",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "QEAPj01N5f7w",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "nRhnJkTT8cPs",
"isInheritable": false,
"position": 20
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-bookmark",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "shareAlias",
"value": "bookmarks",
"isInheritable": false,
"position": 30
}
],
"format": "markdown",
"dataFileName": "Bookmarks.md",
"attachments": [
{
"attachmentId": "2cn9iY3Qgyjs",
"title": "plus.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Bookmarks_plus.png"
},
{
"attachmentId": "JaiAT3dHDIyy",
"title": "plus.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Bookmarks_plus.png"
}
]
},
{
"isClone": false,
"noteId": "veGu4faJErEM",
@@ -10147,17 +10226,24 @@
{
"type": "relation",
"name": "internalLink",
"value": "XpOYSgsLkTJy",
"value": "SL5f1Auq7sVN",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "0Ofbk1aSuVRu",
"value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "0Ofbk1aSuVRu",
"isInheritable": false,
"position": 40
},
{
"type": "label",
"name": "shareAlias",
@@ -10171,13 +10257,6 @@
"value": "bx bx-selection",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "SL5f1Auq7sVN",
"isInheritable": false,
"position": 40
}
],
"format": "markdown",
@@ -10839,6 +10918,90 @@
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "Oau6X9rCuegd",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "iPIMuisry3hd",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "SL5f1Auq7sVN",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "NwBbFdNZ9h7O",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "YfYAtQBcfo5V",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "s1aBHPd79XYj",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "nBAXQFj20hS1",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "hrZ1D00cLbal",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "m1lbrzyKDaRB",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "wy8So3yZZlH9",
"isInheritable": false,
"position": 120
},
{
"type": "label",
"name": "iconClass",
@@ -10852,90 +11015,6 @@
"value": "markdown",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "Oau6X9rCuegd",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "iPIMuisry3hd",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "wy8So3yZZlH9",
"isInheritable": false,
"position": 150
},
{
"type": "relation",
"name": "internalLink",
"value": "SL5f1Auq7sVN",
"isInheritable": false,
"position": 160
},
{
"type": "relation",
"name": "internalLink",
"value": "NwBbFdNZ9h7O",
"isInheritable": false,
"position": 170
},
{
"type": "relation",
"name": "internalLink",
"value": "YfYAtQBcfo5V",
"isInheritable": false,
"position": 180
},
{
"type": "relation",
"name": "internalLink",
"value": "s1aBHPd79XYj",
"isInheritable": false,
"position": 190
},
{
"type": "relation",
"name": "internalLink",
"value": "nBAXQFj20hS1",
"isInheritable": false,
"position": 200
},
{
"type": "relation",
"name": "internalLink",
"value": "hrZ1D00cLbal",
"isInheritable": false,
"position": 210
},
{
"type": "relation",
"name": "internalLink",
"value": "m1lbrzyKDaRB",
"isInheritable": false,
"position": 220
}
],
"format": "markdown",

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

View File

@@ -1,20 +1,74 @@
# Note Revisions
<figure class="image"><img style="aspect-ratio:2089/1515;" src="2_Note Revisions_image.png" width="2089" height="1515"></figure>
Trilium supports seamless versioning of notes by storing snapshots ("revisions") of notes at regular intervals.
## Note Revisions Snapshot Interval
## Displaying the revisions
Time interval of taking note snapshot is configurable in the Options -> Other dialog. This provides a tradeoff between more revisions and more data to store.
* On the <a class="reference-link" href="../UI%20Elements/New%20Layout.md">New Layout</a>, press the [note context menu](../UI%20Elements/Note%20buttons.md) and select _Note revisions…_
* On the old layout, press directly the <img class="image_resized" style="aspect-ratio:27/25;width:2.32%;" src="1_Note Revisions_image.png" width="27" height="25"> button in the <a class="reference-link" href="../UI%20Elements/Note%20buttons.md">Note buttons</a> area.
To turn off note versioning for a particular note (or subtree), add `disableVersioning` [label](../../Advanced%20Usage/Attributes.md)to the note.
## Interaction
## Note Revision Snapshots Limit
> [!NOTE]
> This documentation matches the redesign of the note revisions dialog on v0.103.0, older versions have a similar dialog but with some differences.
* The full list of revisions are displayed on the left in reverse chronological order.
* The revisions are grouped by the date the revision was taken.
* This list does not contain the _current state_ of the note, so it is possible to have notes with no revisions/snapshots saved.
* The icon of a revision indicates the _source_ of that revision (e.g. a <img class="image_resized" style="aspect-ratio:20/21;width:2.49%;" src="Note Revisions_image.png" width="20" height="21"> icon for a manually saved revision).
* Pressing the \[…\] on the top-right of the dialog displays multiple options, including:
* Saving a new revision now.
* Checking the interval and limit for this note (see below).
* Deleting all the revisions of this note.
* For supported notes (text, code), changes are highlighted. This behavior can be toggled via the _Highlight changes_ at the top of the dialog.
* The highlighted changes are relative to the **current state of the note**, not to the revision prior to this one.
* For any given revision, the buttons on the top-right allow operating on it:
* Deleting the revision.
* Downloading the revision locally.
* Restoring the revision, which replaces the current content of the note with the one from the revision. Another revision is saved containing the current content of the note.
## Named revisions
Named revisions are a new feature of Trilium v0.103.0 which allows adding a short description of what the changes in the snapshot contain.
In the list of note revisions:
* The name of the revision is displayed underneath the time of the revision in the sidebar, as well as at the top of the dialog where it is displayed in full.
* Clicking on the edit button near the name of the revision allows it to be changed.
To create a named revision, either:
* Go to the <a class="reference-link" href="../UI%20Elements/Note%20buttons.md">Note buttons</a>, select _Save named revision…_, enter the name of revision and confirm.
* Use the corresponding [keyboard shortcut](../Keyboard%20Shortcuts.md) or the <a class="reference-link" href="../Navigation/Jump%20to.md">Jump to...</a> command with the same name.
* Save a revision normally, and adjust the name afterwards from the note revision list.
## When revisions are saved
Revisions are saved:
* Automatically at a fixed interval. This behavior can be configured (see below).
* Manually, by:
* Going to the press the [note context menu](../UI%20Elements/Note%20buttons.md) and select _Save revision._
* Using the _Force Save Revision_ [keyboard shortcut](../Keyboard%20Shortcuts.md).
* In the _Revisions_ dialog, pressing the \[…\] button in the top-right and selecting _Save a revision now_.
Additionally, revisions can also come from somewhere else, and this is indicated via the icon of the revision:
* Generated externally, by <a class="reference-link" href="../../Advanced%20Usage/ETAPI%20(REST%20API).md">ETAPI (REST API)</a>.
* A modification created by <a class="reference-link" href="../../AI.md">AI</a>.
* A revision is restored, causing the existing note content to be saved as a revision to prevent potential data loss.
#### Snapshot interval
Time interval of taking note snapshot is configurable in the Options -> Other dialog. This provides a trade-off between more revisions and more data to store.
To turn off note versioning for a particular note (or sub-tree), add `disableVersioning` [label](../../Advanced%20Usage/Attributes.md) to the note.
#### Maximum revisions
The limit on the number of note snapshots can be configured in the Options -> Other dialog. The note revision snapshot number limit refers to the maximum number of revisions that can be saved for each note. Where -1 means no limit, 0 means delete all revisions. You can set the maximum revisions for a single note through the `versioningLimit=X` label.
The note limit will not take effect immediately; it will only apply when the note is modified.
You can click the **Erase excess revision snapshots now** button to apply the changes immediately.
Note revisions can be accessed through the button on the right of ribbon toolbar.
![](Note%20Revisions_note-revisi.png)
You can click the _Erase excess revision snapshots now_ button to apply the changes immediately.

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -99,12 +99,15 @@ To do so:
For example, to change the font of the document from the one defined by the theme or the user to a serif one:
```
body {
--main-font-family: serif !important;
--detail-font-family: var(--main-font-family) !important;
body{
--print-font-family: serif;
--print-font-size: 11pt;
}
```
> [!IMPORTANT]
> When altering `--print-font-family`, make sure the change is done at `body` level and not `:root`, since otherwise it won't be picked up due to specificity rules.
To remark:
* Multiple CSS notes can be add by using multiple `~printCss` relations.

View File

@@ -77,6 +77,12 @@ TRILIUM_DATA_DIR=/home/myuser/data/my-trilium-data trilium
You can then save the above command as a shell script on your path for convenience.
## Electron user data directory (desktop only)
When running the desktop application, Electron stores internal data (caches, spell-check dictionaries, session storage, etc.) separately from the Trilium data directory. By default this goes to the system's application data folder (e.g. `%APPDATA%` on Windows), which may be undesirable in corporate environments with roaming profiles or when running in portable mode.
To keep Electron data out of the system's roaming profile, set the `TRILIUM_ELECTRON_DATA_DIR` environment variable to an explicit path. The `trilium-portable` script does this automatically, pointing it to `trilium-electron-data/` next to the application.
## Fine-grained directory/path location
Apart from the data directory, some of the subdirectories of it can be moved elsewhere by changing an environment variable:
@@ -88,4 +94,5 @@ Apart from the data directory, some of the subdirectories of it can be moved els
| `TRILIUM_LOG_DIR` | `${TRILIUM_DATA_DIR}/log` | Directory where daily <a class="reference-link" href="../Troubleshooting/Error%20logs/Backend%20(server)%20logs.md">Backend (server) logs</a> are stored. |
| `TRILIUM_TMP_DIR` | `${TRILIUM_DATA_DIR}/tmp` | Directory where temporary files are stored (for example when opening in an external app). |
| `TRILIUM_ANONYMIZED_DB_DIR` | `${TRILIUM_DATA_DIR}/anonymized-db` | Directory where a <a class="reference-link" href="../Troubleshooting/Anonymized%20Database.md">Anonymized Database</a> is stored. |
| `TRILIUM_CONFIG_INI_PATH` | `${TRILIUM_DATA_DIR}/config.ini` | Path to <a class="reference-link" href="../Advanced%20Usage/Configuration%20(config.ini%20or%20e.md">Configuration (config.ini or environment variables)</a> file. |
| `TRILIUM_CONFIG_INI_PATH` | `${TRILIUM_DATA_DIR}/config.ini` | Path to <a class="reference-link" href="../Advanced%20Usage/Configuration%20(config.ini%20or%20e.md">Configuration (config.ini or environment variables)</a> file. |
| `TRILIUM_ELECTRON_DATA_DIR` | System appData | Directory for Electron internal data (caches, spell-check dictionaries, etc.). Set this in portable mode to avoid writing to the system profile (desktop only). |

View File

@@ -11,7 +11,7 @@ Trilium offers various startup scripts to customize your experience:
* `trilium-no-cert-check`: Starts Trilium without validating [TLS certificates](Server%20Installation/HTTPS%20\(TLS\).md), useful if connecting to a server with a self-signed certificate.
* Alternatively, set the `NODE_TLS_REJECT_UNAUTHORIZED=0` environment variable before starting Trilium.
* `trilium-portable`: Launches Trilium in portable mode, where the [data directory](Data%20directory.md) is created within the application's directory, making it easy to move the entire setup.
* `trilium-portable`: Launches Trilium in portable mode, where the [data directory](Data%20directory.md) is created within the application's directory, making it easy to move the entire setup. Electron's internal data (caches, dictionaries, etc.) is also stored within the data directory, so no files are written to the system's roaming profile.
* `trilium-safe-mode`: Boots Trilium in "safe mode," disabling any startup scripts that might cause the application to crash.
## Synchronization

View File

@@ -33,7 +33,7 @@ The following features are supported by Trilium's Markdown format and will show
```
<section class="include-note" data-note-id="vJDjQm0VK8Na" data-box-size="expandable">
&nbsp;
&nbsp;
</section>n
```
* <a class="reference-link" href="Text/Links/Internal%20(reference)%20links.md">Internal (reference) links</a> via its HTML syntax, or through a _Wikilinks_\-like format (only <a class="reference-link" href="../Advanced%20Usage/Note%20ID.md">Note ID</a>):
@@ -55,7 +55,6 @@ There are two ways to create a Markdown note:
## Import/export
* By default, when importing a single Markdown file it automatically gets converted to a <a class="reference-link" href="Text.md">Text</a> note. To avoid that and have it imported as a Markdown note instead:
* Right click the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a> and select _Import into note_.
* Select the file normally.
* Uncheck _Import HTML, Markdown and TXT as text notes if it's unclear from the metadata_.

View File

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 703 B

View File

@@ -0,0 +1,46 @@
# Anchors
> [!NOTE]
> This feature used to be called _Bookmarks_ (as it is the official name in the editor we are using), but in order not to collide with the concept of <a class="reference-link" href="../../Basic%20Concepts%20and%20Features/Navigation/Bookmarks.md">Bookmarks</a>, we have renamed it to _Anchors._
Anchors allows creating [links](Links.md) to a certain part of a note, such as referencing a particular heading or section within a note.
This feature was introduced in TriliumNext v0.94.0 and augmented in v0.130.0 to support linking across notes.
## Interaction
* To create a anchor:
* Place the cursor at the desired position where to place the anchor.
* Look for the <img src="Anchors_plus.png" width="15" height="16"> button in the <a class="reference-link" href="Formatting%20toolbar.md">Formatting toolbar</a>, and then press the <img src="1_Anchors_plus.png" width="12" height="15"> button.
* Alternatively, use <a class="reference-link" href="Premium%20features/Slash%20Commands.md">Slash Commands</a> and look for _anchor_.
* To place a link to a anchor:
* Place the cursor at the desired position of the link.
* From the [link](Links.md) pane, select the _Anchors_ section and select the desired anchor.
## Linking across notes
Trilium v0.103.0 introduces cross-note Anchors, which makes it possible to create <a class="reference-link" href="Links/Internal%20(reference)%20links.md">Internal (reference) links</a> which point to a specific anchor in that document.
### Compatibility with documents from previous versions
For notes created prior to Trilium v0.103.0, you might notice that the Anchors might not be identified. This limitation is intentional in order not to have to re-process all the notes, looking for anchors.
To fix this, simply go that note and make any change (e.g. inserting a space), this will trigger the recalculation of the links.
### Linking to anchors through the _Add link_ dialog
1. Create an anchor in the target note using the same process as described above.
2. In another note, press <kbd>Ctrl</kbd>+<kbd>L</kbd> to insert an internal link. Select the target note containing Anchors.
3. If the target note contains Anchors, a section will appear underneath the note selector with the list of Anchors.
4. Add the link normally.
Clicking on a reference link pointing to a anchor will automatically scroll to the desired section.
### Linking to anchors through the bookmark toolbar
1. Create an anchor in the target note using the same process as described above.
2. Click on the anchor to reveal the anchor's floating toolbar.
3. Click on the _Copy anchor reference link_ button.
4. Go to the note where to insert the link and press <kbd>Ctrl</kbd>+<kbd>V</kbd>.
> [!NOTE]
> Use this method only to insert <a class="reference-link" href="Links/Internal%20(reference)%20links.md">Internal (reference) links</a> between two documents. To link to an anchor on the same note, use the _Insert link_ dialog (<kbd>Ctrl</kbd>+<kbd>K</kbd>) and select the _Anchors_ item instead.

Some files were not shown because too many files have changed in this diff Show More