mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 09:36:37 +02:00
Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/about-dialog-overhaul
This commit is contained in:
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -93,7 +93,7 @@ jobs:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
uses: softprops/action-gh-release@v2.6.2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
uses: softprops/action-gh-release@v2.6.2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -150,7 +150,7 @@ jobs:
|
||||
path: upload
|
||||
|
||||
- name: Publish stable release
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
uses: softprops/action-gh-release@v2.6.2
|
||||
with:
|
||||
draft: false
|
||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||
|
||||
2
.github/workflows/web-clipper.yml
vendored
2
.github/workflows/web-clipper.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
compression-level: 0
|
||||
|
||||
- name: Release web clipper extension
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
uses: softprops/action-gh-release@v2.6.2
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/web-clipper-v') }}
|
||||
with:
|
||||
draft: false
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/tabulator-tables": "6.3.1",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"happy-dom": "20.8.9",
|
||||
"happy-dom": "20.9.0",
|
||||
"lightningcss": "1.32.0",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "4.0.1"
|
||||
|
||||
@@ -25,6 +25,15 @@ export type GetTextEditorCallback = (editor: CKTextEditor) => void;
|
||||
|
||||
export type SaveState = "saved" | "saving" | "unsaved" | "error";
|
||||
|
||||
const READ_ONLY_CAPABLE_TYPES: string[] = [
|
||||
"text",
|
||||
"code",
|
||||
"mermaid",
|
||||
"canvas",
|
||||
"mindMap",
|
||||
"spreadsheet"
|
||||
];
|
||||
|
||||
export interface NoteContextDataMap {
|
||||
toc: HeadingContext;
|
||||
pdfPages: {
|
||||
@@ -303,8 +312,12 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
return false;
|
||||
}
|
||||
|
||||
// "readOnly" is a state valid only for text/code notes
|
||||
if (!this.note || (this.note.type !== "text" && this.note.type !== "code")) {
|
||||
if (!this.note) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note types that support a read-only state (via the #readOnly label, source view, or auto-readonly).
|
||||
if (!READ_ONLY_CAPABLE_TYPES.includes(this.note.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -320,6 +333,11 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
return true;
|
||||
}
|
||||
|
||||
// Auto read-only based on content size is only configurable for text/code.
|
||||
if (this.note.type !== "text" && this.note.type !== "code") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store the initial decision about read-only status in the viewScope
|
||||
// This will be "remembered" until the viewScope is refreshed
|
||||
if (!this.viewScope) {
|
||||
|
||||
@@ -1069,6 +1069,10 @@ export default class FNote {
|
||||
return this.mime === "text/x-sqlite;schema=trilium";
|
||||
}
|
||||
|
||||
isMarkdown() {
|
||||
return this.type === "code" && (this.mime === "text/markdown" || this.mime === "text/x-markdown" || this.mime === "text/x-gfm");
|
||||
}
|
||||
|
||||
isTriliumScript() {
|
||||
return this.mime.startsWith("application/javascript");
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface MenuCommandItem<T> {
|
||||
title: string;
|
||||
command?: T;
|
||||
type?: string;
|
||||
mime?: string;
|
||||
/**
|
||||
* The icon to display in the menu item.
|
||||
*
|
||||
|
||||
101
apps/client/src/menus/launcher_button_context_menu.ts
Normal file
101
apps/client/src/menus/launcher_button_context_menu.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { ToggleInParentResponse } from "@triliumnext/commons";
|
||||
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import branchService from "../services/branches.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import server from "../services/server.js";
|
||||
import toast from "../services/toast.js";
|
||||
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
||||
|
||||
const VISIBLE_LAUNCHER_PARENTS = ["_lbVisibleLaunchers", "_lbMobileVisibleLaunchers"];
|
||||
|
||||
function getVisibleLauncherBranch(launcherNote: FNote) {
|
||||
return launcherNote.getParentBranches().find((b) => VISIBLE_LAUNCHER_PARENTS.includes(b.parentNoteId));
|
||||
}
|
||||
|
||||
function getBookmarkBranch(launcherNote: FNote) {
|
||||
return launcherNote.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
|
||||
}
|
||||
|
||||
async function removeFromLaunchBar(launcherNote: FNote) {
|
||||
const bookmarkBranch = getBookmarkBranch(launcherNote);
|
||||
if (bookmarkBranch) {
|
||||
// Individual bookmarks are represented via a branch under `_lbBookmarks`; removing them
|
||||
// from the launch bar is the same as unbookmarking the note.
|
||||
const resp = await server.put<ToggleInParentResponse>(
|
||||
`notes/${launcherNote.noteId}/toggle-in-parent/_lbBookmarks/false`
|
||||
);
|
||||
if (!resp.success && resp.message) {
|
||||
toast.showError(resp.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const launcherBranch = getVisibleLauncherBranch(launcherNote);
|
||||
if (!launcherBranch) return;
|
||||
|
||||
const isMobileLauncher = launcherBranch.parentNoteId === "_lbMobileVisibleLaunchers";
|
||||
// Branch IDs in the hidden subtree follow the `${parentNoteId}_${noteId}` convention,
|
||||
// so the branch linking `_lb(Mobile)?Root` to the "available" launchers root is predictable.
|
||||
const targetBranchId = isMobileLauncher
|
||||
? "_lbMobileRoot__lbMobileAvailableLaunchers"
|
||||
: "_lbRoot__lbAvailableLaunchers";
|
||||
await branchService.moveToParentNote([launcherBranch.branchId], targetBranchId);
|
||||
}
|
||||
|
||||
export function canRemoveFromLaunchBar(launcherNote: FNote | null | undefined) {
|
||||
if (!launcherNote) return false;
|
||||
return !!(getVisibleLauncherBranch(launcherNote) || getBookmarkBranch(launcherNote));
|
||||
}
|
||||
|
||||
export interface ShowLauncherContextMenuOptions<T extends string> {
|
||||
/** Menu items specific to this launcher (e.g. "Open in new tab" for note-based launchers). They appear above the "Remove from launch bar" item. */
|
||||
extraItems?: MenuItem<T>[];
|
||||
/** Handler for the {@link extraItems}. The "Remove from launch bar" item is handled internally and will not be forwarded. */
|
||||
onCommand?: (command: T | undefined) => void;
|
||||
}
|
||||
|
||||
const REMOVE_COMMAND = "__removeFromLaunchBar__";
|
||||
|
||||
/**
|
||||
* Displays the launch bar icon context menu. When the launcher can be removed (i.e. it is a direct
|
||||
* child of the visible launchers root or of `_lbBookmarks`), a "Remove from launch bar" entry is
|
||||
* appended. Extra items can be supplied to preserve launcher-specific actions (e.g. "Open in new tab").
|
||||
*/
|
||||
export async function showLauncherContextMenu<T extends string>(
|
||||
launcherNote: FNote | null | undefined,
|
||||
e: ContextMenuEvent,
|
||||
options: ShowLauncherContextMenuOptions<T> = {}
|
||||
) {
|
||||
e.preventDefault();
|
||||
|
||||
const items = [...(options.extraItems ?? [])] as MenuItem<string>[];
|
||||
|
||||
if (canRemoveFromLaunchBar(launcherNote)) {
|
||||
if (items.length > 0) {
|
||||
items.push({ kind: "separator" });
|
||||
}
|
||||
items.push({
|
||||
title: t("launcher_button_context_menu.remove_from_launch_bar"),
|
||||
command: REMOVE_COMMAND,
|
||||
uiIcon: "bx bx-x-circle"
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length === 0) return;
|
||||
|
||||
contextMenu.show<string>({
|
||||
x: e.pageX ?? 0,
|
||||
y: e.pageY ?? 0,
|
||||
items,
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (command === REMOVE_COMMAND) {
|
||||
if (launcherNote) {
|
||||
void removeFromLaunchBar(launcherNote);
|
||||
}
|
||||
return;
|
||||
}
|
||||
options.onCommand?.(command as T | undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -288,7 +288,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
return items.filter((row) => row !== null) as MenuItem<TreeCommandNames>[];
|
||||
}
|
||||
|
||||
async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
|
||||
async selectMenuItemHandler({ command, type, mime, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
|
||||
const notePath = treeService.getNotePath(this.node);
|
||||
|
||||
if (utils.isMobile()) {
|
||||
@@ -305,6 +305,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
target: "after",
|
||||
targetBranchId: this.node.data.branchId,
|
||||
type,
|
||||
mime,
|
||||
isProtected,
|
||||
templateNoteId
|
||||
});
|
||||
@@ -313,6 +314,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
type,
|
||||
mime,
|
||||
isProtected: this.node.data.isProtected,
|
||||
templateNoteId
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "./content_renderer.css";
|
||||
|
||||
import { normalizeMimeTypeForCKEditor, type TextRepresentationResponse } from "@triliumnext/commons";
|
||||
import { normalizeMimeTypeForCKEditor, renderToHtml, type TextRepresentationResponse } from "@triliumnext/commons";
|
||||
import DOMPurify from "dompurify";
|
||||
import { h, render } from "preact";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
|
||||
@@ -8,7 +9,7 @@ import FAttachment from "../entities/fattachment.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import renderText from "./content_renderer_text.js";
|
||||
import renderText, { postProcessRichContent, renderChildrenList } from "./content_renderer_text.js";
|
||||
import renderDoc from "./doc_renderer.js";
|
||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||
import openService from "./open.js";
|
||||
@@ -54,6 +55,8 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
||||
|
||||
if (type === "text" || type === "book") {
|
||||
await renderText(entity, $renderedContent, options);
|
||||
} else if (type === "markdown") {
|
||||
await renderMarkdown(entity, $renderedContent, options);
|
||||
} else if (type === "code") {
|
||||
await renderCode(entity, $renderedContent);
|
||||
} else if (["image", "canvas", "mindMap", "spreadsheet"].includes(type)) {
|
||||
@@ -119,6 +122,31 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a markdown note by converting its source to CKEditor-compatible HTML,
|
||||
* then running the same post-render pipeline as text notes (included notes,
|
||||
* math, reference links, Mermaid, code highlight) so the preview matches what
|
||||
* the user sees in the Markdown note type's preview pane.
|
||||
*/
|
||||
async function renderMarkdown(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions) {
|
||||
const blob = await note.getBlob();
|
||||
const source = blob?.content ?? "";
|
||||
|
||||
if (!source.trim()) {
|
||||
if (note instanceof FNote && !options.noChildrenList) {
|
||||
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const html = renderToHtml(source, note.title, {
|
||||
sanitize: (dirty) => DOMPurify.sanitize(dirty),
|
||||
wikiLink: { formatHref: (id) => `#root/${id}` }
|
||||
});
|
||||
$renderedContent.append($('<div class="ck-content">').html(html));
|
||||
await postProcessRichContent(note, $renderedContent, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
|
||||
*/
|
||||
@@ -330,6 +358,8 @@ function getRenderingType(entity: FNote | FAttachment) {
|
||||
|
||||
if (type === "file" && mime === "application/pdf") {
|
||||
type = "pdf";
|
||||
} else if (type === "code" && entity instanceof FNote && entity.isMarkdown()) {
|
||||
type = "markdown";
|
||||
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime) && !isIconPack) {
|
||||
type = "code";
|
||||
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
||||
|
||||
@@ -15,37 +15,47 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
|
||||
|
||||
if (blob && !isHtmlEmpty(blob.content)) {
|
||||
$renderedContent.append($('<div class="ck-content">').html(blob.content));
|
||||
|
||||
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
|
||||
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
|
||||
if (!options.noIncludedNotes) {
|
||||
await renderIncludedNotes($renderedContent[0], seenNoteIds);
|
||||
} else {
|
||||
$renderedContent.find("section.include-note").remove();
|
||||
}
|
||||
|
||||
if ($renderedContent.find("span.math-tex").length > 0) {
|
||||
renderMathInElement($renderedContent[0], { trust: true });
|
||||
}
|
||||
|
||||
const getNoteIdFromLink = (el: HTMLElement) => tree.getNoteIdFromUrl($(el).attr("href") || "");
|
||||
const referenceLinks = $renderedContent.find<HTMLAnchorElement>("a.reference-link");
|
||||
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
|
||||
await froca.getNotes(noteIdsToPrefetch);
|
||||
|
||||
for (const el of referenceLinks) {
|
||||
const innerSpan = document.createElement("span");
|
||||
await link.loadReferenceLinkTitle($(innerSpan), el.href);
|
||||
el.replaceChildren(innerSpan);
|
||||
}
|
||||
|
||||
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
|
||||
await formatCodeBlocks($renderedContent);
|
||||
await postProcessRichContent(note, $renderedContent, options);
|
||||
} else if (note instanceof FNote && !options.noChildrenList) {
|
||||
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the post-render passes that make CKEditor-compatible HTML fully
|
||||
* interactive: expand `<section class="include-note">`, render inline math and
|
||||
* Mermaid diagrams, rewrite reference-link titles, and highlight code blocks.
|
||||
* Assumes the caller has already appended the HTML inside a `.ck-content` child
|
||||
* of `$renderedContent`.
|
||||
*/
|
||||
export async function postProcessRichContent(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
|
||||
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
|
||||
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
|
||||
if (!options.noIncludedNotes) {
|
||||
await renderIncludedNotes($renderedContent[0], seenNoteIds);
|
||||
} else {
|
||||
$renderedContent.find("section.include-note").remove();
|
||||
}
|
||||
|
||||
if ($renderedContent.find("span.math-tex").length > 0) {
|
||||
renderMathInElement($renderedContent[0], { trust: true });
|
||||
}
|
||||
|
||||
const getNoteIdFromLink = (el: HTMLElement) => tree.getNoteIdFromUrl($(el).attr("href") || "");
|
||||
const referenceLinks = $renderedContent.find<HTMLAnchorElement>("a.reference-link");
|
||||
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
|
||||
await froca.getNotes(noteIdsToPrefetch);
|
||||
|
||||
for (const el of referenceLinks) {
|
||||
const innerSpan = document.createElement("span");
|
||||
await link.loadReferenceLinkTitle($(innerSpan), el.href);
|
||||
el.replaceChildren(innerSpan);
|
||||
}
|
||||
|
||||
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
|
||||
await formatCodeBlocks($renderedContent);
|
||||
}
|
||||
|
||||
async function renderIncludedNotes(contentEl: HTMLElement, seenNoteIds: Set<string>) {
|
||||
// TODO: Consider duplicating with server's share/content_renderer.ts.
|
||||
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
|
||||
@@ -101,19 +111,107 @@ export async function rewriteMermaidDiagramsInContainer(container: HTMLDivElemen
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-container cache of rendered mermaid SVG keyed by diagram source text.
|
||||
* Populated after each successful render; reused on subsequent renders to
|
||||
* avoid flicker when the preview HTML is regenerated (e.g. live markdown
|
||||
* editing). Entries for diagrams no longer present in the container are
|
||||
* evicted on each run so the cache can't grow unbounded.
|
||||
*/
|
||||
const mermaidSvgCache = new WeakMap<HTMLElement, Map<string, string>>();
|
||||
|
||||
/**
|
||||
* Per-container, ordered snapshot of the most recently rendered SVGs. Used as
|
||||
* a positional placeholder so edits to a diagram's source keep the previous
|
||||
* SVG visible while the new one renders offscreen.
|
||||
*/
|
||||
const mermaidLastRenderedByPosition = new WeakMap<HTMLElement, string[]>();
|
||||
|
||||
export async function applyInlineMermaid(container: HTMLDivElement) {
|
||||
// Initialize mermaid
|
||||
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
|
||||
if (!nodes.length) {
|
||||
mermaidLastRenderedByPosition.delete(container);
|
||||
return;
|
||||
}
|
||||
|
||||
let cache = mermaidSvgCache.get(container);
|
||||
if (!cache) {
|
||||
cache = new Map();
|
||||
mermaidSvgCache.set(container, cache);
|
||||
}
|
||||
const lastRendered = mermaidLastRenderedByPosition.get(container) ?? [];
|
||||
|
||||
// Decide per node: exact cache hit → paint final SVG; source changed →
|
||||
// paint the previous SVG (by position) as a placeholder and queue an
|
||||
// offscreen re-render. This way the user keeps seeing the old diagram
|
||||
// until mermaid has finished producing the new one.
|
||||
const pending: Array<{ visible: HTMLElement; source: string }> = [];
|
||||
const seenSources = new Set<string>();
|
||||
for (const [ index, node ] of nodes.entries()) {
|
||||
const source = (node.textContent ?? "").trim();
|
||||
seenSources.add(source);
|
||||
|
||||
const cached = cache.get(source);
|
||||
if (cached) {
|
||||
node.innerHTML = cached;
|
||||
node.setAttribute("data-processed", "true");
|
||||
continue;
|
||||
}
|
||||
|
||||
pending.push({ visible: node, source });
|
||||
const placeholder = lastRendered[index];
|
||||
if (placeholder) {
|
||||
node.innerHTML = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
// Evict cache entries whose source is no longer present.
|
||||
for (const key of [ ...cache.keys() ]) {
|
||||
if (!seenSources.has(key)) cache.delete(key);
|
||||
}
|
||||
|
||||
if (!pending.length) {
|
||||
mermaidLastRenderedByPosition.set(container, nodes.map((n) => n.innerHTML));
|
||||
return;
|
||||
}
|
||||
|
||||
const mermaid = (await import("mermaid")).default;
|
||||
mermaid.initialize(getMermaidConfig());
|
||||
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
|
||||
|
||||
// Render clones offscreen so the visible nodes keep showing the placeholder
|
||||
// until the new SVG is ready. Keeps mermaid away from our placeholder SVG
|
||||
// (which would otherwise confuse its text-based parser).
|
||||
const offscreen = document.createElement("div");
|
||||
offscreen.style.cssText = "position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;visibility:hidden;";
|
||||
document.body.appendChild(offscreen);
|
||||
|
||||
const pairs = pending.map(({ visible, source }) => {
|
||||
const clone = document.createElement("div");
|
||||
clone.className = "mermaid-diagram";
|
||||
clone.textContent = source;
|
||||
offscreen.appendChild(clone);
|
||||
return { visible, clone, source };
|
||||
});
|
||||
|
||||
try {
|
||||
await mermaid.run({ nodes });
|
||||
await mermaid.run({ nodes: pairs.map((p) => p.clone) });
|
||||
for (const { visible, clone, source } of pairs) {
|
||||
if (clone.getAttribute("data-processed") !== "true") continue;
|
||||
const svg = clone.innerHTML;
|
||||
visible.innerHTML = svg;
|
||||
visible.setAttribute("data-processed", "true");
|
||||
cache.set(source, svg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
} finally {
|
||||
offscreen.remove();
|
||||
}
|
||||
|
||||
mermaidLastRenderedByPosition.set(container, nodes.map((n) => n.innerHTML));
|
||||
}
|
||||
|
||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
|
||||
export async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
|
||||
let childNoteIds = note.getChildNoteIds();
|
||||
|
||||
if (!childNoteIds.length) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
|
||||
// The default note type (always the first item)
|
||||
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
|
||||
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true },
|
||||
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true, isNew: true },
|
||||
|
||||
// Text notes group
|
||||
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
|
||||
@@ -49,6 +49,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
|
||||
// Code notes
|
||||
{ type: "code", mime: "text/plain", title: t("note_types.code"), icon: "bx-code" },
|
||||
{ type: "code", mime: "text/x-markdown", title: t("note_types.markdown"), icon: "bxl-markdown", isNew: true },
|
||||
|
||||
// Reserved types (cannot be created by the user)
|
||||
{ type: "contentWidget", mime: "", title: t("note_types.widget"), reserved: true },
|
||||
@@ -100,6 +101,7 @@ function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandName
|
||||
title: nt.title,
|
||||
command,
|
||||
type: nt.type,
|
||||
mime: nt.mime,
|
||||
uiIcon: `bx ${nt.icon}`,
|
||||
badges: []
|
||||
};
|
||||
|
||||
@@ -131,11 +131,16 @@ button.tn-low-profile:hover {
|
||||
color: var(--icon-button-color);
|
||||
}
|
||||
|
||||
:root .btn-group .icon-action:last-child {
|
||||
:root .btn-group .icon-action:not(:first-child) {
|
||||
border-top-left-radius: unset !important;
|
||||
border-bottom-left-radius: unset !important;
|
||||
}
|
||||
|
||||
:root .btn-group .icon-action:not(:last-child) {
|
||||
border-top-right-radius: unset !important;
|
||||
border-bottom-right-radius: unset !important;
|
||||
}
|
||||
|
||||
.btn-group .tn-tool-button + .tn-tool-button {
|
||||
margin-inline-start: 4px !important;
|
||||
}
|
||||
|
||||
@@ -635,6 +635,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
|
||||
inset-inline-end: 0.35em;
|
||||
}
|
||||
|
||||
.ck-content h1,
|
||||
.ck-content h2,
|
||||
.ck-content h3,
|
||||
.ck-content h4,
|
||||
@@ -694,7 +695,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--link-selection-outline-color);
|
||||
background: var(--link-hover-background);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reference link */
|
||||
|
||||
|
||||
@@ -90,15 +90,20 @@
|
||||
"delete_notes": {
|
||||
"close": "关闭",
|
||||
"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": "取消",
|
||||
"title": "删除笔记",
|
||||
"delete": "删除",
|
||||
"clones_label": "克隆"
|
||||
"clones_label": "克隆",
|
||||
"delete_clones_description_other": "同时删除 {{count}} 个其他克隆。此操作可在最近的更改中撤销。",
|
||||
"erase_notes_label": "永久擦除",
|
||||
"table_note_with_relation": "关联笔记",
|
||||
"table_relation": "关联",
|
||||
"table_points_to": "指向(已删除)"
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "导出笔记",
|
||||
@@ -209,7 +214,8 @@
|
||||
"box_size_small": "小型 (显示大约10行)",
|
||||
"box_size_medium": "中型 (显示大约30行)",
|
||||
"box_size_full": "完整显示(完整文本框)",
|
||||
"button_include": "包含笔记"
|
||||
"button_include": "包含笔记",
|
||||
"box_size_expandable": "可展开(默认折叠)"
|
||||
},
|
||||
"info": {
|
||||
"modalTitle": "信息消息",
|
||||
@@ -420,7 +426,9 @@
|
||||
"print_landscape": "导出为 PDF 时,将页面方向更改为横向而不是纵向。",
|
||||
"print_page_size": "导出为 PDF 时,更改页面大小。支持的值:<code>A0</code>、<code>A1</code>、<code>A2</code>、<code>A3</code>、<code>A4</code>、<code>A5</code>、<code>A6</code>、<code>Legal</code>、<code>Letter</code>、<code>Tabloid</code>、<code>Ledger</code>。",
|
||||
"color_type": "颜色",
|
||||
"textarea": "多行文本"
|
||||
"textarea": "多行文本",
|
||||
"print_scale": "导出为 PDF 时,更改渲染内容的比例。取值范围从 0.1 (10%) 到 2 (200%),默认值为 1 (100%)。",
|
||||
"print_margins": "导出为 PDF 时,设置页面边距。可以使用 <code>default</code>、<code>none</code>、<code>minimum</code> 或以毫米为单位的自定义值,例如 <code>top,right,bottom,left</code>。"
|
||||
},
|
||||
"attribute_editor": {
|
||||
"help_text_body1": "要添加标签,只需输入例如 <code>#rock</code> 或者如果您还想添加值,则例如 <code>#year = 2020</code>",
|
||||
@@ -683,7 +691,12 @@
|
||||
"export_as_image": "导出为图像",
|
||||
"export_as_image_png": "PNG(栅格)",
|
||||
"export_as_image_svg": "SVG(矢量图)",
|
||||
"view_ocr_text": "查看 OCR 文本"
|
||||
"view_ocr_text": "查看 OCR 文本",
|
||||
"word_wrap": "自动换行",
|
||||
"word_wrap_auto": "自动",
|
||||
"word_wrap_auto_description": "遵循全局设置",
|
||||
"word_wrap_on": "开启",
|
||||
"word_wrap_off": "关闭"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
|
||||
@@ -1050,15 +1063,17 @@
|
||||
"title": "检查一致性",
|
||||
"find_and_fix_button": "查找并修复一致性问题",
|
||||
"finding_and_fixing_message": "正在查找并修复一致性问题...",
|
||||
"issues_fixed_message": "一致性问题应该已被修复。"
|
||||
"issues_fixed_message": "一致性问题应该已被修复。",
|
||||
"find_and_fix_label": "查找并修复一致性问题",
|
||||
"find_and_fix_description": "扫描并自动修复数据库中的任何数据一致性问题。"
|
||||
},
|
||||
"database_anonymization": {
|
||||
"title": "数据库匿名化",
|
||||
"full_anonymization": "完全匿名化",
|
||||
"full_anonymization_description": "此操作将创建一个新的数据库副本并进行匿名化处理(删除所有笔记内容,仅保留结构和一些非敏感元数据),用来分享到网上做调试而不用担心泄漏您的个人资料。",
|
||||
"full_anonymization_description": "创建数据库副本,移除所有笔记内容,仅保留数据库结构和非敏感元数据。在调试问题时,可安全地在线共享。",
|
||||
"save_fully_anonymized_database": "保存完全匿名化的数据库",
|
||||
"light_anonymization": "轻度匿名化",
|
||||
"light_anonymization_description": "此操作将创建一个新的数据库副本,并对其进行轻度匿名化处理——仅删除所有笔记的内容,但保留标题和属性。此外,自定义 JS 前端/后端脚本笔记和自定义小部件将保留。这提供了更多上下文以调试问题。",
|
||||
"light_anonymization_description": "创建一个副本,其中移除笔记内容,但保留标题、属性和自定义脚本/小部件。这有助于提供更多调试信息。",
|
||||
"choose_anonymization": "您可以自行决定是提供完全匿名化还是轻度匿名化的数据库。即使是完全匿名化的数据库也非常有用,但在某些情况下,轻度匿名化的数据库可以加快错误识别和修复的过程。",
|
||||
"save_lightly_anonymized_database": "保存轻度匿名化的数据库",
|
||||
"existing_anonymized_databases": "现有的匿名化数据库",
|
||||
@@ -1067,14 +1082,17 @@
|
||||
"error_creating_anonymized_database": "无法创建匿名化数据库,请检查后端日志以获取详细信息",
|
||||
"successfully_created_fully_anonymized_database": "成功创建完全匿名化的数据库,路径为 {{anonymizedFilePath}}",
|
||||
"successfully_created_lightly_anonymized_database": "成功创建轻度匿名化的数据库,路径为 {{anonymizedFilePath}}",
|
||||
"no_anonymized_database_yet": "尚无匿名化数据库。"
|
||||
"no_anonymized_database_yet": "尚无匿名化数据库。",
|
||||
"description": "创建数据库的匿名副本,以便在调试问题时与开发人员共享,而不会泄露个人数据。"
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"title": "数据库完整性检查",
|
||||
"check_button": "检查数据库完整性",
|
||||
"checking_integrity": "正在检查数据库完整性...",
|
||||
"integrity_check_succeeded": "完整性检查成功 - 未发现问题。",
|
||||
"integrity_check_failed": "完整性检查失败: {{results}}"
|
||||
"integrity_check_failed": "完整性检查失败: {{results}}",
|
||||
"check_integrity_label": "检查数据库完整性",
|
||||
"check_integrity_description": "验证 SQLite 数据库是否已损坏。"
|
||||
},
|
||||
"sync": {
|
||||
"title": "同步",
|
||||
@@ -1084,19 +1102,25 @@
|
||||
"filling_entity_changes": "正在填充实体变更行...",
|
||||
"sync_rows_filled_successfully": "同步行填充成功",
|
||||
"finished-successfully": "同步已完成。",
|
||||
"failed": "同步失败:{{message}}"
|
||||
"failed": "同步失败:{{message}}",
|
||||
"force_full_sync_label": "强制全量同步",
|
||||
"force_full_sync_description": "触发与同步服务器的全量同步,重新上传所有更改。",
|
||||
"fill_entity_changes_description": "重建实体变更记录。如果同步过程中缺少某些变更,请使用此功能。",
|
||||
"fill_entity_changes_label": "填充实体变更"
|
||||
},
|
||||
"vacuum_database": {
|
||||
"title": "数据库清理",
|
||||
"description": "这会重建数据库,通常会减少占用空间,不会删除数据。",
|
||||
"button_text": "清理数据库",
|
||||
"vacuuming_database": "正在清理数据库...",
|
||||
"database_vacuumed": "数据库已清理"
|
||||
"database_vacuumed": "数据库已清理",
|
||||
"vacuum_label": "数据库清理",
|
||||
"vacuum_description": "重建数据库以减小文件大小。数据不会发生任何变化。"
|
||||
},
|
||||
"fonts": {
|
||||
"theme_defined": "跟随主题",
|
||||
"fonts": "字体",
|
||||
"main_font": "主字体",
|
||||
"main_font": "界面字体",
|
||||
"font_family": "字体系列",
|
||||
"size": "大小",
|
||||
"note_tree_font": "笔记树字体",
|
||||
@@ -1111,7 +1135,9 @@
|
||||
"serif": "衬线",
|
||||
"sans-serif": "无衬线",
|
||||
"monospace": "等宽",
|
||||
"system-default": "系统默认"
|
||||
"system-default": "系统默认",
|
||||
"custom_fonts": "使用自定义字体",
|
||||
"preview": "预览"
|
||||
},
|
||||
"max_content_width": {
|
||||
"title": "内容宽度",
|
||||
@@ -2292,5 +2318,35 @@
|
||||
"no_providers_configured": "尚未配置任何供应商。",
|
||||
"provider_name": "名称",
|
||||
"provider_type": "供应商"
|
||||
},
|
||||
"revisions": {
|
||||
"note_revisions": "笔记修订",
|
||||
"delete_all_revisions": "删除此笔记的所有修订版本",
|
||||
"delete_all_button": "删除所有修订版本",
|
||||
"help_title": "关于笔记修订的帮助",
|
||||
"confirm_delete_all": "是否要删除此笔记的所有修订版本?",
|
||||
"no_revisions": "这篇笔记目前还没有修改……",
|
||||
"restore_button": "恢复",
|
||||
"diff_on": "显示差异",
|
||||
"diff_off": "显示内容",
|
||||
"diff_on_hint": "点击显示笔记来源差异",
|
||||
"diff_off_hint": "点击显示笔记内容",
|
||||
"diff_not_available": "差异数据不可用。",
|
||||
"confirm_restore": "是否恢复此版本?这将用此版本覆盖笔记的当前标题和内容。",
|
||||
"delete_button": "删除",
|
||||
"confirm_delete": "您要删除此修订吗?",
|
||||
"revisions_deleted": "笔记修订已被删除。",
|
||||
"revision_restored": "笔记修订已恢复。",
|
||||
"revision_deleted": "笔记修订已删除。",
|
||||
"snapshot_interval": "笔记修订快照间隔:{{seconds}}秒。",
|
||||
"maximum_revisions": "笔记修订快照限制:{{number}}。",
|
||||
"settings": "笔记修订设置",
|
||||
"download_button": "下载",
|
||||
"mime": "MIME 类型: ",
|
||||
"file_size": "文件大小:",
|
||||
"preview_not_available": "无法预览此类型的笔记。"
|
||||
},
|
||||
"database": {
|
||||
"title": "数据库"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1681,6 +1681,7 @@
|
||||
"note_types": {
|
||||
"text": "Text",
|
||||
"code": "Code",
|
||||
"markdown": "Markdown",
|
||||
"saved-search": "Saved Search",
|
||||
"relation-map": "Relation Map",
|
||||
"note-map": "Note Map",
|
||||
@@ -1938,6 +1939,9 @@
|
||||
"move-to-available-launchers": "Move to available launchers",
|
||||
"duplicate-launcher": "Duplicate launcher <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"launcher_button_context_menu": {
|
||||
"remove_from_launch_bar": "Remove from launch bar"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Code Blocks",
|
||||
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
|
||||
@@ -2082,6 +2086,11 @@
|
||||
"unlock-editing": "Unlock editing",
|
||||
"lock-editing": "Lock editing"
|
||||
},
|
||||
"display_mode": {
|
||||
"source": "Source view",
|
||||
"split": "Split view",
|
||||
"preview": "Preview"
|
||||
},
|
||||
"png_export_button": {
|
||||
"button_title": "Export diagram as PNG"
|
||||
},
|
||||
|
||||
@@ -2446,5 +2446,38 @@
|
||||
"destination_pdf": "PDF として保存",
|
||||
"destination_printers": "プリンター",
|
||||
"destination_default": "デフォルト"
|
||||
},
|
||||
"revisions": {
|
||||
"note_revisions": "ノートの変更履歴",
|
||||
"delete_all_revisions": "このノートのすべての変更履歴を削除",
|
||||
"delete_all_button": "すべての変更履歴を削除",
|
||||
"help_title": "ノートの変更履歴に関するヘルプ",
|
||||
"confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
|
||||
"no_revisions": "このノートにはまだ変更履歴がありません...",
|
||||
"restore_button": "復元",
|
||||
"diff_on": "差分を表示",
|
||||
"diff_off": "内容を表示",
|
||||
"diff_on_hint": "クリックしてノートのソース差分を表示",
|
||||
"diff_off_hint": "クリックしてノートの内容を表示",
|
||||
"diff_not_available": "差分は利用できません。",
|
||||
"confirm_restore": "この変更履歴を復元しますか? 復元すると、ノートの現在のタイトルと内容がこの変更履歴で上書きされます。",
|
||||
"delete_button": "削除",
|
||||
"confirm_delete": "この変更履歴を削除しますか?",
|
||||
"revisions_deleted": "ノートの変更履歴が削除されました。",
|
||||
"revision_restored": "ノートの変更履歴が復元されました。",
|
||||
"revision_deleted": "ノートの変更履歴が削除されました。",
|
||||
"snapshot_interval": "ノートの変更履歴スナップショット間隔: {{seconds}} 秒。",
|
||||
"maximum_revisions": "ノートの変更履歴スナップショット制限: {{number}}。",
|
||||
"settings": "ノートの変更履歴設定",
|
||||
"download_button": "ダウンロード",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "ファイルサイズ:",
|
||||
"preview_not_available": "このノートタイプではプレビューは利用できません。"
|
||||
},
|
||||
"revisions_snapshot": {
|
||||
"title": "ノートの変更履歴"
|
||||
},
|
||||
"launcher_button_context_menu": {
|
||||
"remove_from_launch_bar": "ランチャーバーから削除"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import tree from "../services/tree";
|
||||
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
|
||||
import { ViewTypeOptions } from "./collections/interface";
|
||||
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
|
||||
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
|
||||
import { ButtonGroup } from "./react/Button";
|
||||
import { useIsNoteReadOnly, useNoteLabel, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
|
||||
import NoteLink from "./react/NoteLink";
|
||||
import RawHtml from "./react/RawHtml";
|
||||
|
||||
@@ -47,8 +48,9 @@ export type FloatingButtonsList = ((context: FloatingButtonContext) => false | V
|
||||
|
||||
export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
RefreshBackendLogButton,
|
||||
SwitchSplitOrientationButton,
|
||||
ToggleReadOnlyButton,
|
||||
SwitchSplitOrientationButton,
|
||||
DisplayModeSwitcher,
|
||||
EditButton,
|
||||
ShowTocWidgetButton,
|
||||
ShowHighlightsListWidgetButton,
|
||||
@@ -80,9 +82,13 @@ function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefault
|
||||
}
|
||||
|
||||
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode;
|
||||
const [ displayMode ] = useNoteLabel(note, "displayMode");
|
||||
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
|
||||
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
|
||||
const effectiveMode = displayMode === "source" || displayMode === "split" || displayMode === "preview"
|
||||
? displayMode
|
||||
: isReadOnly ? "preview" : "split";
|
||||
const isEnabled = note.type === "mermaid" && note.isContentAvailable() && effectiveMode === "split" && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
|
||||
@@ -94,7 +100,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
|
||||
function ToggleReadOnlyButton({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || isSavedSqlite)
|
||||
const isEnabled = ([ "mindMap", "canvas", "spreadsheet" ].includes(note.type) || isSavedSqlite)
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
@@ -104,6 +110,33 @@ function ToggleReadOnlyButton({ note, isDefaultViewMode }: FloatingButtonContext
|
||||
/>;
|
||||
}
|
||||
|
||||
function DisplayModeSwitcher({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ displayMode, setDisplayMode ] = useNoteLabel(note, "displayMode");
|
||||
const isEnabled = (note.isMarkdown() || note.type === "mermaid") && note.isContentAvailable() && isDefaultViewMode;
|
||||
if (!isEnabled) return false;
|
||||
|
||||
const mode = displayMode === "source" || displayMode === "preview" ? displayMode : "split";
|
||||
const buttons: Array<{ value: "source" | "split" | "preview"; icon: string; text: string }> = [
|
||||
{ value: "source", icon: "bx bx-code", text: t("display_mode.source") },
|
||||
{ value: "split", icon: "bx bxs-dock-left", text: t("display_mode.split") },
|
||||
{ value: "preview", icon: "bx bx-show", text: t("display_mode.preview") }
|
||||
];
|
||||
|
||||
return (
|
||||
<ButtonGroup size="sm">
|
||||
{buttons.map(({ value, icon, text }) => (
|
||||
<FloatingButton
|
||||
key={value}
|
||||
icon={icon}
|
||||
text={text}
|
||||
active={mode === value}
|
||||
onClick={() => setDisplayMode(value)}
|
||||
/>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function EditButton({ note, noteContext }: FloatingButtonContext) {
|
||||
const [animationClass, setAnimationClass] = useState("");
|
||||
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||
|
||||
@@ -352,7 +352,9 @@ export async function getExtendedWidgetType(note: FNote | null | undefined, note
|
||||
resultingType = "readOnlyText";
|
||||
} else if (note.isTriliumSqlite()) {
|
||||
resultingType = "sqlConsole";
|
||||
} else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) {
|
||||
} else if (note.isMarkdown()) {
|
||||
resultingType = "markdown";
|
||||
} else if (type === "code" && (await noteContext?.isReadOnly())) {
|
||||
resultingType = "readOnlyCode";
|
||||
} else if (type === "text") {
|
||||
resultingType = "editableText";
|
||||
|
||||
@@ -63,7 +63,7 @@ export default class FindInText {
|
||||
const findResultElement = editorEl?.querySelectorAll(".ck-find-result");
|
||||
const scrollingContainer = editorEl?.closest('.scrolling-container');
|
||||
const containerTop = scrollingContainer?.getBoundingClientRect().top ?? 0;
|
||||
const closestIndex = Array.from(findResultElement ?? []).findIndex((el) => el.getBoundingClientRect().top >= containerTop);
|
||||
const closestIndex = Array.from(findResultElement ?? []).findIndex((el: Element) => el.getBoundingClientRect().top >= containerTop);
|
||||
currentFound = closestIndex >= 0 ? closestIndex : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import { useChildNotes, useNote, useNoteIcon, useNoteLabelBoolean } from "../rea
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import ResponsiveContainer from "../react/ResponsiveContainer";
|
||||
import { CustomNoteLauncher, launchCustomNoteLauncher } from "./GenericButtons";
|
||||
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import { LaunchBarContext, LaunchBarDropdownButton, launcherContextMenuHandler, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
const PARENT_NOTE_ID = "_lbBookmarks";
|
||||
|
||||
export default function BookmarkButtons() {
|
||||
export default function BookmarkButtons({ launcherNote }: LauncherNoteProps) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
const style = useMemo<CSSProperties>(() => ({
|
||||
display: "flex",
|
||||
@@ -22,20 +22,27 @@ export default function BookmarkButtons() {
|
||||
contain: "none"
|
||||
}), [ isHorizontalLayout ]);
|
||||
const childNotes = useChildNotes(PARENT_NOTE_ID);
|
||||
const bookmarks = childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />);
|
||||
const showContextMenu = launcherContextMenuHandler(launcherNote);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
desktop={
|
||||
<div style={style}>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
<div
|
||||
style={style}
|
||||
// Only trigger on empty container area; individual bookmark buttons handle their own context menu.
|
||||
onContextMenu={(e) => e.target === e.currentTarget && showContextMenu?.(e)}
|
||||
>
|
||||
{bookmarks}
|
||||
</div>
|
||||
}
|
||||
mobile={
|
||||
<LaunchBarDropdownButton
|
||||
launcherNote={launcherNote}
|
||||
icon="bx bx-bookmark"
|
||||
title={t("bookmark_buttons.bookmarks")}
|
||||
>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
{bookmarks}
|
||||
</LaunchBarDropdownButton>
|
||||
}
|
||||
/>
|
||||
@@ -90,6 +97,7 @@ function BookmarkFolder({ note }: { note: FNote }) {
|
||||
|
||||
return (
|
||||
<LaunchBarDropdownButton
|
||||
launcherNote={note}
|
||||
icon={icon}
|
||||
title={title}
|
||||
>
|
||||
|
||||
@@ -58,6 +58,7 @@ export default function CalendarWidget({ launcherNote }: LauncherNoteProps) {
|
||||
|
||||
return (
|
||||
<LaunchBarDropdownButton
|
||||
launcherNote={launcherNote}
|
||||
icon={icon} title={title}
|
||||
onShown={async () => {
|
||||
const dateNote = appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { showLauncherContextMenu } from "../../menus/launcher_button_context_menu";
|
||||
import link_context_menu from "../../menus/link_context_menu";
|
||||
import { isCtrlKey } from "../../services/utils";
|
||||
import { useGlobalShortcut, useNoteLabel } from "../react/hooks";
|
||||
@@ -13,7 +14,7 @@ export function CustomNoteLauncher(props: {
|
||||
getHoistedNoteId?: (launcherNote: FNote) => string | null;
|
||||
keyboardShortcut?: string;
|
||||
}) {
|
||||
const { launcherNote, getTargetNoteId } = props;
|
||||
const { launcherNote, getTargetNoteId, getHoistedNoteId } = props;
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
|
||||
const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => {
|
||||
@@ -31,11 +32,20 @@ export function CustomNoteLauncher(props: {
|
||||
onClick={launch}
|
||||
onAuxClick={launch}
|
||||
onContextMenu={async evt => {
|
||||
// Must preventDefault synchronously — awaiting getTargetNoteId first would let the
|
||||
// native browser context menu open before showLauncherContextMenu gets a chance to.
|
||||
evt.preventDefault();
|
||||
const targetNoteId = await getTargetNoteId(launcherNote);
|
||||
if (targetNoteId) {
|
||||
link_context_menu.openContextMenu(targetNoteId, evt);
|
||||
}
|
||||
const hoistedNoteId = getHoistedNoteId?.(launcherNote) ?? null;
|
||||
const linkItems = targetNoteId ? link_context_menu.getItems(evt) : [];
|
||||
await showLauncherContextMenu<CommandNames>(launcherNote, evt, {
|
||||
extraItems: linkItems,
|
||||
onCommand: (command) => {
|
||||
if (command && targetNoteId) {
|
||||
link_context_menu.handleLinkContextMenuItem(command, evt, targetNoteId, {}, hoistedNoteId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMemo } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
|
||||
import { showLauncherContextMenu } from "../../menus/launcher_button_context_menu";
|
||||
import froca from "../../services/froca";
|
||||
import link from "../../services/link";
|
||||
import tree from "../../services/tree";
|
||||
@@ -25,46 +26,63 @@ export default function HistoryNavigationButton({ launcherNote, command }: Histo
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand={command}
|
||||
onContextMenu={webContents ? handleHistoryContextMenu(webContents) : undefined}
|
||||
onContextMenu={async (e) => {
|
||||
// Prevent the native menu synchronously before awaiting history items.
|
||||
e.preventDefault();
|
||||
const items = webContents ? await getHistoryItems(webContents) : [];
|
||||
showLauncherContextMenu<string>(launcherNote, e, {
|
||||
extraItems: items,
|
||||
onCommand: (cmd) => {
|
||||
if (cmd && webContents) {
|
||||
webContents.navigationHistory.goToIndex(parseInt(cmd, 10));
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getHistoryItems(webContents: WebContents): Promise<MenuCommandItem<string>[]> {
|
||||
if (webContents.navigationHistory.length() < 2) return [];
|
||||
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const history = webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { noteId, notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!noteId || !notePath) continue;
|
||||
|
||||
const title = await tree.getNotePathTitle(notePath);
|
||||
const index = parseInt(idx, 10);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
checked: index === activeIndex,
|
||||
enabled: index !== activeIndex,
|
||||
uiIcon: note?.getIcon()
|
||||
});
|
||||
}
|
||||
|
||||
items.reverse();
|
||||
|
||||
if (items.length > HISTORY_LIMIT) {
|
||||
items = items.slice(0, HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function handleHistoryContextMenu(webContents: WebContents) {
|
||||
return async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!webContents || webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const history = webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { noteId, notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!noteId || !notePath) continue;
|
||||
|
||||
const title = await tree.getNotePathTitle(notePath);
|
||||
const index = parseInt(idx, 10);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
checked: index === activeIndex,
|
||||
enabled: index !== activeIndex,
|
||||
uiIcon: note?.getIcon()
|
||||
});
|
||||
}
|
||||
|
||||
items.reverse();
|
||||
|
||||
if (items.length > HISTORY_LIMIT) {
|
||||
items = items.slice(0, HISTORY_LIMIT);
|
||||
}
|
||||
const items = await getHistoryItems(webContents);
|
||||
if (items.length === 0) return;
|
||||
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
|
||||
@@ -83,13 +83,13 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
|
||||
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
|
||||
|
||||
return <SpacerWidget baseSize={baseSize} growthFactor={growthFactor} />;
|
||||
return <SpacerWidget launcherNote={note} baseSize={baseSize} growthFactor={growthFactor} />;
|
||||
case "bookmarks":
|
||||
return <BookmarkButtons />;
|
||||
return <BookmarkButtons launcherNote={note} />;
|
||||
case "protectedSession":
|
||||
return <ProtectedSessionStatusWidget />;
|
||||
return <ProtectedSessionStatusWidget launcherNote={note} />;
|
||||
case "syncStatus":
|
||||
return <SyncStatus />;
|
||||
return <SyncStatus launcherNote={note} />;
|
||||
case "backInHistoryButton":
|
||||
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />;
|
||||
case "forwardInHistoryButton":
|
||||
@@ -97,11 +97,11 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
case "todayInJournal":
|
||||
return <TodayLauncher launcherNote={note} />;
|
||||
case "quickSearch":
|
||||
return <QuickSearchLauncherWidget />;
|
||||
return <QuickSearchLauncherWidget launcherNote={note} />;
|
||||
case "mobileTabSwitcher":
|
||||
return <TabSwitcher />;
|
||||
return <TabSwitcher launcherNote={note} />;
|
||||
case "sidebarChat":
|
||||
return isExperimentalFeatureEnabled("llm") ? <SidebarChatButton /> : undefined;
|
||||
return isExperimentalFeatureEnabled("llm") ? <SidebarChatButton launcherNote={note} /> : undefined;
|
||||
default:
|
||||
console.warn(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import QuickSearchWidget from "../quick_search";
|
||||
import { useGlobalShortcut, useLegacyWidget, useNoteLabel, useNoteRelationTarget } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { CustomNoteLauncher } from "./GenericButtons";
|
||||
import { LaunchBarActionButton, LaunchBarContext, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import { LaunchBarActionButton, LaunchBarContext, launcherContextMenuHandler, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
export function CommandButton({ launcherNote }: LauncherNoteProps) {
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
@@ -22,6 +22,7 @@ export function CommandButton({ launcherNote }: LauncherNoteProps) {
|
||||
|
||||
return command && (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand={command as CommandNames}
|
||||
@@ -74,6 +75,7 @@ export function ScriptLauncher({ launcherNote }: LauncherNoteProps) {
|
||||
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon={icon}
|
||||
text={title}
|
||||
onClick={launch}
|
||||
@@ -93,7 +95,7 @@ export function TodayLauncher({ launcherNote }: LauncherNoteProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function QuickSearchLauncherWidget() {
|
||||
export function QuickSearchLauncherWidget({ launcherNote }: LauncherNoteProps) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
const widget = useMemo(() => new QuickSearchWidget(), []);
|
||||
const parentComponent = useContext(ParentComponent) as BasicWidget | null;
|
||||
@@ -101,7 +103,7 @@ export function QuickSearchLauncherWidget() {
|
||||
parentComponent?.contentSized();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onContextMenu={launcherContextMenuHandler(launcherNote)}>
|
||||
{isEnabled && <LegacyWidgetRenderer widget={widget} />}
|
||||
</div>
|
||||
);
|
||||
@@ -136,7 +138,7 @@ export function CustomWidget({ launcherNote }: LauncherNoteProps) {
|
||||
}, [ widgetNote ]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onContextMenu={launcherContextMenuHandler(launcherNote)}>
|
||||
{widget && (
|
||||
("type" in widget && widget.type === "preact-launcher-widget")
|
||||
? <ReactWidgetRenderer widget={widget as LauncherWidgetDefinitionWithType} />
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import { LaunchBarActionButton } from "./launch_bar_widgets";
|
||||
import { LaunchBarActionButton, LauncherNoteProps } from "./launch_bar_widgets";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
export default function ProtectedSessionStatusWidget() {
|
||||
export default function ProtectedSessionStatusWidget({ launcherNote }: LauncherNoteProps) {
|
||||
const protectedSessionAvailable = useProtectedSessionAvailable();
|
||||
|
||||
return (
|
||||
protectedSessionAvailable ? (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon="bx bx-check-shield"
|
||||
text={t("protected_session_status.active")}
|
||||
triggerCommand="leaveProtectedSession"
|
||||
/>
|
||||
) : (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon="bx bx-shield-quarter"
|
||||
text={t("protected_session_status.inactive")}
|
||||
triggerCommand="enterProtectedSession"
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useCallback } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import { t } from "../../services/i18n";
|
||||
import { LaunchBarActionButton } from "./launch_bar_widgets";
|
||||
import { LaunchBarActionButton, LauncherNoteProps } from "./launch_bar_widgets";
|
||||
|
||||
/**
|
||||
* Launcher button to open the sidebar (which contains the chat).
|
||||
* The chat widget is always visible in the sidebar for non-chat notes.
|
||||
*/
|
||||
export default function SidebarChatButton() {
|
||||
export default function SidebarChatButton({ launcherNote }: LauncherNoteProps) {
|
||||
const handleClick = useCallback(() => {
|
||||
// Open right pane if hidden, or toggle it if visible
|
||||
appContext.triggerEvent("toggleRightPane", {});
|
||||
@@ -16,6 +16,7 @@ export default function SidebarChatButton() {
|
||||
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon="bx bx-message-square-dots"
|
||||
text={t("sidebar_chat.launcher_title")}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import contextMenu from "../../menus/context_menu";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { showLauncherContextMenu } from "../../menus/launcher_button_context_menu";
|
||||
import { t } from "../../services/i18n";
|
||||
import { isMobile } from "../../services/utils";
|
||||
|
||||
interface SpacerWidgetProps {
|
||||
launcherNote?: FNote;
|
||||
baseSize?: number;
|
||||
growthFactor?: number;
|
||||
}
|
||||
|
||||
export default function SpacerWidget({ baseSize, growthFactor }: SpacerWidgetProps) {
|
||||
export default function SpacerWidget({ launcherNote, baseSize, growthFactor }: SpacerWidgetProps) {
|
||||
return (
|
||||
<div
|
||||
className="spacer"
|
||||
@@ -17,19 +19,16 @@ export default function SpacerWidget({ baseSize, growthFactor }: SpacerWidgetPro
|
||||
flexGrow: growthFactor ?? 1000,
|
||||
flexShrink: 1000
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (command) {
|
||||
appContext.triggerCommand(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
onContextMenu={launcherNote ? (e) => showLauncherContextMenu<CommandNames>(launcherNote, e, {
|
||||
extraItems: [{
|
||||
title: t("spacer.configure_launchbar"),
|
||||
command: "showLaunchBarSubtree",
|
||||
uiIcon: "bx " + (isMobile() ? "bx-mobile" : "bx-sidebar")
|
||||
}],
|
||||
onCommand: (command) => {
|
||||
if (command) appContext.triggerCommand(command);
|
||||
}
|
||||
}) : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import sync from "../../services/sync";
|
||||
import { escapeQuotes } from "../../services/utils";
|
||||
import ws, { subscribeToMessages, unsubscribeToMessage } from "../../services/ws";
|
||||
import { useStaticTooltip, useTriliumOption } from "../react/hooks";
|
||||
import { launcherContextMenuHandler, LauncherNoteProps } from "./launch_bar_widgets";
|
||||
|
||||
type SyncState = "unknown" | "in-progress"
|
||||
| "connected-with-changes" | "connected-no-changes"
|
||||
@@ -49,7 +50,7 @@ const STATE_MAPPINGS: Record<SyncState, StateMapping> = {
|
||||
}
|
||||
};
|
||||
|
||||
export default function SyncStatus() {
|
||||
export default function SyncStatus({ launcherNote }: LauncherNoteProps) {
|
||||
const syncState = useSyncStatus();
|
||||
const { title, icon, hasChanges } = STATE_MAPPINGS[syncState];
|
||||
const spanRef = useRef<HTMLSpanElement>(null);
|
||||
@@ -60,7 +61,10 @@ export default function SyncStatus() {
|
||||
});
|
||||
|
||||
return (syncServerHost &&
|
||||
<div class="sync-status-widget launcher-button">
|
||||
<div
|
||||
class="sync-status-widget launcher-button"
|
||||
onContextMenu={launcherContextMenuHandler(launcherNote)}
|
||||
>
|
||||
<div class="sync-status">
|
||||
<span
|
||||
key={syncState} // Force re-render when state changes to update tooltip content.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createContext } from "preact";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { showLauncherContextMenu } from "../../menus/launcher_button_context_menu";
|
||||
import utils from "../../services/utils";
|
||||
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
|
||||
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
||||
@@ -22,7 +23,13 @@ export interface LauncherNoteProps {
|
||||
launcherNote: FNote;
|
||||
}
|
||||
|
||||
export function LaunchBarActionButton({ className, ...props }: Omit<ActionButtonProps, "noIconActionClass" | "titlePosition">) {
|
||||
/** Builds the default right-click handler that shows the launch-bar icon context menu (with the "Remove from launch bar" entry). Used by widgets that render a raw element rather than going through {@link LaunchBarActionButton} / {@link LaunchBarDropdownButton}. */
|
||||
export function launcherContextMenuHandler(launcherNote: FNote | null | undefined) {
|
||||
if (!launcherNote) return undefined;
|
||||
return (e: MouseEvent) => showLauncherContextMenu(launcherNote, e);
|
||||
}
|
||||
|
||||
export function LaunchBarActionButton({ className, launcherNote, onContextMenu, ...props }: Omit<ActionButtonProps, "noIconActionClass" | "titlePosition"> & { launcherNote?: FNote }) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
|
||||
return (
|
||||
@@ -30,15 +37,20 @@ export function LaunchBarActionButton({ className, ...props }: Omit<ActionButton
|
||||
className={clsx("button-widget launcher-button", className)}
|
||||
noIconActionClass
|
||||
titlePosition={getTitlePosition(isHorizontalLayout)}
|
||||
onContextMenu={onContextMenu ?? launcherContextMenuHandler(launcherNote)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...props }: Pick<DropdownProps, "title" | "children" | "onShown" | "dropdownOptions" | "dropdownRef"> & { icon: string }) {
|
||||
export function LaunchBarDropdownButton({ children, icon, dropdownOptions, launcherNote, buttonProps, ...props }: Pick<DropdownProps, "title" | "children" | "onShown" | "dropdownOptions" | "dropdownRef" | "buttonProps"> & { icon: string, launcherNote?: FNote }) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
const titlePosition = getTitlePosition(isHorizontalLayout);
|
||||
|
||||
const resolvedButtonProps = launcherNote && !buttonProps?.onContextMenu
|
||||
? { ...buttonProps, onContextMenu: launcherContextMenuHandler(launcherNote) }
|
||||
: buttonProps;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="right-dropdown-widget"
|
||||
@@ -54,6 +66,7 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr
|
||||
}
|
||||
}}
|
||||
mobileBackdrop
|
||||
buttonProps={resolvedButtonProps}
|
||||
{...props}
|
||||
>{children}</Dropdown>
|
||||
);
|
||||
|
||||
@@ -75,6 +75,7 @@ function shouldShow(note: FNote | null | undefined, type: NoteType | undefined,
|
||||
if (viewScope?.viewMode !== "default") return false;
|
||||
if (note?.noteId?.startsWith("_options")) return true;
|
||||
if (note?.isTriliumSqlite()) return false;
|
||||
if (note?.isMarkdown()) return false;
|
||||
return type && supportedNoteTypes.has(type);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function NoteTypeSwitcher() {
|
||||
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
|
||||
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
|
||||
|
||||
return (currentNoteType && supportedNoteTypes.has(currentNoteType) && !note?.isTriliumSqlite() &&
|
||||
return (currentNoteType && supportedNoteTypes.has(currentNoteType) && !note?.isTriliumSqlite() && !note?.isMarkdown() &&
|
||||
<div
|
||||
className="note-type-switcher"
|
||||
onWheel={onWheelHorizontalScroll}
|
||||
|
||||
@@ -14,7 +14,7 @@ import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import type { ViewMode, ViewScope } from "../../services/link";
|
||||
import { NoteContent } from "../collections/legacy/ListOrGridView";
|
||||
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
|
||||
import { LaunchBarActionButton, LauncherNoteProps } from "../launch_bar/launch_bar_widgets";
|
||||
import { ICON_MAPPINGS } from "../note_bars/CollectionProperties";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks";
|
||||
@@ -30,13 +30,14 @@ const VIEW_MODE_ICON_MAPPINGS: Record<Exclude<ViewMode, "default">, string> = {
|
||||
ocr: "bx bx-text"
|
||||
};
|
||||
|
||||
export default function TabSwitcher() {
|
||||
export default function TabSwitcher({ launcherNote }: LauncherNoteProps) {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const mainNoteContexts = useMainNoteContexts();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
className="mobile-tab-switcher"
|
||||
icon="bx bx-rectangle"
|
||||
text="Tabs"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
|
||||
* for protected session or attachment information.
|
||||
*/
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code" | "llmChat"> | "empty" | "readOnlyCode" | "readOnlyText" | "readOnlyOCRText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "sqlConsole" | "llmChat";
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code" | "llmChat"> | "empty" | "readOnlyCode" | "readOnlyText" | "readOnlyOCRText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "sqlConsole" | "markdown" | "llmChat";
|
||||
|
||||
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
|
||||
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
|
||||
@@ -147,6 +147,12 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
|
||||
className: "sql-console-widget-container",
|
||||
isFullHeight: true
|
||||
},
|
||||
markdown: {
|
||||
view: () => import("./type_widgets/code/Markdown"),
|
||||
className: "note-detail-markdown",
|
||||
printable: true,
|
||||
isFullHeight: true
|
||||
},
|
||||
spreadsheet: {
|
||||
view: () => import("./type_widgets/spreadsheet/Spreadsheet"),
|
||||
className: "note-detail-spreadsheet",
|
||||
|
||||
@@ -84,9 +84,9 @@ function Button({ name, buttonRef, className, text, onClick, keyboardShortcut, i
|
||||
);
|
||||
}
|
||||
|
||||
export function ButtonGroup({ children }: { children: ComponentChildren }) {
|
||||
export function ButtonGroup({ size, className, children }: { size?: "sm" | "lg"; className?: string; children: ComponentChildren }) {
|
||||
return (
|
||||
<div className="btn-group" role="group">
|
||||
<div className={`btn-group ${size ? `btn-group-${size}` : ""} ${className ?? ""}`} role="group">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import { FilterLabelsByType, KeyboardActionNames, NoteType, OptionNames, RelationNames } from "@triliumnext/commons";
|
||||
import { Tooltip } from "bootstrap";
|
||||
import Mark from "mark.js";
|
||||
import { RefObject, VNode } from "preact";
|
||||
import { Ref, RefObject, VNode } from "preact";
|
||||
import { CSSProperties, useSyncExternalStore } from "preact/compat";
|
||||
import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -964,11 +964,13 @@ export function useLegacyImperativeHandlers(handlers: Record<string, Function>)
|
||||
}, [ handlers ]);
|
||||
}
|
||||
|
||||
export function useSyncedRef<T>(externalRef?: RefObject<T>, initialValue: T | null = null): RefObject<T> {
|
||||
export function useSyncedRef<T>(externalRef?: Ref<T>, initialValue: T | null = null): RefObject<T> {
|
||||
const ref = useRef<T>(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
if (externalRef) {
|
||||
if (typeof externalRef === "function") {
|
||||
externalRef(ref.current);
|
||||
} else if (externalRef) {
|
||||
externalRef.current = ref.current;
|
||||
}
|
||||
}, [ ref, externalRef ]);
|
||||
@@ -1140,6 +1142,29 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
|
||||
return { isReadOnly, enableEditing, temporarilyEditable };
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous effective read-only state for widgets that honor the `#readOnly` label
|
||||
* (mermaid, canvas, mind map, spreadsheet). Combines the label with the temporary
|
||||
* "enable editing" toggle (driven by `readOnlyTemporarilyDisabled`) so clicking the
|
||||
* read-only badge unlocks the widget.
|
||||
*/
|
||||
export function useEffectiveReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
|
||||
const [ readOnlyLabel ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const [ tempDisabled, setTempDisabled ] = useState<boolean>(!!noteContext?.viewScope?.readOnlyTemporarilyDisabled);
|
||||
|
||||
useEffect(() => {
|
||||
setTempDisabled(!!noteContext?.viewScope?.readOnlyTemporarilyDisabled);
|
||||
}, [ note, noteContext, noteContext?.viewScope ]);
|
||||
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
|
||||
if (noteContext?.ntxId === eventNoteContext?.ntxId) {
|
||||
setTempDisabled(!!eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
|
||||
}
|
||||
});
|
||||
|
||||
return readOnlyLabel && !tempDisabled;
|
||||
}
|
||||
|
||||
async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
||||
|
||||
if (note.isProtected && !protected_session_holder.isProtectedSessionAvailable()) {
|
||||
|
||||
@@ -75,7 +75,8 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
|
||||
const noteType = useNoteProperty(note, "type") ?? "";
|
||||
const [viewType] = useNoteLabel(note, "viewType");
|
||||
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
|
||||
const isSearchable = ["text", "code", "book", "mindMap", "doc", "spreadsheet"].includes(noteType);
|
||||
const isSourceView = noteContext?.viewScope?.viewMode === "source";
|
||||
const isSearchable = isSourceView || ["text", "code", "book", "mindMap", "doc", "spreadsheet"].includes(noteType);
|
||||
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
|
||||
const isExportableToImage = ["mermaid", "mindMap"].includes(noteType);
|
||||
const isContentAvailable = note.isContentAvailable();
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
body.mobile .note-actions-custom:not(:empty) {
|
||||
margin-bottom: calc(var(--bs-dropdown-divider-margin-y) * 2);
|
||||
}
|
||||
|
||||
body.mobile .note-actions-custom-display-mode {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
& > .dropdown-item {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 0.25em;
|
||||
padding-inline: 0.25em;
|
||||
font-size: 0.85em;
|
||||
border-radius: 0 !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
& > .dropdown-item:first-child {
|
||||
border-start-start-radius: var(--bs-border-radius) !important;
|
||||
border-end-start-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
& > .dropdown-item:last-child {
|
||||
border-start-end-radius: var(--bs-border-radius) !important;
|
||||
border-end-end-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createImageSrcUrl, isMobile, openInAppHelpFromUrl } from "../../service
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import { buildSaveSqlToNoteHandler } from "../FloatingButtonsDefinitions";
|
||||
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
|
||||
import { ButtonGroup } from "../react/Button";
|
||||
import { FormFileUploadActionButton, FormFileUploadFormListItem, FormFileUploadProps } from "../react/FormFileUpload";
|
||||
import { FormListItem } from "../react/FormList";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
|
||||
@@ -72,7 +73,7 @@ export default function NoteActionsCustom(props: NoteActionsCustomProps) {
|
||||
<AddChildButton {...innerProps} />
|
||||
<RunActiveNoteButton {...innerProps } />
|
||||
<SwitchSplitOrientationButton {...innerProps} />
|
||||
<ToggleReadOnlyButton {...innerProps} />
|
||||
<DisplayModeSwitcher {...innerProps} />
|
||||
<SaveToNoteButton {...innerProps} />
|
||||
<RefreshButton {...innerProps} />
|
||||
<CopyReferenceToClipboardButton {...innerProps} />
|
||||
@@ -189,28 +190,66 @@ function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, not
|
||||
|
||||
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
const isShown = note.type === "mermaid" && !cachedIsMobile && note.isContentAvailable() && isDefaultViewMode;
|
||||
const [ displayMode ] = useNoteLabel(note, "displayMode");
|
||||
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
|
||||
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
|
||||
const effectiveMode = displayMode === "source" || displayMode === "split" || displayMode === "preview"
|
||||
? displayMode
|
||||
: isReadOnly ? "preview" : "split";
|
||||
|
||||
return isShown && <NoteAction
|
||||
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
|
||||
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
|
||||
onClick={() => setSplitEditorOrientation(upcomingOrientation)}
|
||||
disabled={isReadOnly}
|
||||
disabled={effectiveMode !== "split"}
|
||||
/>;
|
||||
}
|
||||
|
||||
export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas", "spreadsheet" ].includes(note.type) || isSavedSqlite)
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
function DisplayModeSwitcher({ note, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
const [ displayMode, setDisplayMode ] = useNoteLabel(note, "displayMode");
|
||||
const isEnabled = (note.isMarkdown() || note.type === "mermaid") && note.isContentAvailable() && isDefaultViewMode;
|
||||
if (!isEnabled) return null;
|
||||
|
||||
return isEnabled && <NoteAction
|
||||
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
|
||||
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
|
||||
onClick={() => setReadOnly(!isReadOnly)}
|
||||
/>;
|
||||
const mode = displayMode === "source" || displayMode === "preview" ? displayMode : "split";
|
||||
const buttons: Array<{ value: "source" | "split" | "preview"; icon: string; text: string }> = [
|
||||
{ value: "source", icon: "bx bx-code", text: t("display_mode.source") },
|
||||
{ value: "split", icon: "bx bxs-dock-left", text: t("display_mode.split") },
|
||||
{ value: "preview", icon: "bx bx-show", text: t("display_mode.preview") }
|
||||
];
|
||||
|
||||
if (cachedIsMobile) {
|
||||
return (
|
||||
<div className="note-actions-custom-display-mode">
|
||||
{buttons.map(({ value, icon, text }) => (
|
||||
<NoteAction
|
||||
key={value}
|
||||
icon={icon}
|
||||
text={text}
|
||||
active={mode === value}
|
||||
onClick={() => setDisplayMode(value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="note-actions-custom-spacer" />
|
||||
<ButtonGroup size="sm">
|
||||
{buttons.map(({ value, icon, text }) => (
|
||||
<NoteAction
|
||||
key={value}
|
||||
icon={icon}
|
||||
text={text}
|
||||
active={mode === value}
|
||||
onClick={() => setDisplayMode(value)}
|
||||
/>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
<div className="note-actions-custom-spacer" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RunActiveNoteButton({ noteMime }: NoteActionsCustomInnerProps) {
|
||||
@@ -268,12 +307,12 @@ function AddChildButton({ parentComponent, noteType, ntxId, isReadOnly }: NoteAc
|
||||
}
|
||||
//#endregion
|
||||
|
||||
function NoteAction({ text, ...props }: Pick<ActionButtonProps, "text" | "icon" | "disabled" | "triggerCommand"> & {
|
||||
function NoteAction({ text, active, ...props }: Pick<ActionButtonProps, "text" | "icon" | "disabled" | "triggerCommand" | "active"> & {
|
||||
onClick?: ((e: MouseEvent) => void) | undefined;
|
||||
}) {
|
||||
return (cachedIsMobile
|
||||
? <FormListItem {...props}>{text}</FormListItem>
|
||||
: <ActionButton text={text} {...props} />
|
||||
: <ActionButton text={text} active={active} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||
const [ state, setState ] = useState<"normal" | "showHelpTooltip" | "showAttributeDetail">();
|
||||
const [ error, setError ] = useState<unknown>();
|
||||
const [ needsSaving, setNeedsSaving ] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const suppressNextOnHide = useRef(false);
|
||||
|
||||
const lastSavedContent = useRef<string>();
|
||||
const currentValueRef = useRef(currentValue);
|
||||
@@ -383,6 +385,12 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||
onClick={(e) => {
|
||||
// Prevent automatic hiding of the context menu due to the button being clicked.
|
||||
e.stopPropagation();
|
||||
if (isMenuOpen) {
|
||||
// If we re-show the menu, ContextMenu.show() will call hide()
|
||||
// and immediately trigger onHide. Suppress that transient hide.
|
||||
suppressNextOnHide.current = true;
|
||||
}
|
||||
setIsMenuOpen(true);
|
||||
|
||||
contextMenu.show<AttributeCommandNames>({
|
||||
x: e.pageX,
|
||||
@@ -395,7 +403,14 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||
{ title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" },
|
||||
{ title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" }
|
||||
],
|
||||
selectMenuItemHandler: (item) => handleAddNewAttributeCommand(item.command)
|
||||
selectMenuItemHandler: (item) => handleAddNewAttributeCommand(item.command),
|
||||
onHide: () => {
|
||||
if (suppressNextOnHide.current) {
|
||||
suppressNextOnHide.current = false;
|
||||
return;
|
||||
}
|
||||
setIsMenuOpen(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -438,4 +453,4 @@ function getClickIndex(pos: ModelPosition) {
|
||||
}
|
||||
|
||||
return clickIndex;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ export default function ScrollPadding() {
|
||||
const isEnabled = ["text", "code"].includes(note?.type ?? "")
|
||||
&& viewScope?.viewMode === "default"
|
||||
&& note?.isContentAvailable()
|
||||
&& !note?.isTriliumSqlite();
|
||||
&& !note?.isTriliumSqlite()
|
||||
&& !note?.isMarkdown();
|
||||
|
||||
const refreshHeight = () => {
|
||||
if (!ref.current) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./TableOfContents.css";
|
||||
|
||||
import { attributeChangeAffectsHeading, CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
|
||||
import { attributeChangeAffectsHeading, CKTextEditor, ModelElement, type ModelNode } from "@triliumnext/ckeditor5";
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -230,7 +230,7 @@ function extractTocFromTextEditor(editor: CKTextEditor) {
|
||||
// Fallback to plain text if DOM conversion fails
|
||||
if (!text) {
|
||||
text = Array.from( item.getChildren() )
|
||||
.map( c => c.is( '$text' ) ? c.data : '' )
|
||||
.map( (c: ModelNode) => c.is( '$text' ) ? c.data : '' )
|
||||
.join( '' );
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import Icon from "../react/Icon";
|
||||
import Modal from "../react/Modal";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { ParentComponent, refToJQuerySelector } from "../react/react_utils";
|
||||
import { TextPreview } from "./File";
|
||||
import { TextRepresentation } from "./ReadOnlyTextRepresentation";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
@@ -144,6 +145,7 @@ export function AttachmentDetail({ note, viewScope }: TypeWidgetProps) {
|
||||
function AttachmentInfo({ attachment, isFullDetail }: { attachment: FAttachment, isFullDetail?: boolean }) {
|
||||
const contentWrapper = useRef<HTMLDivElement>(null);
|
||||
const [ ocrModalShown, setOcrModalShown ] = useState(false);
|
||||
const [ textContent, setTextContent ] = useState<string | null>(null);
|
||||
const supportsOcr = attachment.role === "image" || attachment.role === "file";
|
||||
|
||||
function refresh() {
|
||||
@@ -151,6 +153,10 @@ function AttachmentInfo({ attachment, isFullDetail }: { attachment: FAttachment,
|
||||
.then(({ $renderedContent }) => {
|
||||
contentWrapper.current?.replaceChildren(...$renderedContent);
|
||||
});
|
||||
|
||||
if (attachment.role === "file") {
|
||||
attachment.getBlob().then(blob => setTextContent(blob?.content ?? null));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(refresh, [ attachment ]);
|
||||
@@ -213,6 +219,7 @@ function AttachmentInfo({ attachment, isFullDetail }: { attachment: FAttachment,
|
||||
</div>
|
||||
|
||||
{attachment.utcDateScheduledForErasureSince && <DeletionAlert utcDateScheduledForErasureSince={attachment.utcDateScheduledForErasureSince} />}
|
||||
{textContent && <TextPreview content={textContent} />}
|
||||
<div ref={contentWrapper} className="attachment-content-wrapper" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function FileTypeWidget({ note, parentComponent, noteContext }: T
|
||||
|
||||
}
|
||||
|
||||
function TextPreview({ content }: { content: string }) {
|
||||
export function TextPreview({ content }: { content: string }) {
|
||||
const trimmedContent = content.substring(0, TEXT_MAX_NUM_CHARS);
|
||||
const isTooLarge = trimmedContent.length !== content.length;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { HTMLAttributes, RefObject } from "preact";
|
||||
import { useCallback, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import utils from "../../services/utils";
|
||||
import { useColorScheme, useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
|
||||
import { useColorScheme, useEditorSpacedUpdate, useEffectiveReadOnly, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
@@ -46,7 +46,7 @@ function buildMindElixirLangPack(): LangPack {
|
||||
export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
const apiRef = useRef<MindElixirInstance>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isReadOnly = useEffectiveReadOnly(note, noteContext);
|
||||
|
||||
|
||||
const spacedUpdate = useEditorSpacedUpdate({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import { TypeWidgetProps } from "../type_widget";
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
import { useColorScheme, useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||
import { useColorScheme, useEffectiveReadOnly, useTriliumOption } from "../../react/hooks";
|
||||
import { useCallback, useMemo, useRef } from "preact/hooks";
|
||||
import { type ExcalidrawImperativeAPI, type AppState } from "@excalidraw/excalidraw/types";
|
||||
import options from "../../../services/options";
|
||||
@@ -18,7 +18,7 @@ window.EXCALIDRAW_ASSET_PATH = `${window.location.pathname}/node_modules/@excali
|
||||
|
||||
export default function Canvas({ note, noteContext }: TypeWidgetProps) {
|
||||
const apiRef = useRef<ExcalidrawImperativeAPI>(null);
|
||||
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isReadOnly = useEffectiveReadOnly(note, noteContext);
|
||||
const colorScheme = useColorScheme();
|
||||
const [ locale ] = useTriliumOption("locale");
|
||||
const persistence = useCanvasPersistence(note, noteContext, apiRef, colorScheme, isReadOnly);
|
||||
|
||||
@@ -2,6 +2,7 @@ import "./code.css";
|
||||
|
||||
import { default as VanillaCodeMirror, getThemeById } from "@triliumnext/codemirror";
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { Ref } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandListenerData } from "../../../components/app_context";
|
||||
@@ -31,9 +32,11 @@ export interface EditableCodeProps extends TypeWidgetProps, Omit<CodeEditorProps
|
||||
/** Invoked after the content of the note has been uploaded to the server, using a spaced update. */
|
||||
dataSaved?: () => void;
|
||||
placeholder?: string;
|
||||
/** Optional external ref to the underlying CodeMirror `EditorView`. Populated once the editor has initialized. */
|
||||
editorRef?: Ref<VanillaCodeMirror>;
|
||||
}
|
||||
|
||||
export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent }: TypeWidgetProps) {
|
||||
export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent, editorRef }: TypeWidgetProps & { editorRef?: Ref<VanillaCodeMirror> }) {
|
||||
const [ content, setContent ] = useState("");
|
||||
const blob = useNoteBlob(note);
|
||||
const [ noteTabWidth ] = useNoteLabelInt(note, "tabWidth");
|
||||
@@ -54,6 +57,7 @@ export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent }: TypeWi
|
||||
return (
|
||||
<CodeEditor
|
||||
ntxId={ntxId} parentComponent={parentComponent}
|
||||
editorRef={editorRef}
|
||||
className="note-detail-readonly-code-content"
|
||||
content={content}
|
||||
mime={note.mime}
|
||||
@@ -81,9 +85,14 @@ function formatViewSource(note: FNote, content: string) {
|
||||
return content;
|
||||
}
|
||||
|
||||
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, placeholder, ...editorProps }: EditableCodeProps) {
|
||||
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, placeholder, editorRef: externalEditorRef, ...editorProps }: EditableCodeProps) {
|
||||
const editorRef = useRef<VanillaCodeMirror>(null);
|
||||
const containerRef = useRef<HTMLPreElement>(null);
|
||||
const combinedEditorRef = (view: VanillaCodeMirror | null) => {
|
||||
editorRef.current = view;
|
||||
if (typeof externalEditorRef === "function") externalEditorRef(view);
|
||||
else if (externalEditorRef) externalEditorRef.current = view;
|
||||
};
|
||||
const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled");
|
||||
const [ noteTabWidth ] = useNoteLabelInt(note, "tabWidth");
|
||||
const [ noteUseTabs ] = useNoteLabelOptionalBool(note, "indentWithTabs");
|
||||
@@ -122,7 +131,7 @@ export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentC
|
||||
<>
|
||||
<CodeEditor
|
||||
ntxId={ntxId} parentComponent={parentComponent}
|
||||
editorRef={editorRef} containerRef={containerRef}
|
||||
editorRef={combinedEditorRef} containerRef={containerRef}
|
||||
mime={mime ?? "text/plain"}
|
||||
className="note-detail-code-editor"
|
||||
placeholder={placeholder ?? t("editable_code.placeholder")}
|
||||
@@ -217,11 +226,13 @@ export function CodeEditor({ parentComponent, ntxId, containerRef: externalConta
|
||||
indentSize={editorProps.indentSize ?? (parseInt(codeNoteTabWidth) || 4)}
|
||||
useTabs={editorProps.useTabs ?? codeNoteIndentWithTabs}
|
||||
onInitialized={() => {
|
||||
if (externalContainerRef && containerRef.current) {
|
||||
externalContainerRef.current = containerRef.current;
|
||||
if (containerRef.current) {
|
||||
if (typeof externalContainerRef === "function") externalContainerRef(containerRef.current);
|
||||
else if (externalContainerRef) externalContainerRef.current = containerRef.current;
|
||||
}
|
||||
if (externalEditorRef && codeEditorRef.current) {
|
||||
externalEditorRef.current = codeEditorRef.current;
|
||||
if (codeEditorRef.current) {
|
||||
if (typeof externalEditorRef === "function") externalEditorRef(codeEditorRef.current);
|
||||
else if (externalEditorRef) externalEditorRef.current = codeEditorRef.current;
|
||||
}
|
||||
initialized.current.resolve();
|
||||
onInitialized?.();
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { EditorConfig, default as VanillaCodeMirror } from "@triliumnext/codemirror";
|
||||
import { useSyncedRef } from "../../react/hooks";
|
||||
import { RefObject } from "preact";
|
||||
import { Ref } from "preact";
|
||||
|
||||
export interface CodeMirrorProps extends Omit<EditorConfig, "parent"> {
|
||||
content?: string;
|
||||
mime: string;
|
||||
className?: string;
|
||||
editorRef?: RefObject<VanillaCodeMirror>;
|
||||
containerRef?: RefObject<HTMLPreElement>;
|
||||
editorRef?: Ref<VanillaCodeMirror>;
|
||||
containerRef?: Ref<HTMLPreElement>;
|
||||
onInitialized?: () => void;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,8 @@ export default function CodeMirror({ className, content, mime, editorRef: extern
|
||||
...extraOpts
|
||||
});
|
||||
codeEditorRef.current = codeEditor;
|
||||
if (externalEditorRef) {
|
||||
externalEditorRef.current = codeEditor;
|
||||
}
|
||||
if (typeof externalEditorRef === "function") externalEditorRef(codeEditor);
|
||||
else if (externalEditorRef) externalEditorRef.current = codeEditor;
|
||||
onInitialized?.();
|
||||
|
||||
return () => codeEditor.destroy();
|
||||
|
||||
34
apps/client/src/widgets/type_widgets/code/Markdown.css
Normal file
34
apps/client/src/widgets/type_widgets/code/Markdown.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.note-detail-markdown {
|
||||
.note-detail-code-editor {
|
||||
margin-top: 0 !important;
|
||||
|
||||
.cm-editor .cm-scroller {
|
||||
padding-block: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-preview {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 0.5em 1em;
|
||||
user-select: text;
|
||||
|
||||
.mermaid-diagram,
|
||||
.mermaid-diagram svg {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-preview [data-source-line] {
|
||||
border-left: 2px solid transparent;
|
||||
padding-left: 0.5em;
|
||||
margin-left: -0.5em;
|
||||
}
|
||||
|
||||
[data-source-line].markdown-preview-active {
|
||||
border-left-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
93
apps/client/src/widgets/type_widgets/code/Markdown.spec.ts
Normal file
93
apps/client/src/widgets/type_widgets/code/Markdown.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { renderWithSourceLines } from "./Markdown.js";
|
||||
|
||||
describe("renderWithSourceLines", () => {
|
||||
function extractLines(html: string): number[] {
|
||||
return [ ...html.matchAll(/data-source-line="(\d+)"/g) ].map((m) => parseInt(m[1], 10));
|
||||
}
|
||||
|
||||
it("returns empty string for empty input", () => {
|
||||
expect(renderWithSourceLines("")).toBe("");
|
||||
});
|
||||
|
||||
it("tags a single block as line 1", () => {
|
||||
const html = renderWithSourceLines("hello");
|
||||
expect(extractLines(html)).toEqual([ 1 ]);
|
||||
expect(html).toContain("hello");
|
||||
});
|
||||
|
||||
it("assigns correct source lines to consecutive blocks separated by blank lines", () => {
|
||||
const src = [
|
||||
"# Heading", // line 1
|
||||
"", // line 2
|
||||
"A paragraph.", // line 3
|
||||
"", // line 4
|
||||
"Another one." // line 5
|
||||
].join("\n");
|
||||
|
||||
expect(extractLines(renderWithSourceLines(src))).toEqual([ 1, 3, 5 ]);
|
||||
});
|
||||
|
||||
it("counts multi-line blocks so subsequent blocks get the right line", () => {
|
||||
const src = [
|
||||
"```", // 1
|
||||
"code", // 2
|
||||
"more code", // 3
|
||||
"```", // 4
|
||||
"", // 5
|
||||
"after" // 6
|
||||
].join("\n");
|
||||
|
||||
expect(extractLines(renderWithSourceLines(src))).toEqual([ 1, 6 ]);
|
||||
});
|
||||
|
||||
it("renders standard markdown constructs inside the wrappers", () => {
|
||||
const html = renderWithSourceLines("## Heading\n\n- item\n");
|
||||
expect(html).toContain("<h2>Heading</h2>");
|
||||
expect(html).toContain("<ul>");
|
||||
expect(html).toContain("<li>item</li>");
|
||||
});
|
||||
|
||||
it("keeps H1 as H1 in the preview (no title-row context to avoid)", () => {
|
||||
const html = renderWithSourceLines("# Top level");
|
||||
expect(html).toContain("<h1>Top level</h1>");
|
||||
});
|
||||
|
||||
it("preserves reference-style links across per-block parsing", () => {
|
||||
const src = [
|
||||
"[trilium][t]", // 1
|
||||
"", // 2
|
||||
"[t]: https://example.com"
|
||||
].join("\n");
|
||||
|
||||
const html = renderWithSourceLines(src);
|
||||
expect(html).toContain('href="https://example.com"');
|
||||
});
|
||||
|
||||
it("normalizes fenced code languages to CKEditor MIME identifiers for syntax highlighting", () => {
|
||||
const html = renderWithSourceLines("```javascript\nconst x = 1;\n```");
|
||||
expect(html).toMatch(/class="language-application-javascript-env-(backend|frontend)"/);
|
||||
});
|
||||
|
||||
it("produces CKEditor admonition markup for GFM callouts", () => {
|
||||
const html = renderWithSourceLines("> [!NOTE]\n> heads up");
|
||||
expect(html).toContain('<aside class="admonition note">');
|
||||
});
|
||||
|
||||
it("preserves the `mermaid` fence language so the mermaid rewrite can match it", () => {
|
||||
const html = renderWithSourceLines("```mermaid\ngraph TD;\nA-->B;\n```");
|
||||
expect(html).toContain('class="language-mermaid"');
|
||||
});
|
||||
|
||||
it("produces math-tex spans for inline math", () => {
|
||||
const html = renderWithSourceLines("Energy: $e=mc^2$.");
|
||||
expect(html).toContain('<span class="math-tex">');
|
||||
});
|
||||
|
||||
it("renders [[wikilinks]] with hash-router hrefs so the preview navigates correctly", () => {
|
||||
const html = renderWithSourceLines("See [[abc123]] for details.");
|
||||
expect(html).toContain('class="reference-link"');
|
||||
expect(html).toContain('href="#root/abc123"');
|
||||
});
|
||||
});
|
||||
193
apps/client/src/widgets/type_widgets/code/Markdown.tsx
Normal file
193
apps/client/src/widgets/type_widgets/code/Markdown.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import "./Markdown.css";
|
||||
|
||||
import VanillaCodeMirror from "@triliumnext/codemirror";
|
||||
import { renderToHtml } from "@triliumnext/commons";
|
||||
import DOMPurify from "dompurify";
|
||||
import { Marked } from "marked";
|
||||
import { createContext } from "preact";
|
||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import SplitEditor from "../helpers/SplitEditor";
|
||||
import { ReadOnlyTextContent } from "../text/ReadOnlyText";
|
||||
import { TypeWidgetProps } from "../type_widget";
|
||||
|
||||
const marked = new Marked({ breaks: true, gfm: true });
|
||||
|
||||
interface MarkdownContextValue {
|
||||
html: string;
|
||||
setEditorView: (view: VanillaCodeMirror | null) => void;
|
||||
setPreviewEl: (el: HTMLDivElement | null) => void;
|
||||
}
|
||||
|
||||
const MarkdownContext = createContext<MarkdownContextValue | null>(null);
|
||||
|
||||
function useMarkdownContext() {
|
||||
const ctx = useContext(MarkdownContext);
|
||||
if (!ctx) throw new Error("useMarkdownContext must be used within a Markdown component");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export default function Markdown(props: TypeWidgetProps) {
|
||||
const [ content, setContent ] = useState("");
|
||||
const [ editorView, setEditorView ] = useState<VanillaCodeMirror | null>(null);
|
||||
const [ previewEl, setPreviewEl ] = useState<HTMLDivElement | null>(null);
|
||||
const html = useMemo(() => renderWithSourceLines(content), [ content ]);
|
||||
|
||||
useSyncedScrolling(editorView, previewEl);
|
||||
useSyncedHighlight(editorView, previewEl, html);
|
||||
|
||||
const ctx = useMemo<MarkdownContextValue>(
|
||||
() => ({ html, setEditorView, setPreviewEl }),
|
||||
[ html ]
|
||||
);
|
||||
|
||||
return (
|
||||
<MarkdownContext.Provider value={ctx}>
|
||||
<SplitEditor
|
||||
noteType="code"
|
||||
{...props}
|
||||
editorRef={setEditorView}
|
||||
onContentChanged={setContent}
|
||||
previewContent={<MarkdownPreview ntxId={props.ntxId} />}
|
||||
forceOrientation="horizontal"
|
||||
/>
|
||||
</MarkdownContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function MarkdownPreview({ ntxId }: { ntxId: TypeWidgetProps["ntxId"] }) {
|
||||
const { html, setPreviewEl } = useMarkdownContext();
|
||||
return (
|
||||
<ReadOnlyTextContent
|
||||
html={html}
|
||||
ntxId={ntxId}
|
||||
className="markdown-preview"
|
||||
contentRef={setPreviewEl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//#region Synced scrolling
|
||||
/**
|
||||
* One-directional (editor → preview) scroll sync. On editor scroll, finds the
|
||||
* top visible source line via the CodeMirror `EditorView`, then scrolls the
|
||||
* preview so the block tagged with that line is at the top — interpolating to
|
||||
* the next block for smoothness.
|
||||
*/
|
||||
function useSyncedScrolling(view: VanillaCodeMirror | null, preview: HTMLDivElement | null) {
|
||||
useEffect(() => {
|
||||
if (!view || !preview) return;
|
||||
|
||||
const scroller = view.scrollDOM;
|
||||
|
||||
function onScroll() {
|
||||
if (!view || !preview) return;
|
||||
const topLine = view.state.doc.lineAt(view.lineBlockAtHeight(scroller.scrollTop).from).number;
|
||||
|
||||
const blocks = preview.querySelectorAll<HTMLElement>("[data-source-line]");
|
||||
if (!blocks.length) return;
|
||||
|
||||
let before: HTMLElement | null = null;
|
||||
let after: HTMLElement | null = null;
|
||||
for (const el of blocks) {
|
||||
const l = parseInt(el.dataset.sourceLine!, 10);
|
||||
if (l <= topLine) before = el;
|
||||
else { after = el; break; }
|
||||
}
|
||||
|
||||
if (!before) { preview.scrollTop = 0; return; }
|
||||
|
||||
const previewTop = preview.getBoundingClientRect().top - preview.scrollTop;
|
||||
const beforeOffset = before.getBoundingClientRect().top - previewTop;
|
||||
const beforeLine = parseInt(before.dataset.sourceLine!, 10);
|
||||
|
||||
if (!after) { preview.scrollTop = beforeOffset; return; }
|
||||
|
||||
const afterOffset = after.getBoundingClientRect().top - previewTop;
|
||||
const afterLine = parseInt(after.dataset.sourceLine!, 10);
|
||||
const ratio = (topLine - beforeLine) / (afterLine - beforeLine);
|
||||
preview.scrollTop = beforeOffset + (afterOffset - beforeOffset) * ratio;
|
||||
}
|
||||
|
||||
scroller.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => scroller.removeEventListener("scroll", onScroll);
|
||||
}, [ view, preview ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights the preview block that corresponds to the editor's active line,
|
||||
* matching the built-in `cm-activeLine` behavior. Re-runs when the rendered
|
||||
* HTML changes so newly inserted blocks pick up the current cursor position.
|
||||
*/
|
||||
function useSyncedHighlight(view: VanillaCodeMirror | null, preview: HTMLDivElement | null, html: string) {
|
||||
useEffect(() => {
|
||||
if (!view || !preview) return;
|
||||
|
||||
let current: HTMLElement | null = null;
|
||||
|
||||
function update() {
|
||||
if (!view || !preview) return;
|
||||
const activeLine = view.state.doc.lineAt(view.state.selection.main.head).number;
|
||||
|
||||
const blocks = preview.querySelectorAll<HTMLElement>("[data-source-line]");
|
||||
let match: HTMLElement | null = null;
|
||||
for (const el of blocks) {
|
||||
if (parseInt(el.dataset.sourceLine!, 10) <= activeLine) match = el;
|
||||
else break;
|
||||
}
|
||||
|
||||
if (match === current) return;
|
||||
current?.classList.remove("markdown-preview-active");
|
||||
match?.classList.add("markdown-preview-active");
|
||||
current = match;
|
||||
}
|
||||
|
||||
update();
|
||||
const unsubscribe = view.addUpdateListener((v) => {
|
||||
if (v.selectionSet || v.docChanged) update();
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [ view, preview, html ]);
|
||||
}
|
||||
|
||||
/** Token types the parser emits but which don't produce top-level block HTML. */
|
||||
const NON_RENDERED_TOKENS = new Set([ "space", "def" ]);
|
||||
|
||||
/**
|
||||
* Render markdown and tag each top-level block with its 1-indexed source line,
|
||||
* so the preview can be scrolled to match the editor. Uses the shared
|
||||
* `renderToHtml` pipeline (admonitions, math, tables, etc.) with DOMPurify for
|
||||
* sanitization, then walks the rendered DOM and pairs each top-level child
|
||||
* with the matching lexer token's start line. Marked does not emit source
|
||||
* positions (markedjs/marked#1267) so we count newlines in `raw` ourselves.
|
||||
*/
|
||||
export function renderWithSourceLines(src: string): string {
|
||||
// Compute the start line of each renderable top-level token in source order.
|
||||
const tokens = marked.lexer(src);
|
||||
const lines: number[] = [];
|
||||
let line = 1;
|
||||
for (const token of tokens) {
|
||||
const startLine = line;
|
||||
line += (token.raw.match(/\n/g) ?? []).length;
|
||||
if (!NON_RENDERED_TOKENS.has(token.type)) lines.push(startLine);
|
||||
}
|
||||
|
||||
const html = renderToHtml(src, "", {
|
||||
sanitize: (h) => DOMPurify.sanitize(h),
|
||||
wikiLink: { formatHref: (id) => `#root/${id}` },
|
||||
demoteH1: false
|
||||
});
|
||||
if (!html) return "";
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = html;
|
||||
|
||||
const parts: string[] = [];
|
||||
const children = Array.from(container.children);
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const sourceLine = lines[i] ?? lines[lines.length - 1] ?? 1;
|
||||
parts.push(`<div data-source-line="${sourceLine}">${children[i].outerHTML}</div>`);
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
//#endregion
|
||||
@@ -17,6 +17,10 @@
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
>* {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.note-detail-split .note-detail-split-editor .note-detail-code {
|
||||
@@ -91,11 +95,26 @@ body.desktop .note-detail-split .note-detail-code-editor {
|
||||
|
||||
/* Read-only view */
|
||||
|
||||
.note-detail-split.split-read-only .note-detail-split-editor-col {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note-detail-split.split-read-only .note-detail-split-preview-col {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Source-only view */
|
||||
|
||||
.note-detail-split.split-source-only .note-detail-split-preview-col {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note-detail-split.split-source-only .note-detail-split-editor-col {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* #region SVG */
|
||||
.note-detail-split.svg-editor .render-container {
|
||||
height: 100%;
|
||||
|
||||
@@ -8,8 +8,8 @@ import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
|
||||
import utils, { isMobile } from "../../../services/utils";
|
||||
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import { useNoteBlob, useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||
import { EditableCode, EditableCodeProps } from "../code/Code";
|
||||
import { useEffectiveReadOnly, useNoteBlob, useNoteLabel, useTriliumOption } from "../../react/hooks";
|
||||
import { EditableCode, EditableCodeProps, ReadOnlyCode } from "../code/Code";
|
||||
|
||||
export interface SplitEditorProps extends EditableCodeProps {
|
||||
className?: string;
|
||||
@@ -25,38 +25,60 @@ export interface SplitEditorProps extends EditableCodeProps {
|
||||
/**
|
||||
* Abstract `TypeWidget` which contains a preview and editor pane, each displayed on half of the available screen.
|
||||
*
|
||||
* The active view is driven by the `#displayMode` label (`source`, `split`, `preview`); when unset
|
||||
* it falls back to the `#readOnly` label (truthy → preview, falsy → split). `#displayMode` always
|
||||
* wins so an explicit choice is never overridden by `#readOnly`. The editor and preview panes are
|
||||
* always mounted; switching modes only toggles a CSS class so CodeMirror state, scroll position and
|
||||
* pending edits survive the change.
|
||||
*
|
||||
* Features:
|
||||
*
|
||||
* - The two panes are resizeable via a split, on desktop. The split can be optionally customized via {@link buildSplitExtraOptions}.
|
||||
* - Can display errors to the user via {@link setError}.
|
||||
* - Horizontal or vertical orientation for the editor/preview split, adjustable via the switch split orientation button floating button.
|
||||
*/
|
||||
export default function SplitEditor(props: SplitEditorProps) {
|
||||
const [ readOnly ] = useNoteLabelBoolean(props.note, "readOnly");
|
||||
|
||||
if (readOnly) {
|
||||
return <ReadOnlyView {...props} />;
|
||||
}
|
||||
|
||||
return <EditorWithSplit {...props} />;
|
||||
|
||||
}
|
||||
|
||||
function EditorWithSplit({ note, error, splitOptions, previewContent, previewButtons, className, editorBefore, forceOrientation, extraContent, ...editorProps }: SplitEditorProps) {
|
||||
export default function SplitEditor({ note, noteContext, error, splitOptions, previewContent, previewButtons, className, editorBefore, forceOrientation, extraContent, ...editorProps }: SplitEditorProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const splitEditorOrientation = useSplitOrientation(forceOrientation);
|
||||
const [ displayMode ] = useNoteLabel(note, "displayMode");
|
||||
const readOnly = useEffectiveReadOnly(note, noteContext);
|
||||
const mode = displayMode === "source" || displayMode === "split" || displayMode === "preview"
|
||||
? displayMode
|
||||
: readOnly ? "preview" : "split";
|
||||
|
||||
const editor = (
|
||||
// Lazy-mount each pane on first need, then keep it mounted so subsequent switches stay instant.
|
||||
const editorMounted = useRef(mode !== "preview");
|
||||
const previewMounted = useRef(mode !== "source");
|
||||
if (mode !== "preview") editorMounted.current = true;
|
||||
if (mode !== "source") previewMounted.current = true;
|
||||
|
||||
// The editor only feeds content to the preview when it's an `EditableCode`. `ReadOnlyCode`
|
||||
// doesn't expose `onContentChanged`, and in preview-only mode the editor isn't mounted at all —
|
||||
// in both cases we read the blob directly so the preview stays populated.
|
||||
const editorPropagatesContent = editorMounted.current && !readOnly;
|
||||
const fallbackBlob = useNoteBlob(editorPropagatesContent ? null : note);
|
||||
const onContentChangedRef = useRef(editorProps.onContentChanged);
|
||||
useEffect(() => { onContentChangedRef.current = editorProps.onContentChanged; });
|
||||
useEffect(() => {
|
||||
if (!editorPropagatesContent && fallbackBlob) {
|
||||
onContentChangedRef.current?.(fallbackBlob.content ?? "");
|
||||
}
|
||||
}, [ fallbackBlob, editorPropagatesContent ]);
|
||||
|
||||
const editor = editorMounted.current && (
|
||||
<div className="note-detail-split-editor-col">
|
||||
{editorBefore}
|
||||
<div className="note-detail-split-editor">
|
||||
<EditableCode
|
||||
note={note}
|
||||
lineWrapping={false}
|
||||
updateInterval={750} debounceUpdate
|
||||
noBackgroundChange
|
||||
{...editorProps}
|
||||
/>
|
||||
{readOnly
|
||||
? <ReadOnlyCode note={note} noteContext={noteContext} {...editorProps} />
|
||||
: <EditableCode
|
||||
note={note}
|
||||
noteContext={noteContext}
|
||||
lineWrapping={false}
|
||||
updateInterval={750} debounceUpdate
|
||||
noBackgroundChange
|
||||
{...editorProps}
|
||||
/>}
|
||||
</div>
|
||||
{error && (
|
||||
<Admonition type="caution" className="note-detail-error-container">
|
||||
@@ -67,15 +89,17 @@ function EditorWithSplit({ note, error, splitOptions, previewContent, previewBut
|
||||
</div>
|
||||
);
|
||||
|
||||
const preview = <PreviewContainer
|
||||
const preview = previewMounted.current && <PreviewContainer
|
||||
error={error}
|
||||
previewContent={previewContent}
|
||||
previewButtons={previewButtons}
|
||||
/>;
|
||||
|
||||
useEffect(() => {
|
||||
if (!utils.isDesktop() || !containerRef.current) return;
|
||||
const elements = Array.from(containerRef.current?.children) as HTMLElement[];
|
||||
if (mode !== "split" || !utils.isDesktop() || !containerRef.current) return;
|
||||
// Only the visible (non-display:none) panes participate in the split.
|
||||
const elements = (Array.from(containerRef.current.children) as HTMLElement[])
|
||||
.filter(el => el.offsetParent !== null);
|
||||
const splitInstance = Split(elements, {
|
||||
rtl: glob.isRtl,
|
||||
sizes: [ 50, 50 ],
|
||||
@@ -85,10 +109,14 @@ function EditorWithSplit({ note, error, splitOptions, previewContent, previewBut
|
||||
});
|
||||
|
||||
return () => splitInstance.destroy();
|
||||
}, [ splitEditorOrientation ]);
|
||||
}, [ splitEditorOrientation, mode ]);
|
||||
|
||||
const layoutClass = mode === "source" ? "split-source-only"
|
||||
: mode === "preview" ? "split-read-only"
|
||||
: `split-${splitEditorOrientation}`;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${splitEditorOrientation}`} ${className ?? ""}`}>
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${layoutClass} ${className ?? ""}`}>
|
||||
{splitEditorOrientation === "horizontal"
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
@@ -96,26 +124,6 @@ function EditorWithSplit({ note, error, splitOptions, previewContent, previewBut
|
||||
);
|
||||
}
|
||||
|
||||
function ReadOnlyView({ ...props }: SplitEditorProps) {
|
||||
const { note, onContentChanged } = props;
|
||||
const content = useNoteBlob(note);
|
||||
const onContentChangedRef = useRef(onContentChanged);
|
||||
|
||||
useEffect(() => {
|
||||
onContentChangedRef.current = onContentChanged;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onContentChangedRef.current?.(content?.content ?? "");
|
||||
}, [ content ]);
|
||||
|
||||
return (
|
||||
<div className={`note-detail-split note-detail-printable ${props.className} split-read-only`}>
|
||||
<PreviewContainer {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewContainer({ error, previewContent, previewButtons }: {
|
||||
error?: string | null;
|
||||
previewContent: ComponentChildren;
|
||||
|
||||
@@ -24,12 +24,12 @@ import UniverPresetSheetsSortEnUS from '@univerjs/preset-sheets-sort/locales/en-
|
||||
import { createUniver, FUniver, LocaleType, mergeLocales } from '@univerjs/presets';
|
||||
import { MutableRef, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import { useColorScheme, useNoteLabelBoolean, useTriliumEvent } from "../../react/hooks";
|
||||
import { useColorScheme, useEffectiveReadOnly, useTriliumEvent } from "../../react/hooks";
|
||||
import { TypeWidgetProps } from "../type_widget";
|
||||
import usePersistence from "./persistence";
|
||||
|
||||
export default function Spreadsheet(props: TypeWidgetProps) {
|
||||
const [ readOnly ] = useNoteLabelBoolean(props.note, "readOnly");
|
||||
const readOnly = useEffectiveReadOnly(props.note, props.noteContext);
|
||||
|
||||
// Use readOnly as key to force full remount (and data reload) when it changes.
|
||||
return <SpreadsheetEditor key={String(readOnly)} {...props} readOnly={readOnly} />;
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
/* h1 should not be used at all since semantically that's a note title */
|
||||
.note-detail-readonly-text h1 { font-size: 1.8em; }
|
||||
.note-detail-readonly-text h2 { font-size: 1.6em; }
|
||||
.note-detail-readonly-text h3 { font-size: 1.4em; }
|
||||
.note-detail-readonly-text h4 { font-size: 1.2em; }
|
||||
.note-detail-readonly-text h5 { font-size: 1.1em; }
|
||||
.note-detail-readonly-text h6 { font-size: 1.0em; }
|
||||
.note-detail-readonly-text-content {
|
||||
h1 { font-size: 1.8em; }
|
||||
h2 { font-size: 1.6em; }
|
||||
h3 { font-size: 1.4em; }
|
||||
h4 { font-size: 1.2em; }
|
||||
h5 { font-size: 1.1em; }
|
||||
h6 { font-size: 1.0em; }
|
||||
}
|
||||
|
||||
body.heading-style-markdown .note-detail-readonly-text h1::before { content: "#\2004"; color: var(--muted-text-color); }
|
||||
body.heading-style-markdown .note-detail-readonly-text h2::before { content: "##\2004"; color: var(--muted-text-color); }
|
||||
body.heading-style-markdown .note-detail-readonly-text h3::before { content: "###\2004"; color: var(--muted-text-color); }
|
||||
body.heading-style-markdown .note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\2004"; color: var(--muted-text-color); }
|
||||
body.heading-style-markdown .note-detail-readonly-text h5::before { content: "#####\2004"; color: var(--muted-text-color); }
|
||||
body.heading-style-markdown .note-detail-readonly-text h6::before { content: "######\2004"; color: var(--muted-text-color); }
|
||||
body.heading-style-markdown .note-detail-readonly-text-content {
|
||||
h1::before { content: "#\2004"; color: var(--muted-text-color); }
|
||||
h2::before { content: "##\2004"; color: var(--muted-text-color); }
|
||||
h3::before { content: "###\2004"; color: var(--muted-text-color); }
|
||||
h4:not(.include-note-title)::before { content: "####\2004"; color: var(--muted-text-color); }
|
||||
h5::before { content: "#####\2004"; color: var(--muted-text-color); }
|
||||
h6::before { content: "######\2004"; color: var(--muted-text-color); }
|
||||
}
|
||||
|
||||
body.heading-style-underline .note-detail-readonly-text h1 { border-bottom: 1px solid var(--main-border-color); }
|
||||
body.heading-style-underline .note-detail-readonly-text h2 { border-bottom: 1px solid var(--main-border-color); }
|
||||
body.heading-style-underline .note-detail-readonly-text h3 { border-bottom: 1px solid var(--main-border-color); }
|
||||
body.heading-style-underline .note-detail-readonly-text h4:not(.include-note-title) { border-bottom: 1px solid var(--main-border-color); }
|
||||
body.heading-style-underline .note-detail-readonly-text h5 { border-bottom: 1px solid var(--main-border-color); }
|
||||
body.heading-style-underline .note-detail-readonly-text h6 { border-bottom: 1px solid var(--main-border-color); }
|
||||
body.heading-style-underline .note-detail-readonly-text-content {
|
||||
h1 { border-bottom: 1px solid var(--main-border-color); }
|
||||
h2 { border-bottom: 1px solid var(--main-border-color); }
|
||||
h3 { border-bottom: 1px solid var(--main-border-color); }
|
||||
h4:not(.include-note-title) { border-bottom: 1px solid var(--main-border-color); }
|
||||
h5 { border-bottom: 1px solid var(--main-border-color); }
|
||||
h6 { border-bottom: 1px solid var(--main-border-color); }
|
||||
}
|
||||
|
||||
.note-detail-readonly-text {
|
||||
padding-inline-start: 24px;
|
||||
@@ -65,4 +71,4 @@ body.mobile .note-detail-readonly-text {
|
||||
|
||||
.note-detail-readonly-text-content code.copyable-inline-code:hover {
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import "./ReadOnlyText.css";
|
||||
import "@triliumnext/ckeditor5";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||
import { Ref } from "preact";
|
||||
import { useEffect, useLayoutEffect, useMemo } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
@@ -13,7 +14,7 @@ import { applyInlineMermaid, rewriteMermaidDiagramsInContainer } from "../../../
|
||||
import { getLocaleById } from "../../../services/i18n";
|
||||
import { renderMathInElement } from "../../../services/math";
|
||||
import { formatCodeBlocks } from "../../../services/syntax_highlight";
|
||||
import { useNoteBlob, useNoteLabel, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import { useNoteBlob, useNoteLabel, useSyncedRef, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import TouchBar, { TouchBarButton, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import { TypeWidgetProps } from "../type_widget";
|
||||
@@ -22,51 +23,14 @@ import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./util
|
||||
|
||||
export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
const blob = useNoteBlob(note);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [ codeBlockWordWrap ] = useTriliumOptionBool("codeBlockWordWrap");
|
||||
const [ codeBlockTabWidth ] = useTriliumOption("codeBlockTabWidth");
|
||||
const { isRtl } = useNoteLanguage(note);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.setProperty("--code-block-tab-width", codeBlockTabWidth || "4");
|
||||
}, [codeBlockTabWidth]);
|
||||
|
||||
// Apply necessary transforms.
|
||||
useEffect(() => {
|
||||
const container = contentRef.current;
|
||||
if (!container) return;
|
||||
|
||||
appContext.triggerEvent("contentElRefreshed", { ntxId, contentEl: container });
|
||||
|
||||
rewriteMermaidDiagramsInContainer(container);
|
||||
applyInlineMermaid(container);
|
||||
applyIncludedNotes(container);
|
||||
applyMath(container);
|
||||
applyReferenceLinks(container);
|
||||
formatCodeBlocks($(container));
|
||||
setupImageOpening(container, true);
|
||||
}, [ blob ]);
|
||||
|
||||
// React to included note changes.
|
||||
useTriliumEvent("refreshIncludedNote", ({ noteId }) => {
|
||||
if (!contentRef.current) return;
|
||||
refreshIncludedNote(contentRef.current, noteId);
|
||||
});
|
||||
|
||||
// Search integration.
|
||||
useTriliumEvent("executeWithContentElement", ({ resolve, ntxId: eventNtxId }) => {
|
||||
if (eventNtxId !== ntxId || !contentRef.current) return;
|
||||
resolve($(contentRef.current));
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<RawHtmlBlock
|
||||
containerRef={contentRef}
|
||||
className={clsx("note-detail-readonly-text-content ck-content use-tn-links selectable-text", codeBlockWordWrap && "word-wrap")}
|
||||
tabindex={100}
|
||||
<ReadOnlyTextContent
|
||||
html={blob?.content ?? ""}
|
||||
ntxId={ntxId}
|
||||
dir={isRtl ? "rtl" : "ltr"}
|
||||
html={blob?.content}
|
||||
/>
|
||||
|
||||
<TouchBar>
|
||||
@@ -85,6 +49,77 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
|
||||
);
|
||||
}
|
||||
|
||||
interface ReadOnlyTextContentProps {
|
||||
/** CKEditor-compatible HTML to render. */
|
||||
html: string;
|
||||
/** Note context id — enables `contentElRefreshed` / `executeWithContentElement` integrations when provided. */
|
||||
ntxId?: string | null;
|
||||
dir?: "ltr" | "rtl";
|
||||
/** Extra classes appended to the content div. */
|
||||
className?: string;
|
||||
/** Optional external ref to the rendered content div (e.g. to drive scroll sync). */
|
||||
contentRef?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders arbitrary CKEditor-style HTML with the same pipeline as {@link ReadOnlyText}:
|
||||
* mermaid rewriting, inline mermaid, included-note expansion, KaTeX math, reference-link
|
||||
* titles, code-block syntax highlighting, and image click handling. Transforms re-run
|
||||
* whenever `html` changes.
|
||||
*/
|
||||
export function ReadOnlyTextContent({ html, ntxId, dir, className, contentRef: externalContentRef }: ReadOnlyTextContentProps) {
|
||||
const contentRef = useSyncedRef(externalContentRef);
|
||||
const [ codeBlockWordWrap ] = useTriliumOptionBool("codeBlockWordWrap");
|
||||
const [ codeBlockTabWidth ] = useTriliumOption("codeBlockTabWidth");
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.setProperty("--code-block-tab-width", codeBlockTabWidth || "4");
|
||||
}, [codeBlockTabWidth]);
|
||||
|
||||
// Apply necessary transforms. Runs in a layout effect so the synchronous
|
||||
// DOM mutations (mermaid rewrite + cached-SVG repaint, math, etc.) happen
|
||||
// before the browser paints — prevents a flash of raw `<pre>` content
|
||||
// during live preview re-renders.
|
||||
useLayoutEffect(() => {
|
||||
const container = contentRef.current;
|
||||
if (!container) return;
|
||||
|
||||
if (ntxId) {
|
||||
appContext.triggerEvent("contentElRefreshed", { ntxId, contentEl: container });
|
||||
}
|
||||
|
||||
rewriteMermaidDiagramsInContainer(container);
|
||||
applyInlineMermaid(container);
|
||||
applyIncludedNotes(container);
|
||||
applyMath(container);
|
||||
applyReferenceLinks(container);
|
||||
formatCodeBlocks($(container));
|
||||
setupImageOpening(container, true);
|
||||
}, [ html, ntxId, contentRef ]);
|
||||
|
||||
// React to included note changes.
|
||||
useTriliumEvent("refreshIncludedNote", ({ noteId }) => {
|
||||
if (!contentRef.current) return;
|
||||
refreshIncludedNote(contentRef.current, noteId);
|
||||
});
|
||||
|
||||
// Search integration.
|
||||
useTriliumEvent("executeWithContentElement", ({ resolve, ntxId: eventNtxId }) => {
|
||||
if (!ntxId || eventNtxId !== ntxId || !contentRef.current) return;
|
||||
resolve($(contentRef.current));
|
||||
});
|
||||
|
||||
return (
|
||||
<RawHtmlBlock
|
||||
containerRef={contentRef}
|
||||
className={clsx("note-detail-readonly-text-content ck-content use-tn-links selectable-text", codeBlockWordWrap && "word-wrap", className)}
|
||||
tabindex={100}
|
||||
dir={dir}
|
||||
html={html}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useNoteLanguage(note: FNote) {
|
||||
const [ language ] = useNoteLabel(note, "language");
|
||||
const isRtl = useMemo(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "Tool to compare content of Trilium databases. Useful for debugging sync problems.",
|
||||
"dependencies": {
|
||||
"colors": "1.4.0",
|
||||
"diff": "8.0.4",
|
||||
"diff": "9.0.0",
|
||||
"sqlite": "5.1.1",
|
||||
"sqlite3": "6.0.1"
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.3",
|
||||
"better-sqlite3": "12.8.0",
|
||||
"better-sqlite3": "12.9.0",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-dl": "4.0.0",
|
||||
"electron-squirrel-startup": "1.0.1"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.8.0",
|
||||
"better-sqlite3": "12.9.0",
|
||||
"mime-types": "3.0.2",
|
||||
"sanitize-filename": "1.6.4",
|
||||
"tsx": "4.21.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
|
||||
"dependencies": {
|
||||
"archiver": "7.0.1",
|
||||
"better-sqlite3": "12.8.0"
|
||||
"better-sqlite3": "12.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@triliumnext/client": "workspace:*",
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"e2e": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "17.4.1"
|
||||
"dotenv": "17.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.8.0"
|
||||
"better-sqlite3": "12.9.0"
|
||||
}
|
||||
}
|
||||
@@ -31,16 +31,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.69",
|
||||
"@ai-sdk/google": "3.0.62",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/openai": "3.0.52",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"ai": "6.0.158",
|
||||
"better-sqlite3": "12.8.0",
|
||||
"ai": "6.0.159",
|
||||
"better-sqlite3": "12.9.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"js-yaml": "4.1.1",
|
||||
"node-html-parser": "7.1.0",
|
||||
"sucrase": "3.35.1",
|
||||
"unpdf": "1.4.0"
|
||||
"unpdf": "1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
@@ -118,7 +118,7 @@
|
||||
"rand-token": "1.0.1",
|
||||
"safe-compare": "1.1.4",
|
||||
"sanitize-filename": "1.6.4",
|
||||
"sanitize-html": "2.17.2",
|
||||
"sanitize-html": "2.17.3",
|
||||
"sax": "1.6.0",
|
||||
"serve-favicon": "2.5.1",
|
||||
"stream-throttle": "0.1.3",
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
2
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html
generated
vendored
@@ -16,6 +16,8 @@
|
||||
</li>
|
||||
<li><a href="https://github.com/perfectra1n/triliumnext-mcp">perfectra1n/triliumnext-mcp</a>
|
||||
</li>
|
||||
<li><a href="https://github.com/eliassoares/trilium-fastmcp">eliassoares/trilium-fastmcp</a>
|
||||
</li>
|
||||
</ul>
|
||||
<aside class="admonition important">
|
||||
<p>These solutions are third-party and thus not endorsed or supported directly
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<img style="aspect-ratio:1124/571;" src="2_SQL Console_image.png"
|
||||
width="1124" height="571">
|
||||
</figure>
|
||||
|
||||
<h3>Interacting with the table</h3>
|
||||
<p>After executing a query, a table with the results will be displayed:</p>
|
||||
<ul>
|
||||
|
||||
@@ -6,8 +6,7 @@ class="image">
|
||||
<img style="aspect-ratio:1144/660;" src="Sharing_image.png"
|
||||
width="1144" height="660">
|
||||
</figure>
|
||||
|
||||
<h2>Features, interaction and limitations</h2>
|
||||
<h2>Features, interaction and limitations</h2>
|
||||
<ul>
|
||||
<li>Searching by note title.</li>
|
||||
<li>Automatic dark/light mode based on the user's browser settings.</li>
|
||||
@@ -419,8 +418,7 @@ for (const attr of parentNote.attributes) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Customizing logo</h3>
|
||||
<h3>Customizing logo</h3>
|
||||
<p>It's possible to adjust the logo which is displayed on the top-left of
|
||||
the left pane.</p>
|
||||
<table>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<img style="aspect-ratio:991/403;" src="1_Jump to_image.png"
|
||||
width="991" height="403">
|
||||
</figure>
|
||||
|
||||
<h2>Jump to Note</h2>
|
||||
<p>The <em>Jump to Note</em> function allows easy navigation between notes
|
||||
by searching for their title. In addition to that, it can also trigger
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<figcaption>Screenshot of the note contextual menu indicating the “Export as PDF”
|
||||
option.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2>Printing</h2>
|
||||
<p>This feature allows printing of notes. It works on both the desktop client,
|
||||
but also on the web.</p>
|
||||
|
||||
@@ -15,8 +15,7 @@ class="image">
|
||||
<img style="aspect-ratio:1150/27;" src="5_New Layout_image.png"
|
||||
width="1150" height="27">
|
||||
</figure>
|
||||
|
||||
<h3>Inline title</h3>
|
||||
<h3>Inline title</h3>
|
||||
<p>In previous versions of Trilium, the title bar was fixed at all times.
|
||||
In the new layout, there is both a fixed title bar and one that scrolls
|
||||
with the text. The newly introduced title is called the <em>Inline title</em> and
|
||||
@@ -42,8 +41,7 @@ class="image">
|
||||
width="910" height="104">
|
||||
<figcaption>The fixed title bar. The title only appears after scrolling past the <em>Inline title</em>.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>New note type switcher</h3>
|
||||
<h3>New note type switcher</h3>
|
||||
<p>When a new <a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a> or
|
||||
<a
|
||||
class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a> note is created, a note type switcher will appear below
|
||||
@@ -52,8 +50,7 @@ class="image">
|
||||
<p>The switcher will disappear as soon as a text is entered.</p>
|
||||
<img src="6_New Layout_image.png"
|
||||
width="735" height="143">
|
||||
|
||||
<h3>Note badges</h3>
|
||||
<h3>Note badges</h3>
|
||||
<p>Note badges appear near the fixed note title and indicate important information
|
||||
about the note such as whether it is read-only. Some of the badges are
|
||||
also interactive.</p>
|
||||
|
||||
31
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note types with split view.html
generated
vendored
Normal file
31
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note types with split view.html
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<p>Split view is a feature of <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_s1aBHPd79XYj">Mermaid Diagrams</a> and
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6RM1Q7ppFVoj">Markdown</a> 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> 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>
|
||||
<p>The split comes with three different display modes:</p>
|
||||
<ul>
|
||||
<li><em>Split view</em>, in which both the source code is available on one
|
||||
side and can be edited, and the preview is available on the other side.
|
||||
<ul>
|
||||
<li>In this mode, the size of either the source pane or the preview pane can
|
||||
be adjusted by dragging the small border between them.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><em>Source view</em> which shows the source code on the entire screen for
|
||||
a more focused editing experience.</li>
|
||||
<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 <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_8YBEPzcpUgxw">Note buttons</a> section
|
||||
on the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
|
||||
or in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_XpOYSgsLkTJy">Floating buttons</a> 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>
|
||||
@@ -2,7 +2,6 @@
|
||||
<img style="aspect-ratio:988/572;" src="1_Recent Changes_image.png"
|
||||
width="988" height="572">
|
||||
</figure>
|
||||
|
||||
<h2>Accessing the recent changes</h2>
|
||||
<ul>
|
||||
<li>For an overview of the changes across all documents, press the
|
||||
|
||||
@@ -4,8 +4,7 @@ class="image image-style-align-center">
|
||||
<img style="aspect-ratio:1398/1015;" src="Split View_2_Split View_im.png"
|
||||
width="1398" height="1015">
|
||||
</figure>
|
||||
|
||||
<h2><strong>Interactions</strong></h2>
|
||||
<h2><strong>Interactions</strong></h2>
|
||||
<ul>
|
||||
<li>Press the
|
||||
<img src="Split View_Split View_imag.png">button to the right of a note's title to open a new split to the right
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Calendar.html
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Calendar.html
generated
vendored
@@ -356,7 +356,6 @@
|
||||
</ul>
|
||||
<img src="6_Calendar_image.png" width="1217"
|
||||
height="724">
|
||||
|
||||
<h3>Using a different attribute as event title</h3>
|
||||
<p>By default, events are displayed on the calendar by their note title.
|
||||
However, it is possible to configure a different attribute to be displayed
|
||||
@@ -389,7 +388,6 @@ height="724">
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Using a relation attribute as event title</h3>
|
||||
<p>Similarly to using an attribute, use <code spellcheck="false">#calendar:title</code> and
|
||||
set it to <code spellcheck="false">name</code> where <code spellcheck="false">name</code> is
|
||||
|
||||
1
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Geo Map.html
generated
vendored
1
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Geo Map.html
generated
vendored
@@ -397,7 +397,6 @@ width="1288" height="278">
|
||||
<img style="aspect-ratio:678/499;" src="12_Geo Map_image.png"
|
||||
width="678" height="499">
|
||||
</figure>
|
||||
|
||||
<h3>Grid-like artifacts on the map</h3>
|
||||
<p>This occurs if the application is not at 100% zoom which causes the pixels
|
||||
of the map to not render correctly due to fractional scaling. The only
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
also some backup capabilities by its nature of distributing the data to
|
||||
other computers.</p>
|
||||
<h2>Downloading backup</h2>
|
||||
<p>You can download a existing backup by going to Settings > Backup >
|
||||
<p>You can download an existing backup by going to Settings > Backup >
|
||||
Existing backups > Download</p>
|
||||
<h2>Restoring backup</h2>
|
||||
<p>Let's assume you want to restore the weekly backup, here's how to do it:</p>
|
||||
|
||||
517
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Miscellaneous/License.html
generated
vendored
Normal file
517
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Miscellaneous/License.html
generated
vendored
Normal file
@@ -0,0 +1,517 @@
|
||||
<p>GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
<br>Version 3, 19 November 2007</p>
|
||||
<p>Copyright (C) 2007 Free Software Foundation, Inc. <a href="https://fsf.org/">https://fsf.org/</a>
|
||||
</p>
|
||||
<p>Everyone is permitted to copy and distribute verbatim copies of this license
|
||||
document, but changing it is not allowed.</p>
|
||||
<h2>Preamble</h2>
|
||||
<p>The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure cooperation
|
||||
with the community in the case of network server software.</p>
|
||||
<p>The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast, our
|
||||
General Public Licenses are intended to guarantee your freedom to share
|
||||
and change all versions of a program--to make sure it remains free software
|
||||
for all its users.</p>
|
||||
<p>When we speak of free software, we are referring to freedom, not price.
|
||||
Our General Public Licenses are designed to make sure that you have the
|
||||
freedom to distribute copies of free software (and charge for them if you
|
||||
wish), that you receive source code or can get it if you want it, that
|
||||
you can change the software or use pieces of it in new free programs, and
|
||||
that you know you can do these things.</p>
|
||||
<p>Developers that use our General Public Licenses protect your rights with
|
||||
two steps: (1) assert copyright on the software, and (2) offer you this
|
||||
License which gives you legal permission to copy, distribute and/or modify
|
||||
the software.</p>
|
||||
<p>A secondary benefit of defending all users' freedom is that improvements
|
||||
made in alternate versions of the program, if they receive widespread use,
|
||||
become available for other developers to incorporate. Many developers of
|
||||
free software are heartened and encouraged by the resulting cooperation.
|
||||
However, in the case of software used on network servers, this result may
|
||||
fail to come about. The GNU General Public License permits making a modified
|
||||
version and letting the public access it on a server without ever releasing
|
||||
its source code to the public.</p>
|
||||
<p>The GNU Affero General Public License is designed specifically to ensure
|
||||
that, in such cases, the modified source code becomes available to the
|
||||
community. It requires the operator of a network server to provide the
|
||||
source code of the modified version running there to the users of that
|
||||
server. Therefore, public use of a modified version, on a publicly accessible
|
||||
server, gives the public access to the source code of the modified version.</p>
|
||||
<p>An older license, called the Affero General Public License and published
|
||||
by Affero, was designed to accomplish similar goals. This is a different
|
||||
license, not a version of the Affero GPL, but Affero has released a new
|
||||
version of the Affero GPL which permits relicensing under this license.</p>
|
||||
<p>The precise terms and conditions for copying, distribution and modification
|
||||
follow.</p>
|
||||
<h2>TERMS AND CONDITIONS</h2>
|
||||
<h3>0. Definitions.</h3>
|
||||
<p>"This License" refers to version 3 of the GNU Affero General Public License.</p>
|
||||
<p>"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.</p>
|
||||
<p>"The Program" refers to any copyrightable work licensed under this License.
|
||||
Each licensee is addressed as "you". "Licensees" and "recipients" may be
|
||||
individuals or organizations.</p>
|
||||
<p>To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the earlier
|
||||
work or a work "based on" the earlier work.</p>
|
||||
<p>A "covered work" means either the unmodified Program or a work based on
|
||||
the Program.</p>
|
||||
<p>To "propagate" a work means to do anything with it that, without permission,
|
||||
would make you directly or secondarily liable for infringement under applicable
|
||||
copyright law, except executing it on a computer or modifying a private
|
||||
copy. Propagation includes copying, distribution (with or without modification),
|
||||
making available to the public, and in some countries other activities
|
||||
as well.</p>
|
||||
<p>To "convey" a work means any kind of propagation that enables other parties
|
||||
to make or receive copies. Mere interaction with a user through a computer
|
||||
network, with no transfer of a copy, is not conveying.</p>
|
||||
<p>An interactive user interface displays "Appropriate Legal Notices" to
|
||||
the extent that it includes a convenient and prominently visible feature
|
||||
that (1) displays an appropriate copyright notice, and (2) tells the user
|
||||
that there is no warranty for the work (except to the extent that warranties
|
||||
are provided), that licensees may convey the work under this License, and
|
||||
how to view a copy of this License. If the interface presents a list of
|
||||
user commands or options, such as a menu, a prominent item in the list
|
||||
meets this criterion.</p>
|
||||
<h3>1. Source Code.</h3>
|
||||
<p>The "source code" for a work means the preferred form of the work for
|
||||
making modifications to it. "Object code" means any non-source form of
|
||||
a work.</p>
|
||||
<p>A "Standard Interface" means an interface that either is an official standard
|
||||
defined by a recognized standards body, or, in the case of interfaces specified
|
||||
for a particular programming language, one that is widely used among developers
|
||||
working in that language.</p>
|
||||
<p>The "System Libraries" of an executable work include anything, other than
|
||||
the work as a whole, that (a) is included in the normal form of packaging
|
||||
a Major Component, but which is not part of that Major Component, and (b)
|
||||
serves only to enable use of the work with that Major Component, or to
|
||||
implement a Standard Interface for which an implementation is available
|
||||
to the public in source code form. A "Major Component", in this context,
|
||||
means a major essential component (kernel, window system, and so on) of
|
||||
the specific operating system (if any) on which the executable work runs,
|
||||
or a compiler used to produce the work, or an object code interpreter used
|
||||
to run it.</p>
|
||||
<p>The "Corresponding Source" for a work in object code form means all the
|
||||
source code needed to generate, install, and (for an executable work) run
|
||||
the object code and to modify the work, including scripts to control those
|
||||
activities. However, it does not include the work's System Libraries, or
|
||||
general-purpose tools or generally available free programs which are used
|
||||
unmodified in performing those activities but which are not part of the
|
||||
work. For example, Corresponding Source includes interface definition files
|
||||
associated with source files for the work, and the source code for shared
|
||||
libraries and dynamically linked subprograms that the work is specifically
|
||||
designed to require, such as by intimate data communication or control
|
||||
flow between those subprograms and other parts of the work.</p>
|
||||
<p>The Corresponding Source need not include anything that users can regenerate
|
||||
automatically from other parts of the Corresponding Source.</p>
|
||||
<p>The Corresponding Source for a work in source code form is that same work.</p>
|
||||
<h3>2. Basic Permissions.</h3>
|
||||
<p>All rights granted under this License are granted for the term of copyright
|
||||
on the Program, and are irrevocable provided the stated conditions are
|
||||
met. This License explicitly affirms your unlimited permission to run the
|
||||
unmodified Program. The output from running a covered work is covered by
|
||||
this License only if the output, given its content, constitutes a covered
|
||||
work. This License acknowledges your rights of fair use or other equivalent,
|
||||
as provided by copyright law.</p>
|
||||
<p>You may make, run and propagate covered works that you do not convey,
|
||||
without conditions so long as your license otherwise remains in force.
|
||||
You may convey covered works to others for the sole purpose of having them
|
||||
make modifications exclusively for you, or provide you with facilities
|
||||
for running those works, provided that you comply with the terms of this
|
||||
License in conveying all material for which you do not control copyright.
|
||||
Those thus making or running the covered works for you must do so exclusively
|
||||
on your behalf, under your direction and control, on terms that prohibit
|
||||
them from making any copies of your copyrighted material outside their
|
||||
relationship with you.</p>
|
||||
<p>Conveying under any other circumstances is permitted solely under the
|
||||
conditions stated below. Sublicensing is not allowed; section 10 makes
|
||||
it unnecessary.</p>
|
||||
<h3>3. Protecting Users' Legal Rights From Anti-Circumvention Law.</h3>
|
||||
<p>No covered work shall be deemed part of an effective technological measure
|
||||
under any applicable law fulfilling obligations under article 11 of the
|
||||
WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting
|
||||
or restricting circumvention of such measures.</p>
|
||||
<p>When you convey a covered work, you waive any legal power to forbid circumvention
|
||||
of technological measures to the extent such circumvention is effected
|
||||
by exercising rights under this License with respect to the covered work,
|
||||
and you disclaim any intention to limit operation or modification of the
|
||||
work as a means of enforcing, against the work's users, your or third parties'
|
||||
legal rights to forbid circumvention of technological measures.</p>
|
||||
<h3>4. Conveying Verbatim Copies.</h3>
|
||||
<p>You may convey verbatim copies of the Program's source code as you receive
|
||||
it, in any medium, provided that you conspicuously and appropriately publish
|
||||
on each copy an appropriate copyright notice; keep intact all notices stating
|
||||
that this License and any non-permissive terms added in accord with section
|
||||
7 apply to the code; keep intact all notices of the absence of any warranty;
|
||||
and give all recipients a copy of this License along with the Program.</p>
|
||||
<p>You may charge any price or no price for each copy that you convey, and
|
||||
you may offer support or warranty protection for a fee.</p>
|
||||
<h3>5. Conveying Modified Source Versions.</h3>
|
||||
<p>You may convey a work based on the Program, or the modifications to produce
|
||||
it from the Program, in the form of source code under the terms of section
|
||||
4, provided that you also meet all of these conditions:</p>
|
||||
<ul>
|
||||
<li>a) The work must carry prominent notices stating that you modified it,
|
||||
and giving a relevant date.</li>
|
||||
<li>b) The work must carry prominent notices stating that it is released under
|
||||
this License and any conditions added under section 7. This requirement
|
||||
modifies the requirement in section 4 to "keep intact all notices".</li>
|
||||
<li>c) You must license the entire work, as a whole, under this License to
|
||||
anyone who comes into possession of a copy. This License will therefore
|
||||
apply, along with any applicable section 7 additional terms, to the whole
|
||||
of the work, and all its parts, regardless of how they are packaged. This
|
||||
License gives no permission to license the work in any other way, but it
|
||||
does not invalidate such permission if you have separately received it.</li>
|
||||
<li>d) If the work has interactive user interfaces, each must display Appropriate
|
||||
Legal Notices; however, if the Program has interactive interfaces that
|
||||
do not display Appropriate Legal Notices, your work need not make them
|
||||
do so.</li>
|
||||
</ul>
|
||||
<p>A compilation of a covered work with other separate and independent works,
|
||||
which are not by their nature extensions of the covered work, and which
|
||||
are not combined with it such as to form a larger program, in or on a volume
|
||||
of a storage or distribution medium, is called an "aggregate" if the compilation
|
||||
and its resulting copyright are not used to limit the access or legal rights
|
||||
of the compilation's users beyond what the individual works permit. Inclusion
|
||||
of a covered work in an aggregate does not cause this License to apply
|
||||
to the other parts of the aggregate.</p>
|
||||
<h3>6. Conveying Non-Source Forms.</h3>
|
||||
<p>You may convey a covered work in object code form under the terms of sections
|
||||
4 and 5, provided that you also convey the machine-readable Corresponding
|
||||
Source under the terms of this License, in one of these ways:</p>
|
||||
<ul>
|
||||
<li>a) Convey the object code in, or embodied in, a physical product (including
|
||||
a physical distribution medium), accompanied by the Corresponding Source
|
||||
fixed on a durable physical medium customarily used for software interchange.</li>
|
||||
<li>b) Convey the object code in, or embodied in, a physical product (including
|
||||
a physical distribution medium), accompanied by a written offer, valid
|
||||
for at least three years and valid for as long as you offer spare parts
|
||||
or customer support for that product model, to give anyone who possesses
|
||||
the object code either (1) a copy of the Corresponding Source for all the
|
||||
software in the product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no more than
|
||||
your reasonable cost of physically performing this conveying of source,
|
||||
or (2) access to copy the Corresponding Source from a network server at
|
||||
no charge.</li>
|
||||
<li>c) Convey individual copies of the object code with a copy of the written
|
||||
offer to provide the Corresponding Source. This alternative is allowed
|
||||
only occasionally and noncommercially, and only if you received the object
|
||||
code with such an offer, in accord with subsection 6b.</li>
|
||||
<li>d) Convey the object code by offering access from a designated place (gratis
|
||||
or for a charge), and offer equivalent access to the Corresponding Source
|
||||
in the same way through the same place at no further charge. You need not
|
||||
require recipients to copy the Corresponding Source along with the object
|
||||
code. If the place to copy the object code is a network server, the Corresponding
|
||||
Source may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain clear
|
||||
directions next to the object code saying where to find the Corresponding
|
||||
Source. Regardless of what server hosts the Corresponding Source, you remain
|
||||
obligated to ensure that it is available for as long as needed to satisfy
|
||||
these requirements.</li>
|
||||
<li>e) Convey the object code using peer-to-peer transmission, provided you
|
||||
inform other peers where the object code and Corresponding Source of the
|
||||
work are being offered to the general public at no charge under subsection
|
||||
6d.</li>
|
||||
</ul>
|
||||
<p>A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be included
|
||||
in conveying the object code work.</p>
|
||||
<p>A "User Product" is either (1) a "consumer product", which means any tangible
|
||||
personal property which is normally used for personal, family, or household
|
||||
purposes, or (2) anything designed or sold for incorporation into a dwelling.
|
||||
In determining whether a product is a consumer product, doubtful cases
|
||||
shall be resolved in favor of coverage. For a particular product received
|
||||
by a particular user, "normally used" refers to a typical or common use
|
||||
of that class of product, regardless of the status of the particular user
|
||||
or of the way in which the particular user actually uses, or expects or
|
||||
is expected to use, the product. A product is a consumer product regardless
|
||||
of whether the product has substantial commercial, industrial or non-consumer
|
||||
uses, unless such uses represent the only significant mode of use of the
|
||||
product.</p>
|
||||
<p>"Installation Information" for a User Product means any methods, procedures,
|
||||
authorization keys, or other information required to install and execute
|
||||
modified versions of a covered work in that User Product from a modified
|
||||
version of its Corresponding Source. The information must suffice to ensure
|
||||
that the continued functioning of the modified object code is in no case
|
||||
prevented or interfered with solely because modification has been made.</p>
|
||||
<p>If you convey an object code work under this section in, or with, or specifically
|
||||
for use in, a User Product, and the conveying occurs as part of a transaction
|
||||
in which the right of possession and use of the User Product is transferred
|
||||
to the recipient in perpetuity or for a fixed term (regardless of how the
|
||||
transaction is characterized), the Corresponding Source conveyed under
|
||||
this section must be accompanied by the Installation Information. But this
|
||||
requirement does not apply if neither you nor any third party retains the
|
||||
ability to install modified object code on the User Product (for example,
|
||||
the work has been installed in ROM).</p>
|
||||
<p>The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to
|
||||
a network may be denied when the modification itself materially and adversely
|
||||
affects the operation of the network or violates the rules and protocols
|
||||
for communication across the network.</p>
|
||||
<p>Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly documented
|
||||
(and with an implementation available to the public in source code form),
|
||||
and must require no special password or key for unpacking, reading or copying.</p>
|
||||
<h3>7. Additional Terms.</h3>
|
||||
<p>"Additional permissions" are terms that supplement the terms of this License
|
||||
by making exceptions from one or more of its conditions. Additional permissions
|
||||
that are applicable to the entire Program shall be treated as though they
|
||||
were included in this License, to the extent that they are valid under
|
||||
applicable law. If additional permissions apply only to part of the Program,
|
||||
that part may be used separately under those permissions, but the entire
|
||||
Program remains governed by this License without regard to the additional
|
||||
permissions.</p>
|
||||
<p>When you convey a copy of a covered work, you may at your option remove
|
||||
any additional permissions from that copy, or from any part of it. (Additional
|
||||
permissions may be written to require their own removal in certain cases
|
||||
when you modify the work.) You may place additional permissions on material,
|
||||
added by you to a covered work, for which you have or can give appropriate
|
||||
copyright permission.</p>
|
||||
<p>Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders
|
||||
of that material) supplement the terms of this License with terms:</p>
|
||||
<ul>
|
||||
<li>a) Disclaiming warranty or limiting liability differently from the terms
|
||||
of sections 15 and 16 of this License; or</li>
|
||||
<li>b) Requiring preservation of specified reasonable legal notices or author
|
||||
attributions in that material or in the Appropriate Legal Notices displayed
|
||||
by works containing it; or</li>
|
||||
<li>c) Prohibiting misrepresentation of the origin of that material, or requiring
|
||||
that modified versions of such material be marked in reasonable ways as
|
||||
different from the original version; or</li>
|
||||
<li>d) Limiting the use for publicity purposes of names of licensors or authors
|
||||
of the material; or</li>
|
||||
<li>e) Declining to grant rights under trademark law for use of some trade
|
||||
names, trademarks, or service marks; or</li>
|
||||
<li>f) Requiring indemnification of licensors and authors of that material
|
||||
by anyone who conveys the material (or modified versions of it) with contractual
|
||||
assumptions of liability to the recipient, for any liability that these
|
||||
contractual assumptions directly impose on those licensors and authors.</li>
|
||||
</ul>
|
||||
<p>All other non-permissive additional terms are considered "further restrictions"
|
||||
within the meaning of section 10. If the Program as you received it, or
|
||||
any part of it, contains a notice stating that it is governed by this License
|
||||
along with a term that is a further restriction, you may remove that term.
|
||||
If a license document contains a further restriction but permits relicensing
|
||||
or conveying under this License, you may add to a covered work material
|
||||
governed by the terms of that license document, provided that the further
|
||||
restriction does not survive such relicensing or conveying.</p>
|
||||
<p>If you add terms to a covered work in accord with this section, you must
|
||||
place, in the relevant source files, a statement of the additional terms
|
||||
that apply to those files, or a notice indicating where to find the applicable
|
||||
terms.</p>
|
||||
<p>Additional terms, permissive or non-permissive, may be stated in the form
|
||||
of a separately written license, or stated as exceptions; the above requirements
|
||||
apply either way.</p>
|
||||
<h3>8. Termination.</h3>
|
||||
<p>You may not propagate or modify a covered work except as expressly provided
|
||||
under this License. Any attempt otherwise to propagate or modify it is
|
||||
void, and will automatically terminate your rights under this License (including
|
||||
any patent licenses granted under the third paragraph of section 11).</p>
|
||||
<p>However, if you cease all violation of this License, then your license
|
||||
from a particular copyright holder is reinstated (a) provisionally, unless
|
||||
and until the copyright holder explicitly and finally terminates your license,
|
||||
and (b) permanently, if the copyright holder fails to notify you of the
|
||||
violation by some reasonable means prior to 60 days after the cessation.</p>
|
||||
<p>Moreover, your license from a particular copyright holder is reinstated
|
||||
permanently if the copyright holder notifies you of the violation by some
|
||||
reasonable means, this is the first time you have received notice of violation
|
||||
of this License (for any work) from that copyright holder, and you cure
|
||||
the violation prior to 30 days after your receipt of the notice.</p>
|
||||
<p>Termination of your rights under this section does not terminate the licenses
|
||||
of parties who have received copies or rights from you under this License.
|
||||
If your rights have been terminated and not permanently reinstated, you
|
||||
do not qualify to receive new licenses for the same material under section
|
||||
10.</p>
|
||||
<h3>9. Acceptance Not Required for Having Copies.</h3>
|
||||
<p>You are not required to accept this License in order to receive or run
|
||||
a copy of the Program. Ancillary propagation of a covered work occurring
|
||||
solely as a consequence of using peer-to-peer transmission to receive a
|
||||
copy likewise does not require acceptance. However, nothing other than
|
||||
this License grants you permission to propagate or modify any covered work.
|
||||
These actions infringe copyright if you do not accept this License. Therefore,
|
||||
by modifying or propagating a covered work, you indicate your acceptance
|
||||
of this License to do so.</p>
|
||||
<h3>10. Automatic Licensing of Downstream Recipients.</h3>
|
||||
<p>Each time you convey a covered work, the recipient automatically receives
|
||||
a license from the original licensors, to run, modify and propagate that
|
||||
work, subject to this License. You are not responsible for enforcing compliance
|
||||
by third parties with this License.</p>
|
||||
<p>An "entity transaction" is a transaction transferring control of an organization,
|
||||
or substantially all assets of one, or subdividing an organization, or
|
||||
merging organizations. If propagation of a covered work results from an
|
||||
entity transaction, each party to that transaction who receives a copy
|
||||
of the work also receives whatever licenses to the work the party's predecessor
|
||||
in interest had or could give under the previous paragraph, plus a right
|
||||
to possession of the Corresponding Source of the work from the predecessor
|
||||
in interest, if the predecessor has it or can get it with reasonable efforts.</p>
|
||||
<p>You may not impose any further restrictions on the exercise of the rights
|
||||
granted or affirmed under this License. For example, you may not impose
|
||||
a license fee, royalty, or other charge for exercise of rights granted
|
||||
under this License, and you may not initiate litigation (including a cross-claim
|
||||
or counterclaim in a lawsuit) alleging that any patent claim is infringed
|
||||
by making, using, selling, offering for sale, or importing the Program
|
||||
or any portion of it.</p>
|
||||
<h3>11. Patents.</h3>
|
||||
<p>A "contributor" is a copyright holder who authorizes use under this License
|
||||
of the Program or a work on which the Program is based. The work thus licensed
|
||||
is called the contributor's "contributor version".</p>
|
||||
<p>A contributor's "essential patent claims" are all patent claims owned
|
||||
or controlled by the contributor, whether already acquired or hereafter
|
||||
acquired, that would be infringed by some manner, permitted by this License,
|
||||
of making, using, or selling its contributor version, but do not include
|
||||
claims that would be infringed only as a consequence of further modification
|
||||
of the contributor version. For purposes of this definition, "control"
|
||||
includes the right to grant patent sublicenses in a manner consistent with
|
||||
the requirements of this License.</p>
|
||||
<p>Each contributor grants you a non-exclusive, worldwide, royalty-free patent
|
||||
license under the contributor's essential patent claims, to make, use,
|
||||
sell, offer for sale, import and otherwise run, modify and propagate the
|
||||
contents of its contributor version.</p>
|
||||
<p>In the following three paragraphs, a "patent license" is any express agreement
|
||||
or commitment, however denominated, not to enforce a patent (such as an
|
||||
express permission to practice a patent or covenant not to sue for patent
|
||||
infringement). To "grant" such a patent license to a party means to make
|
||||
such an agreement or commitment not to enforce a patent against the party.</p>
|
||||
<p>If you convey a covered work, knowingly relying on a patent license, and
|
||||
the Corresponding Source of the work is not available for anyone to copy,
|
||||
free of charge and under the terms of this License, through a publicly
|
||||
available network server or other readily accessible means, then you must
|
||||
either (1) cause the Corresponding Source to be so available, or (2) arrange
|
||||
to deprive yourself of the benefit of the patent license for this particular
|
||||
work, or (3) arrange, in a manner consistent with the requirements of this
|
||||
License, to extend the patent license to downstream recipients. "Knowingly
|
||||
relying" means you have actual knowledge that, but for the patent license,
|
||||
your conveying the covered work in a country, or your recipient's use of
|
||||
the covered work in a country, would infringe one or more identifiable
|
||||
patents in that country that you have reason to believe are valid.</p>
|
||||
<p>If, pursuant to or in connection with a single transaction or arrangement,
|
||||
you convey, or propagate by procuring conveyance of, a covered work, and
|
||||
grant a patent license to some of the parties receiving the covered work
|
||||
authorizing them to use, propagate, modify or convey a specific copy of
|
||||
the covered work, then the patent license you grant is automatically extended
|
||||
to all recipients of the covered work and works based on it.</p>
|
||||
<p>A patent license is "discriminatory" if it does not include within the
|
||||
scope of its coverage, prohibits the exercise of, or is conditioned on
|
||||
the non-exercise of one or more of the rights that are specifically granted
|
||||
under this License. You may not convey a covered work if you are a party
|
||||
to an arrangement with a third party that is in the business of distributing
|
||||
software, under which you make payment to the third party based on the
|
||||
extent of your activity of conveying the work, and under which the third
|
||||
party grants, to any of the parties who would receive the covered work
|
||||
from you, a discriminatory patent license (a) in connection with copies
|
||||
of the covered work conveyed by you (or copies made from those copies),
|
||||
or (b) primarily for and in connection with specific products or compilations
|
||||
that contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.</p>
|
||||
<p>Nothing in this License shall be construed as excluding or limiting any
|
||||
implied license or other defenses to infringement that may otherwise be
|
||||
available to you under applicable patent law.</p>
|
||||
<h3>12. No Surrender of Others' Freedom.</h3>
|
||||
<p>If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not convey it at all. For example, if you agree to terms that obligate
|
||||
you to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this License
|
||||
would be to refrain entirely from conveying the Program.</p>
|
||||
<h3>13. Remote Network Interaction; Use with the GNU General Public License.</h3>
|
||||
<p>Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users interacting
|
||||
with it remotely through a computer network (if your version supports such
|
||||
interaction) an opportunity to receive the Corresponding Source of your
|
||||
version by providing access to the Corresponding Source from a network
|
||||
server at no charge, through some standard or customary means of facilitating
|
||||
copying of software. This Corresponding Source shall include the Corresponding
|
||||
Source for any work covered by version 3 of the GNU General Public License
|
||||
that is incorporated pursuant to the following paragraph.</p>
|
||||
<p>Notwithstanding any other provision of this License, you have permission
|
||||
to link or combine any covered work with a work licensed under version
|
||||
3 of the GNU General Public License into a single combined work, and to
|
||||
convey the resulting work. The terms of this License will continue to apply
|
||||
to the part which is the covered work, but the work with which it is combined
|
||||
will remain governed by version 3 of the GNU General Public License.</p>
|
||||
<h3>14. Revised Versions of this License.</h3>
|
||||
<p>The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail
|
||||
to address new problems or concerns.</p>
|
||||
<p>Each version is given a distinguishing version number. If the Program
|
||||
specifies that a certain numbered version of the GNU Affero General Public
|
||||
License "or any later version" applies to it, you have the option of following
|
||||
the terms and conditions either of that numbered version or of any later
|
||||
version published by the Free Software Foundation. If the Program does
|
||||
not specify a version number of the GNU Affero General Public License,
|
||||
you may choose any version ever published by the Free Software Foundation.</p>
|
||||
<p>If the Program specifies that a proxy can decide which future versions
|
||||
of the GNU Affero General Public License can be used, that proxy's public
|
||||
statement of acceptance of a version permanently authorizes you to choose
|
||||
that version for the Program.</p>
|
||||
<p>Later license versions may give you additional or different permissions.
|
||||
However, no additional obligations are imposed on any author or copyright
|
||||
holder as a result of your choosing to follow a later version.</p>
|
||||
<h3>15. Disclaimer of Warranty.</h3>
|
||||
<p>THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
|
||||
LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
||||
EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
|
||||
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.
|
||||
SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY
|
||||
SERVICING, REPAIR OR CORRECTION.</p>
|
||||
<h3>16. Limitation of Liability.</h3>
|
||||
<p>IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
|
||||
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
|
||||
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS
|
||||
OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR
|
||||
THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY
|
||||
OF SUCH DAMAGES.</p>
|
||||
<h3>17. Interpretation of Sections 15 and 16.</h3>
|
||||
<p>If the disclaimer of warranty and limitation of liability provided above
|
||||
cannot be given local legal effect according to their terms, reviewing
|
||||
courts shall apply local law that most closely approximates an absolute
|
||||
waiver of all civil liability in connection with the Program, unless a
|
||||
warranty or assumption of liability accompanies a copy of the Program in
|
||||
return for a fee.</p>
|
||||
<p>END OF TERMS AND CONDITIONS</p>
|
||||
<h2>How to Apply These Terms to Your New Programs</h2>
|
||||
<p>If you develop a new program, and you want it to be of the greatest possible
|
||||
use to the public, the best way to achieve this is to make it free software
|
||||
which everyone can redistribute and change under these terms.</p>
|
||||
<p>To do so, attach the following notices to the program. It is safest to
|
||||
attach them to the start of each source file to most effectively state
|
||||
the exclusion of warranty; and each file should have at least the "copyright"
|
||||
line and a pointer to where the full notice is found.</p><pre><code class="language-text-x-trilium-auto"> <one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.</code></pre>
|
||||
<p>Also add information on how to contact you by electronic and paper mail.</p>
|
||||
<p>If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could
|
||||
display a "Source" link that leads users to an archive of the code. There
|
||||
are many ways you could offer source, and different solutions will be better
|
||||
for different programs; see section 13 for the specific requirements.</p>
|
||||
<p>You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL,
|
||||
see <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p>
|
||||
190
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types.html
generated
vendored
190
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types.html
generated
vendored
@@ -9,7 +9,8 @@
|
||||
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>
|
||||
@@ -20,7 +21,8 @@
|
||||
<li>When adding a <a href="#root/_help_QEAPj01N5f7w">link</a> in a <a class="reference-link"
|
||||
href="#root/_help_iPIMuisry3hd">Text</a> 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
|
||||
@@ -30,94 +32,96 @@
|
||||
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>
|
||||
<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 <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.
|
||||
<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 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 <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.
|
||||
<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>
|
||||
37
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html
generated
vendored
37
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html
generated
vendored
@@ -5,7 +5,8 @@
|
||||
create a <em>File</em> note type directly:</p>
|
||||
<ul>
|
||||
<li>Drag a file into the <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>
|
||||
@@ -82,28 +83,30 @@
|
||||
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 <a class="reference-link" href="#root/_help_0ESUbbAxVnoK">Note List</a> based
|
||||
on their type:</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 <a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a> functionality.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Image files can be embedded into text notes like normal images via
|
||||
<a
|
||||
class="reference-link" href="#root/_help_0Ofbk1aSuVRu">Image references</a>.</p>
|
||||
<p>
|
||||
<img class="image_resized" style="aspect-ratio:853/315;width:50%;" src="4_File_image.png"
|
||||
width="853" height="315">
|
||||
</p>
|
||||
</li>
|
||||
<li>Non-image files can be embedded into text notes as read-only widgets via
|
||||
the <a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a> functionality.</li>
|
||||
<li
|
||||
>Image files can be embedded into text notes like normal images via
|
||||
<a
|
||||
class="reference-link" href="#root/_help_0Ofbk1aSuVRu">Image references</a>.</li>
|
||||
</ul>
|
||||
140
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Markdown.html
generated
vendored
Normal file
140
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Markdown.html
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
<p>Trilium has always supported Markdown through its <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/mHbBMPDPkVV5/_help_Oau6X9rCuegd">import feature</a>,
|
||||
however the file was either transformed to a <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a> note
|
||||
(converted to Trilium's internal HTML format) or saved as a <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a> 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 <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_SL5f1Auq7sVN">Note types with split view</a> 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
|
||||
otherwise through import/export.</p>
|
||||
<p>Even if Markdown is now specially treated by having a preview mechanism,
|
||||
Trilium remains at its core a WYSWYG editor so Markdown will not replace
|
||||
text notes.</p>
|
||||
<aside class="admonition note">
|
||||
<p>Feature requests regarding the Markdown implementation will be considered,
|
||||
but if they are outside the realm of Trilium they will not be implemented.
|
||||
One of the core aspects of the Markdown integration is that it reuses components
|
||||
that are already available through other features of the application.</p>
|
||||
</aside>
|
||||
<h2>Features</h2>
|
||||
<h3>Source view pane</h3>
|
||||
<ul>
|
||||
<li>Syntax highlighting for the Markdown syntax.</li>
|
||||
<li>Nested syntax highlighting for code inside code blocks.</li>
|
||||
<li>When editing larger documents, the preview scrolls along with the source
|
||||
editor.</li>
|
||||
</ul>
|
||||
<h3>Preview pane</h3>
|
||||
<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 & 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> using
|
||||
<code
|
||||
spellcheck="false">```mermaid</code>
|
||||
</li>
|
||||
<li>
|
||||
<p><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/_help_nBAXQFj20hS1">Include Note</a> (no
|
||||
builtin Markdown syntax, but HTML syntax works just fine):</p><pre><code class="language-text-x-trilium-auto"><section class="include-note" data-note-id="vJDjQm0VK8Na" data-box-size="expandable">
|
||||
&nbsp;
|
||||
</section>n</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/QEAPj01N5f7w/_help_hrZ1D00cLbal">Internal (reference) links</a> via
|
||||
its HTML syntax, or through a <em>Wikilinks</em>-like format (only
|
||||
<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>
|
||||
</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 <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_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 <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a> 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 <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a> 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 <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a> note.
|
||||
To avoid that and have it imported as a Markdown note instead:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Right click the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> 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>
|
||||
</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>
|
||||
</ul>
|
||||
<h2>Conversion between text notes and Markdown notes</h2>
|
||||
<p>Currently there is no built-in functionality to convert a <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a> 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>
|
||||
<li>Import the file again, but unchecking <em>Import HTML, Markdown and TXT as text notes if it's unclear from the metadata</em>.</li>
|
||||
</ol>
|
||||
<p>For multiple notes, the process is slightly more involved:</p>
|
||||
<ol>
|
||||
<li>Export the file as Markdown, ZIP.</li>
|
||||
<li>Extract the archive.</li>
|
||||
<li>Remove the <code spellcheck="false">!!!meta.json</code> file.</li>
|
||||
<li>Compress the extracted files back into an archive.</li>
|
||||
<li>Import the newly create archive, but unchecking <em>Import HTML, Markdown and TXT as text notes if it's unclear from the metadata</em>.</li>
|
||||
</ol>
|
||||
<h2>Sync-scrolling & block highlight</h2>
|
||||
<p>When scrolling through the editing pane, the preview pane will attempt
|
||||
to synchronize its position to make it easier to see the preview.</p>
|
||||
<p>In addition, the block in the preview matching the position of the cursor
|
||||
in the source view will appear slightly highlighted.</p>
|
||||
<p>The sync is currently one-way only, scrolling the preview will not synchronize
|
||||
the position of the editor.</p>
|
||||
<p>This feature cannot be disabled as of now; if the scrolling feels distracting,
|
||||
consider temporarily switching to the editor mode and then switching to
|
||||
preview mode when ready.</p>
|
||||
<aside class="admonition note">
|
||||
<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>
|
||||
</aside>
|
||||
@@ -6,12 +6,15 @@
|
||||
<img style="aspect-ratio:886/663;" src="2_Mermaid Diagrams_image.png"
|
||||
width="886" height="663">
|
||||
</figure>
|
||||
|
||||
<h2>Types of diagrams</h2>
|
||||
<p>Trilium supports Mermaid, which adds support for various diagrams such
|
||||
as flowchart, sequence diagram, class diagram, state diagram, pie charts,
|
||||
etc., all using a text description of the chart instead of manually drawing
|
||||
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 <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_SL5f1Auq7sVN">Note types with split view</a> 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
|
||||
diagrams with sample code for each:</p>
|
||||
@@ -49,30 +52,34 @@
|
||||
<img src="1_Mermaid Diagrams_image.png">
|
||||
</li>
|
||||
<li>The preview can be moved around by holding the left mouse button and dragging.</li>
|
||||
<li>Zooming can also be done by using the scroll wheel.</li>
|
||||
<li>The zoom and position on the preview will remain fixed as the diagram
|
||||
changes, to be able to work more easily with large diagrams.</li>
|
||||
</ul>
|
||||
<li
|
||||
>Zooming can also be done by using the scroll wheel.</li>
|
||||
<li>The zoom and position on the preview will remain fixed as the diagram
|
||||
changes, to be able to work more easily with large diagrams.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>The size of the source/preview panes can be adjusted by hovering over
|
||||
the border between them and dragging it with the mouse.</li>
|
||||
<li>In the <a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a> area:
|
||||
<ul>
|
||||
<li>The source/preview can be laid out left-right or bottom-top via the <em>Move editing pane to the left / bottom</em> option.</li>
|
||||
<li>Press <em>Lock editing</em> to automatically mark the note as read-only.
|
||||
<li
|
||||
>Press <em>Lock editing</em> to automatically mark the note as read-only.
|
||||
In this mode, the code pane is hidden and the diagram is displayed full-size.
|
||||
Similarly, press <em>Unlock editing</em> to mark a read-only note as editable.</li>
|
||||
<li>Press the <em>Copy image reference to the clipboard</em> to be able to insert
|
||||
the image representation of the diagram into a text note. See <a class="reference-link"
|
||||
href="#root/_help_0Ofbk1aSuVRu">Image references</a> for more information.</li>
|
||||
<li>Press the <em>Export diagram as SVG</em> to download a scalable/vector rendering
|
||||
of the diagram. Can be used to present the diagram without degrading when
|
||||
zooming.</li>
|
||||
<li
|
||||
>Press the <em>Copy image reference to the clipboard</em> to be able to insert
|
||||
the image representation of the diagram into a text note. See <a class="reference-link"
|
||||
href="#root/_help_0Ofbk1aSuVRu">Image references</a> for more information.</li>
|
||||
<li
|
||||
>Press the <em>Export diagram as SVG</em> to download a scalable/vector rendering
|
||||
of the diagram. Can be used to present the diagram without degrading when
|
||||
zooming.</li>
|
||||
<li>Press the <em>Export diagram as PNG</em> to download a normal image (at
|
||||
1x scale, raster) of the diagram. Can be used to send the diagram in more
|
||||
traditional channels such as e-mail.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Errors in the diagram</h2>
|
||||
<p>If there is an error in the source code, the error will be displayed in
|
||||
|
||||
34
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html
generated
vendored
34
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html
generated
vendored
@@ -13,11 +13,13 @@
|
||||
<ol>
|
||||
<li>HTML language for the legacy/vanilla method, with what needs to be displayed
|
||||
(for example <code spellcheck="false"><p>Hello world.</p></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 <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>
|
||||
@@ -46,10 +48,9 @@ $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>
|
||||
<p>Create a note of type <a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<li>Create a note of type <a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</li>
|
||||
<li
|
||||
>
|
||||
<p>Create a child <a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a> 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() {
|
||||
@@ -59,24 +60,21 @@ $dateEl.text(new Date());</code></pre>
|
||||
</>
|
||||
);
|
||||
}</code></pre>
|
||||
</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>
|
||||
</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>
|
||||
</ol>
|
||||
<h2>Refreshing the note</h2>
|
||||
<p>It's possible to refresh the note via:</p>
|
||||
<ul>
|
||||
<li>The corresponding button in <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>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/_help_R7abl2fc6Mxi">Weight Tracker</a> which
|
||||
is present in the <a class="reference-link" href="#root/_help_6tZeKvSHEUiB">Demo Notes</a>.</li>
|
||||
<li><a class="reference-link" href="#root/_help_R7abl2fc6Mxi">[missing note]</a> which
|
||||
is present in the <a class="reference-link" href="#root/_help_6tZeKvSHEUiB">[missing note]</a>.</li>
|
||||
</ul>
|
||||
@@ -6,7 +6,6 @@ style="width:50%;">
|
||||
<img style="aspect-ratio:812/585;" src="Saved Search_saved-search.gif"
|
||||
width="812" height="585">
|
||||
</figure>
|
||||
|
||||
<h2>Location</h2>
|
||||
<p>By default, saved searches are stored in the day note. However, you can
|
||||
designate a different note to store saved searches by marking it with the
|
||||
|
||||
23
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Spreadsheets.html
generated
vendored
23
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Spreadsheets.html
generated
vendored
@@ -64,15 +64,15 @@
|
||||
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>
|
||||
<ul>
|
||||
<li>
|
||||
<p>It is possible to share a spreadsheet, case in which a best-effort HTML
|
||||
rendering of the spreadsheet is done.</p>
|
||||
<li>It is possible to share a spreadsheet, case in which a best-effort HTML
|
||||
rendering of the spreadsheet is done.
|
||||
<ul>
|
||||
<li>For more advanced use cases, this will most likely not work as intended.
|
||||
Feel free to <a href="#root/_help_wy8So3yZZlH9">report issues</a>, but keep in
|
||||
@@ -80,12 +80,9 @@
|
||||
the features of Univer.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>There is currently no export functionality, as stated previously.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>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.</p>
|
||||
</li>
|
||||
<li>There is currently no export functionality, as stated previously.</li>
|
||||
<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>
|
||||
334
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html
generated
vendored
334
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html
generated
vendored
@@ -20,169 +20,171 @@
|
||||
<p>Fore more information see <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>
|
||||
<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 & 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 & 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 <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
|
||||
<a
|
||||
class="reference-link" href="#root/_help_A9Oc6YKKc65v">Keyboard Shortcuts</a>. In addition, see <a class="reference-link"
|
||||
href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting</a> 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 <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>
|
||||
<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 & 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 & 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 <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
|
||||
<a
|
||||
class="reference-link" href="#root/_help_A9Oc6YKKc65v">Keyboard Shortcuts</a>. In addition, see <a class="reference-link"
|
||||
href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting</a> 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 <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>
|
||||
@@ -21,7 +21,6 @@
|
||||
insert it.</p>
|
||||
<img src="1_Insert buttons_plus.png"
|
||||
width="272" height="187">
|
||||
|
||||
<h2>Symbols</h2>
|
||||
<figure class="image image-style-align-right">
|
||||
<img style="aspect-ratio:346/322;" src="1_Insert buttons_image.png"
|
||||
@@ -53,7 +52,6 @@ width="272" height="187">
|
||||
<img style="aspect-ratio:1174/358;" src="6_Insert buttons_image.png"
|
||||
width="1174" height="358">
|
||||
</figure>
|
||||
|
||||
<h2>Horizontal ruler</h2>
|
||||
<p>This feature will display a horizontal line, generally useful to separate
|
||||
different sections of the text. To do so, press the
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
<img
|
||||
src="10_Tables_image.png" width="384"
|
||||
height="100">
|
||||
|
||||
<h2>Navigating a table</h2>
|
||||
<h2>Navigating a table</h2>
|
||||
<ul>
|
||||
<li>Using the mouse:
|
||||
<ul>
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Entity events</h2>
|
||||
<p>Other events are bound to some entity, these are defined as <a href="#root/_help_zEY4DaJG4YT5">relations</a> -
|
||||
meaning that script is triggered only if note has this script attached
|
||||
|
||||
@@ -81,8 +81,8 @@ module.exports = new WordCountWidget();</code></pre>
|
||||
<p>After you make changes it is necessary to <a href="#root/_help_s8alTXmpFR61">restart Trilium</a> so
|
||||
that the layout can be rebuilt.</p>
|
||||
<p>The widget only activates on text notes that have the <code spellcheck="false">#wordCount</code> label.
|
||||
This label can be a <a href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/QEAPj01N5f7w/_help_hrZ1D00cLbal">reference link</a> to
|
||||
enable the widget for an entire subtree.</p>
|
||||
This label can be a <a href="#root/_help_hrZ1D00cLbal">reference link</a> to enable
|
||||
the widget for an entire subtree.</p>
|
||||
<p>At the bottom of the note you can see the resulting widget:</p>
|
||||
<figure
|
||||
class="image">
|
||||
|
||||
@@ -455,5 +455,11 @@
|
||||
"fulltext-after-expression": "“{{- token}}”不是有效表达式。要搜索文本,请将其放在属性筛选器之前(例如,“{{- token}} #label”而不是“#label {{- token}}”)。",
|
||||
"unrecognized-expression": "无法识别的表达式“{{- token}}”"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"incorrect": "您输入的密码不正确。"
|
||||
},
|
||||
"script": {
|
||||
"wrong-environment": "无法执行笔记“{{-noteTitle}}”({{-noteId}})。这是一个 {{-actualEnv}} 脚本,但尝试在 {{-expectedEnv}} 中执行。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +397,8 @@
|
||||
"migration": {
|
||||
"old_version": "Direct migration from your current version is not supported. Please upgrade to the latest v0.60.4 first and only then to this version.",
|
||||
"error_message": "Error during migration to version {{version}}: {{stack}}",
|
||||
"wrong_db_version": "The version of the database ({{version}}) is newer than what the application expects ({{targetVersion}}), which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue."
|
||||
"wrong_db_version": "The version of the database ({{version}}) is newer than what the application expects ({{targetVersion}}), which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue.",
|
||||
"invalid_db_version": "The database version is not a valid number. This usually indicates a corrupted 'dbVersion' option in the database. Please restore from a backup."
|
||||
},
|
||||
"modals": {
|
||||
"error_title": "Error"
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { ADMONITION_TYPE_MAPPINGS } from "@triliumnext/commons";
|
||||
import { gfm } from "@triliumnext/turndown-plugin-gfm";
|
||||
import Turnish, { type Rule } from "turnish";
|
||||
|
||||
let instance: Turnish | null = null;
|
||||
|
||||
// TODO: Move this to a dedicated file someday.
|
||||
export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
|
||||
note: "NOTE",
|
||||
tip: "TIP",
|
||||
important: "IMPORTANT",
|
||||
caution: "CAUTION",
|
||||
warning: "WARNING"
|
||||
};
|
||||
export { ADMONITION_TYPE_MAPPINGS };
|
||||
|
||||
export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note;
|
||||
|
||||
|
||||
@@ -14,4 +14,16 @@ describe("Note type mappings", () => {
|
||||
mime: "text/vnd.mermaid"
|
||||
});
|
||||
});
|
||||
|
||||
it("exports markdown code notes with a .md extension", () => {
|
||||
// `mime-types` doesn't recognize Trilium's custom `text/x-markdown`;
|
||||
// without the explicit fallback this was exporting as `.code`.
|
||||
for (const mime of [ "text/x-markdown", "text/markdown", "text/x-gfm" ]) {
|
||||
const note = buildNote({ type: "code", mime, title: "Doc" });
|
||||
expect(mapByNoteType(note, "# hi", "markdown")).toMatchObject({
|
||||
extension: "md",
|
||||
mime
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,21 @@ function exportSingleNote(taskContext: TaskContext<"export">, branch: BBranch, f
|
||||
taskContext.taskSucceeded(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension fallback for MIME types the `mime-types` package doesn't recognize —
|
||||
* mostly Trilium's `text/x-` custom MIMEs.
|
||||
*/
|
||||
export function mapCodeMimeToExtension(mime: string): string | null {
|
||||
switch (mime) {
|
||||
case "text/x-markdown":
|
||||
case "text/markdown":
|
||||
case "text/x-gfm":
|
||||
return "md";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function mapByNoteType(note: BNote, content: string | Buffer<ArrayBufferLike>, format: ExportFormat) {
|
||||
let payload, extension, mime;
|
||||
|
||||
@@ -60,7 +75,11 @@ export function mapByNoteType(note: BNote, content: string | Buffer<ArrayBufferL
|
||||
}
|
||||
} else if (note.type === "code") {
|
||||
payload = content;
|
||||
extension = mimeTypes.extension(note.mime) || "code";
|
||||
// Our own map wins over the `mime-types` lookup so markdown MIMEs get
|
||||
// the conventional `.md` rather than `.mkd` (what the lib returns for
|
||||
// `text/x-markdown`) or `.code` (what the fallback produced when the
|
||||
// lib didn't recognize `text/markdown` at all).
|
||||
extension = mapCodeMimeToExtension(note.mime) || mimeTypes.extension(note.mime) || "code";
|
||||
mime = note.mime;
|
||||
} else if (note.type === "canvas") {
|
||||
payload = content;
|
||||
|
||||
@@ -5,6 +5,7 @@ import mimeTypes from "mime-types";
|
||||
import type BBranch from "../../../becca/entities/bbranch.js";
|
||||
import type BNote from "../../../becca/entities/bnote.js";
|
||||
import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js";
|
||||
import { mapCodeMimeToExtension } from "../single.js";
|
||||
|
||||
type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
|
||||
|
||||
@@ -84,7 +85,7 @@ export abstract class ZipExportProvider {
|
||||
} else if (mime?.toLowerCase()?.trim() === "text/mermaid") {
|
||||
return "txt";
|
||||
}
|
||||
return mimeTypes.extension(mime) || "dat";
|
||||
return mapCodeMimeToExtension(mime) || mimeTypes.extension(mime) || "dat";
|
||||
|
||||
|
||||
}
|
||||
|
||||
90
apps/server/src/services/import/enex.spec.ts
Normal file
90
apps/server/src/services/import/enex.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import fs from "fs";
|
||||
import { default as path, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import cls from "../cls.js";
|
||||
import sql_init from "../sql_init.js";
|
||||
import TaskContext from "../task_context.js";
|
||||
import enex from "./enex.js";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function testImport(fileName: string) {
|
||||
const sample = fs.readFileSync(path.join(scriptDir, "samples", fileName));
|
||||
const taskContext = TaskContext.getInstance("import-enex", "importNotes", {});
|
||||
|
||||
return new Promise<{ importedNote: BNote; rootNote: BNote }>((resolve, reject) => {
|
||||
cls.init(async () => {
|
||||
const rootNote = becca.getNote("root");
|
||||
if (!rootNote) {
|
||||
expect(rootNote).toBeTruthy();
|
||||
return;
|
||||
}
|
||||
|
||||
const importedNote = await enex.importEnex(taskContext, {
|
||||
originalname: fileName,
|
||||
mimetype: "application/enex+xml",
|
||||
buffer: sample
|
||||
}, rootNote as BNote);
|
||||
resolve({
|
||||
importedNote,
|
||||
rootNote
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("importEnex", () => {
|
||||
beforeAll(async () => {
|
||||
sql_init.initializeDb();
|
||||
await sql_init.dbReady;
|
||||
});
|
||||
|
||||
it("imports non-image resources as attachments instead of child notes", async () => {
|
||||
const { importedNote } = await testImport("File with attachments.enex");
|
||||
|
||||
// The root import note should contain the individual notes as children
|
||||
const test1 = importedNote.getChildNotes().find(n => n.title === "TEST1");
|
||||
expect(test1).toBeTruthy();
|
||||
|
||||
// Non-image resources should be attachments, not child notes
|
||||
const childNotes = test1!.getChildNotes();
|
||||
expect(childNotes).toHaveLength(0);
|
||||
|
||||
// Should have two file attachments
|
||||
const attachments = test1!.getAttachmentsByRole("file");
|
||||
expect(attachments).toHaveLength(2);
|
||||
|
||||
const txt = attachments.find(a => a.title === "attachments1.txt");
|
||||
expect(txt).toBeTruthy();
|
||||
expect(txt!.mime).toBe("text/plain");
|
||||
expect(txt!.getContent().toString()).toBe("111");
|
||||
|
||||
const bin = attachments.find(a => a.title === "attachments2");
|
||||
expect(bin).toBeTruthy();
|
||||
expect(bin!.mime).toBe("application/octet-stream");
|
||||
expect(bin!.getContent().toString()).toBe("222");
|
||||
|
||||
// The note content should contain reference links to the attachments
|
||||
const content = test1!.getContent().toString();
|
||||
expect(content).toContain(`class="reference-link" href="#root/${test1!.noteId}?viewMode=attachments&attachmentId=${txt!.attachmentId}"`);
|
||||
expect(content).toContain(`class="reference-link" href="#root/${test1!.noteId}?viewMode=attachments&attachmentId=${bin!.attachmentId}"`);
|
||||
});
|
||||
|
||||
it("imports notes without attachments normally", async () => {
|
||||
const { importedNote } = await testImport("File with attachments.enex");
|
||||
|
||||
const test2 = importedNote.getChildNotes().find(n => n.title === "TEST2");
|
||||
expect(test2).toBeTruthy();
|
||||
expect(test2!.getChildNotes()).toHaveLength(0);
|
||||
expect(test2!.getAttachmentsByRole("file")).toHaveLength(0);
|
||||
|
||||
const test3 = importedNote.getChildNotes().find(n => n.title === "TEST3");
|
||||
expect(test3).toBeTruthy();
|
||||
expect(test3!.getChildNotes()).toHaveLength(0);
|
||||
expect(test3!.getAttachmentsByRole("file")).toHaveLength(0);
|
||||
});
|
||||
}, 60_000);
|
||||
@@ -1,20 +1,21 @@
|
||||
import type { AttributeType } from "@triliumnext/commons";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import sax from "sax";
|
||||
import stream from "stream";
|
||||
import { Throttle } from "stream-throttle";
|
||||
import log from "../log.js";
|
||||
import { md5, escapeHtml, fromBase64 } from "../utils.js";
|
||||
import date_utils from "../date_utils.js";
|
||||
import sql from "../sql.js";
|
||||
import noteService from "../notes.js";
|
||||
import imageService from "../image.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import sanitizeAttributeName from "../sanitize_attribute_name.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import date_utils from "../date_utils.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import imageService from "../image.js";
|
||||
import log from "../log.js";
|
||||
import noteService from "../notes.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import sanitizeAttributeName from "../sanitize_attribute_name.js";
|
||||
import sql from "../sql.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import { escapeHtml, fromBase64,md5 } from "../utils.js";
|
||||
import type { File } from "./common.js";
|
||||
import type { AttributeType } from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* date format is e.g. 20181121T193703Z or 2013-04-14T16:19:00.000Z (Mac evernote, see #3496)
|
||||
@@ -25,7 +26,7 @@ function parseDate(text: string) {
|
||||
text = text.replace(/[-:]/g, "");
|
||||
|
||||
// insert - and : to convert it to trilium format
|
||||
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2) + " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
|
||||
text = `${text.substr(0, 4) }-${ text.substr(4, 2) }-${ text.substr(6, 2) } ${ text.substr(9, 2) }:${ text.substr(11, 2) }:${ text.substr(13, 2) }.000Z`;
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -318,27 +319,17 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
|
||||
|
||||
resource.mime = resource.mime || "application/octet-stream";
|
||||
|
||||
const createFileNote = () => {
|
||||
const resourceNote = noteService.createNewNote({
|
||||
parentNoteId: noteEntity.noteId,
|
||||
const createFileAttachment = () => {
|
||||
const attachment = noteEntity.saveAttachment({
|
||||
role: "file",
|
||||
mime: resource.mime || "application/octet-stream",
|
||||
title: resource.title,
|
||||
content: resource.content ?? "",
|
||||
type: "file",
|
||||
mime: resource.mime,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
}).note;
|
||||
content: resource.content ?? ""
|
||||
});
|
||||
|
||||
for (const attr of resource.attributes) {
|
||||
resourceNote.addAttribute(attr.type, attr.name, attr.value);
|
||||
}
|
||||
const attachmentLink = `<a class="reference-link" href="#root/${noteEntity.noteId}?viewMode=attachments&attachmentId=${attachment.attachmentId}">${escapeHtml(resource.title)}</a>`;
|
||||
|
||||
updateDates(resourceNote, utcDateCreated, utcDateModified);
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
const resourceLink = `<a href="#root/${resourceNote.noteId}">${escapeHtml(resource.title)}</a>`;
|
||||
|
||||
content = (content || "").replace(mediaRegex, resourceLink);
|
||||
content = (content || "").replace(mediaRegex, attachmentLink);
|
||||
};
|
||||
|
||||
if (resource.mime && resource.mime.startsWith("image/")) {
|
||||
@@ -360,10 +351,10 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.error(`error when saving image from ENEX file: ${e.message}`);
|
||||
createFileNote();
|
||||
createFileAttachment();
|
||||
}
|
||||
} else {
|
||||
createFileNote();
|
||||
createFileAttachment();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,233 +1,11 @@
|
||||
import { renderToHtml as renderToHtmlShared } from "@triliumnext/commons";
|
||||
|
||||
|
||||
import { getMimeTypeFromMarkdownName, MIME_TYPE_AUTO, normalizeMimeTypeForCKEditor, transclusionExtension, wikiLinkExtension } from "@triliumnext/commons";
|
||||
import { parse, Renderer, type Tokens, use } from "marked";
|
||||
|
||||
import { ADMONITION_TYPE_MAPPINGS } from "../export/markdown.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import utils from "../utils.js";
|
||||
import importUtils from "./utils.js";
|
||||
|
||||
const escape = utils.escapeHtml;
|
||||
|
||||
/**
|
||||
* Keep renderer code up to date with https://github.com/markedjs/marked/blob/master/src/Renderer.ts.
|
||||
*/
|
||||
class CustomMarkdownRenderer extends Renderer {
|
||||
|
||||
override heading(data: Tokens.Heading): string {
|
||||
// Treat h1 as raw text.
|
||||
if (data.depth === 1) {
|
||||
return `<h1>${data.text}</h1>`;
|
||||
}
|
||||
|
||||
return super.heading(data).trimEnd();
|
||||
}
|
||||
|
||||
override paragraph(data: Tokens.Paragraph): string {
|
||||
return super.paragraph(data).trimEnd();
|
||||
}
|
||||
|
||||
override code({ text, lang }: Tokens.Code): string {
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Escape the HTML.
|
||||
text = escape(text);
|
||||
|
||||
// Unescape "
|
||||
text = text.replace(/"/g, '"');
|
||||
|
||||
const ckEditorLanguage = getNormalizedMimeFromMarkdownLanguage(lang);
|
||||
return `<pre><code class="language-${ckEditorLanguage}">${text}</code></pre>`;
|
||||
}
|
||||
|
||||
override list(token: Tokens.List): string {
|
||||
let result = super.list(token)
|
||||
.replace("\n", "") // we replace the first one only.
|
||||
.trimEnd();
|
||||
|
||||
// Handle todo-list in the CKEditor format.
|
||||
if (token.items.some(item => item.task)) {
|
||||
result = result.replace(/^<ul>/, "<ul class=\"todo-list\">");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
override checkbox({ checked }: Tokens.Checkbox): string {
|
||||
return `<input type="checkbox"${
|
||||
checked ? 'checked="checked" ' : ''
|
||||
}disabled="disabled">`;
|
||||
}
|
||||
|
||||
override listitem(item: Tokens.ListItem): string {
|
||||
// Handle todo-list in the CKEditor format.
|
||||
if (item.task) {
|
||||
let itemBody = '';
|
||||
const checkbox = this.checkbox({ checked: !!item.checked, raw: "- [ ]", type: "checkbox" });
|
||||
if (item.loose) {
|
||||
if (item.tokens[0]?.type === 'paragraph') {
|
||||
item.tokens[0].text = checkbox + item.tokens[0].text;
|
||||
if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') {
|
||||
item.tokens[0].tokens[0].text = checkbox + escape(item.tokens[0].tokens[0].text);
|
||||
item.tokens[0].tokens[0].escaped = true;
|
||||
}
|
||||
} else {
|
||||
item.tokens.unshift({
|
||||
type: 'text',
|
||||
raw: checkbox,
|
||||
text: checkbox,
|
||||
escaped: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
itemBody += checkbox;
|
||||
}
|
||||
|
||||
itemBody += `<span class="todo-list__label__description">${this.parser.parse(item.tokens.filter(t => t.type !== "checkbox"))}</span>`;
|
||||
return `<li><label class="todo-list__label">${itemBody}</label></li>`;
|
||||
}
|
||||
|
||||
return super.listitem(item).trimEnd();
|
||||
}
|
||||
|
||||
override image(token: Tokens.Image): string {
|
||||
return super.image(token)
|
||||
.replace(` alt=""`, "");
|
||||
}
|
||||
|
||||
override blockquote({ tokens }: Tokens.Blockquote): string {
|
||||
const body = renderer.parser.parse(tokens);
|
||||
|
||||
const admonitionMatch = /^<p>\[\!([A-Z]+)\]/.exec(body);
|
||||
if (Array.isArray(admonitionMatch) && admonitionMatch.length === 2) {
|
||||
const type = admonitionMatch[1].toLowerCase();
|
||||
|
||||
if (ADMONITION_TYPE_MAPPINGS[type]) {
|
||||
const bodyWithoutHeader = body
|
||||
.replace(/^<p>\[\!([A-Z]+)\]\s*/, "<p>")
|
||||
.replace(/^<p><\/p>/, ""); // Having a heading will generate an empty paragraph that we need to remove.
|
||||
|
||||
return `<aside class="admonition ${type}">${bodyWithoutHeader.trim()}</aside>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `<blockquote>${body}</blockquote>`;
|
||||
}
|
||||
|
||||
codespan({ text }: Tokens.Codespan): string {
|
||||
return `<code spellcheck="false">${escape(text)}</code>`;
|
||||
}
|
||||
|
||||
function renderToHtml(content: string, title: string): string {
|
||||
return renderToHtmlShared(content, title, { sanitize: htmlSanitizer.sanitize });
|
||||
}
|
||||
|
||||
function renderToHtml(content: string, title: string) {
|
||||
// Double-escape slashes in math expression because they are otherwise consumed by the parser somewhere.
|
||||
content = content.replaceAll("\\$", "\\\\$");
|
||||
|
||||
// Extract formulas and replace them with placeholders to prevent interference from Markdown rendering
|
||||
const { processedText, placeholderMap: formulaMap } = extractFormulas(content);
|
||||
|
||||
use({
|
||||
// Order is important, especially for wikilinks.
|
||||
extensions: [
|
||||
transclusionExtension,
|
||||
wikiLinkExtension
|
||||
]
|
||||
});
|
||||
|
||||
let html = parse(processedText, {
|
||||
async: false,
|
||||
renderer
|
||||
}) as string;
|
||||
|
||||
// After rendering, replace placeholders back with the formula HTML
|
||||
html = restoreFromMap(html, formulaMap);
|
||||
|
||||
// h1 handling needs to come before sanitization
|
||||
html = importUtils.handleH1(html, title);
|
||||
html = htmlSanitizer.sanitize(html);
|
||||
|
||||
// Add a trailing semicolon to CSS styles.
|
||||
html = html.replaceAll(/(<(img|figure|col).*?style=".*?)"/g, "$1;\"");
|
||||
|
||||
// Remove slash for self-closing tags to match CKEditor's approach.
|
||||
html = html.replace(/<(\w+)([^>]*)\s+\/>/g, "<$1$2>");
|
||||
|
||||
// Normalize non-breaking spaces to entity.
|
||||
html = html.replaceAll("\u00a0", " ");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function getNormalizedMimeFromMarkdownLanguage(language: string | undefined) {
|
||||
if (language) {
|
||||
const mimeDefinition = getMimeTypeFromMarkdownName(language);
|
||||
if (mimeDefinition) {
|
||||
return normalizeMimeTypeForCKEditor(mimeDefinition.mime);
|
||||
}
|
||||
}
|
||||
|
||||
return MIME_TYPE_AUTO;
|
||||
}
|
||||
|
||||
function extractCodeBlocks(text: string): { processedText: string, placeholderMap: Map<string, string> } {
|
||||
const codeMap = new Map<string, string>();
|
||||
let id = 0;
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Multi-line code block and Inline code
|
||||
text = text.replace(/```[\s\S]*?```/g, (m) => {
|
||||
const key = `<!--CODE_BLOCK_${timestamp}_${id++}-->`;
|
||||
codeMap.set(key, m);
|
||||
return key;
|
||||
}).replace(/`[^`\n]+`/g, (m) => {
|
||||
const key = `<!--INLINE_CODE_${timestamp}_${id++}-->`;
|
||||
codeMap.set(key, m);
|
||||
return key;
|
||||
});
|
||||
|
||||
return { processedText: text, placeholderMap: codeMap };
|
||||
}
|
||||
|
||||
function extractFormulas(text: string): { processedText: string, placeholderMap: Map<string, string> } {
|
||||
// Protect the $ signs inside code blocks from being recognized as formulas.
|
||||
const { processedText: noCodeText, placeholderMap: codeMap } = extractCodeBlocks(text);
|
||||
|
||||
const formulaMap = new Map<string, string>();
|
||||
let id = 0;
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Display math and Inline math
|
||||
let processedText = noCodeText.replace(/(?<!\\)\$\$((?:(?!\n{2,})[\s\S])+?)\$\$/g, (_, formula) => {
|
||||
const key = `<!--FORMULA_BLOCK_${timestamp}_${id++}-->`;
|
||||
const rendered = `<span class="math-tex">\\[${formula}\\]</span>`;
|
||||
formulaMap.set(key, rendered);
|
||||
return key;
|
||||
}).replace(/(?<!\\)\$(.+?)\$/g, (_, formula) => {
|
||||
const key = `<!--FORMULA_INLINE_${timestamp}_${id++}-->`;
|
||||
const rendered = `<span class="math-tex">\\(${formula}\\)</span>`;
|
||||
formulaMap.set(key, rendered);
|
||||
return key;
|
||||
});
|
||||
|
||||
processedText = restoreFromMap(processedText, codeMap);
|
||||
|
||||
return { processedText, placeholderMap: formulaMap };
|
||||
}
|
||||
|
||||
function restoreFromMap(text: string, map: Map<string, string>): string {
|
||||
if (map.size === 0) return text;
|
||||
const pattern = [...map.keys()]
|
||||
.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
.join('|');
|
||||
return text.replace(new RegExp(pattern, 'g'), match => map.get(match) ?? match);
|
||||
}
|
||||
|
||||
const renderer = new CustomMarkdownRenderer({ async: false });
|
||||
|
||||
export default {
|
||||
renderToHtml
|
||||
};
|
||||
|
||||
@@ -123,6 +123,16 @@ describe("#getType", () => {
|
||||
[{textImportedAsText: false}, "text/x-markdown"], "file"
|
||||
],
|
||||
|
||||
[
|
||||
"w/ codeImportedAsCode: true and 'text/markdown' mime type (override) – it should return 'code'",
|
||||
[{codeImportedAsCode: true}, "text/markdown"], "code"
|
||||
],
|
||||
|
||||
[
|
||||
"w/ codeImportedAsCode: true and 'application/javascript' mime type (override) – it should return 'code'",
|
||||
[{codeImportedAsCode: true}, "application/javascript"], "code"
|
||||
],
|
||||
|
||||
[
|
||||
"w/ textImportedAsText: false and 'text/html' mime type – it should return 'file'",
|
||||
[{textImportedAsText: false}, "text/html"], "file"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user