diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index 3ca6a2a792..680e982678 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -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) { diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index a30a83975a..b371372d9c 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -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"); } diff --git a/apps/client/src/menus/context_menu.ts b/apps/client/src/menus/context_menu.ts index 415c0a2c6a..130b4cfd52 100644 --- a/apps/client/src/menus/context_menu.ts +++ b/apps/client/src/menus/context_menu.ts @@ -39,6 +39,7 @@ export interface MenuCommandItem { title: string; command?: T; type?: string; + mime?: string; /** * The icon to display in the menu item. * diff --git a/apps/client/src/menus/tree_context_menu.ts b/apps/client/src/menus/tree_context_menu.ts index 8dca18d905..7fc1a8b008 100644 --- a/apps/client/src/menus/tree_context_menu.ts +++ b/apps/client/src/menus/tree_context_menu.ts @@ -288,7 +288,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener row !== null) as MenuItem[]; } - async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem) { + async selectMenuItemHandler({ command, type, mime, templateNoteId }: MenuCommandItem) { const notePath = treeService.getNotePath(this.node); if (utils.isMobile()) { @@ -305,6 +305,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener, 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($('
').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/")) { diff --git a/apps/client/src/services/content_renderer_text.ts b/apps/client/src/services/content_renderer_text.ts index 9317d0b6e0..1684ce766c 100644 --- a/apps/client/src/services/content_renderer_text.ts +++ b/apps/client/src/services/content_renderer_text.ts @@ -15,37 +15,47 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon if (blob && !isHtmlEmpty(blob.content)) { $renderedContent.append($('
').html(blob.content)); - - const seenNoteIds = options.seenNoteIds ?? new Set(); - 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("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 `
`, 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, options: RenderOptions = {}) { + const seenNoteIds = options.seenNoteIds ?? new Set(); + 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("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) { // 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>(); + +/** + * 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(); + export async function applyInlineMermaid(container: HTMLDivElement) { - // Initialize mermaid + const nodes = Array.from(container.querySelectorAll("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(); + 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("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, note: FNote, includeArchivedNotes: boolean) { +export async function renderChildrenList($renderedContent: JQuery, note: FNote, includeArchivedNotes: boolean) { let childNoteIds = note.getChildNoteIds(); if (!childNoteIds.length) { diff --git a/apps/client/src/services/note_types.ts b/apps/client/src/services/note_types.ts index c99f04ae81..e12e3c5440 100644 --- a/apps/client/src/services/note_types.ts +++ b/apps/client/src/services/note_types.ts @@ -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 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 && ; } +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 ( + + {buttons.map(({ value, icon, text }) => ( + setDisplayMode(value)} + /> + ))} + + ); +} + function EditButton({ note, noteContext }: FloatingButtonContext) { const [animationClass, setAnimationClass] = useState(""); const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext); diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index e734bbf9c7..b5f94b75fa 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -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"; diff --git a/apps/client/src/widgets/find_in_text.ts b/apps/client/src/widgets/find_in_text.ts index e97a7a8dff..049dd9f215 100644 --- a/apps/client/src/widgets/find_in_text.ts +++ b/apps/client/src/widgets/find_in_text.ts @@ -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; } } diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index f6e5a51556..d4e2602f9e 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -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); } diff --git a/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx b/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx index cf685828aa..5babfbef7f 100644 --- a/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx +++ b/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx @@ -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() &&
| "empty" | "readOnlyCode" | "readOnlyText" | "readOnlyOCRText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "sqlConsole" | "llmChat"; +export type ExtendedNoteType = Exclude | "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 = { 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", diff --git a/apps/client/src/widgets/react/Button.tsx b/apps/client/src/widgets/react/Button.tsx index 3114b9dfe8..95601a9228 100644 --- a/apps/client/src/widgets/react/Button.tsx +++ b/apps/client/src/widgets/react/Button.tsx @@ -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 ( -
+
{children}
); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 5b23276923..d507c7e962 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -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) }, [ handlers ]); } -export function useSyncedRef(externalRef?: RefObject, initialValue: T | null = null): RefObject { +export function useSyncedRef(externalRef?: Ref, initialValue: T | null = null): RefObject { const ref = useRef(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(!!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()) { diff --git a/apps/client/src/widgets/ribbon/NoteActionsCustom.css b/apps/client/src/widgets/ribbon/NoteActionsCustom.css index 3c2a2000ce..5e232b114e 100644 --- a/apps/client/src/widgets/ribbon/NoteActionsCustom.css +++ b/apps/client/src/widgets/ribbon/NoteActionsCustom.css @@ -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; + } +} + diff --git a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx index 92587d2ca9..15120fd2aa 100644 --- a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx +++ b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx @@ -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) { - + @@ -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 && 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 && 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 ( +
+ {buttons.map(({ value, icon, text }) => ( + setDisplayMode(value)} + /> + ))} +
+ ); + } + + return ( + <> +
+ + {buttons.map(({ value, icon, text }) => ( + setDisplayMode(value)} + /> + ))} + +
+ + ); } function RunActiveNoteButton({ noteMime }: NoteActionsCustomInnerProps) { @@ -268,12 +307,12 @@ function AddChildButton({ parentComponent, noteType, ntxId, isReadOnly }: NoteAc } //#endregion -function NoteAction({ text, ...props }: Pick & { +function NoteAction({ text, active, ...props }: Pick & { onClick?: ((e: MouseEvent) => void) | undefined; }) { return (cachedIsMobile ? {text} - : + : ); } diff --git a/apps/client/src/widgets/scroll_padding.tsx b/apps/client/src/widgets/scroll_padding.tsx index e277ee86a1..57d7349d05 100644 --- a/apps/client/src/widgets/scroll_padding.tsx +++ b/apps/client/src/widgets/scroll_padding.tsx @@ -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; diff --git a/apps/client/src/widgets/sidebar/TableOfContents.tsx b/apps/client/src/widgets/sidebar/TableOfContents.tsx index 4442af884c..d84670128d 100644 --- a/apps/client/src/widgets/sidebar/TableOfContents.tsx +++ b/apps/client/src/widgets/sidebar/TableOfContents.tsx @@ -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( '' ); } diff --git a/apps/client/src/widgets/type_widgets/MindMap.tsx b/apps/client/src/widgets/type_widgets/MindMap.tsx index d4a3ff1841..bf1f3382b1 100644 --- a/apps/client/src/widgets/type_widgets/MindMap.tsx +++ b/apps/client/src/widgets/type_widgets/MindMap.tsx @@ -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(null); const containerRef = useRef(null); - const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly"); + const isReadOnly = useEffectiveReadOnly(note, noteContext); const spacedUpdate = useEditorSpacedUpdate({ diff --git a/apps/client/src/widgets/type_widgets/canvas/Canvas.tsx b/apps/client/src/widgets/type_widgets/canvas/Canvas.tsx index 3c22af9c76..6ffe65c43f 100644 --- a/apps/client/src/widgets/type_widgets/canvas/Canvas.tsx +++ b/apps/client/src/widgets/type_widgets/canvas/Canvas.tsx @@ -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(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); diff --git a/apps/client/src/widgets/type_widgets/code/Code.tsx b/apps/client/src/widgets/type_widgets/code/Code.tsx index 4f34edd96b..6c74dc8219 100644 --- a/apps/client/src/widgets/type_widgets/code/Code.tsx +++ b/apps/client/src/widgets/type_widgets/code/Code.tsx @@ -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 void; placeholder?: string; + /** Optional external ref to the underlying CodeMirror `EditorView`. Populated once the editor has initialized. */ + editorRef?: Ref; } -export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent }: TypeWidgetProps) { +export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent, editorRef }: TypeWidgetProps & { editorRef?: Ref }) { 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 ( (null); const containerRef = useRef(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 <> { - 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?.(); diff --git a/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx b/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx index c52c8e7676..e80a0202a2 100644 --- a/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx +++ b/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx @@ -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 { content?: string; mime: string; className?: string; - editorRef?: RefObject; - containerRef?: RefObject; + editorRef?: Ref; + containerRef?: Ref; 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(); diff --git a/apps/client/src/widgets/type_widgets/code/Markdown.css b/apps/client/src/widgets/type_widgets/code/Markdown.css new file mode 100644 index 0000000000..30edf72ffe --- /dev/null +++ b/apps/client/src/widgets/type_widgets/code/Markdown.css @@ -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); + } + +} + diff --git a/apps/client/src/widgets/type_widgets/code/Markdown.spec.ts b/apps/client/src/widgets/type_widgets/code/Markdown.spec.ts new file mode 100644 index 0000000000..0addb5037a --- /dev/null +++ b/apps/client/src/widgets/type_widgets/code/Markdown.spec.ts @@ -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("

Heading

"); + expect(html).toContain("
    "); + expect(html).toContain("
  • item
  • "); + }); + + it("keeps H1 as H1 in the preview (no title-row context to avoid)", () => { + const html = renderWithSourceLines("# Top level"); + expect(html).toContain("

    Top level

    "); + }); + + 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('

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 @@ -186,8 +184,7 @@

  • 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.
  • -
  • b) Convey the object code in, or embodied in, a physical product (including +
  • 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 @@ -197,25 +194,25 @@ 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.
  • -
  • 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.
  • -
  • 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.
  • -
  • 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.
  • +
  • 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.
  • +
  • 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.
  • +
  • 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.
  • A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types.html index e1c453eb6b..2607200081 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types.html @@ -9,7 +9,8 @@ note where to place the new one and select:

    • Insert note after, to put the new note underneath the one selected.
    • -
    • Insert child note, to insert the note as a child of the selected +
    • Insert child note, to insert the note as a child of the selected note.

    @@ -20,7 +21,8 @@

  • When adding a link in a Text note, type the desired title of the new note and press Enter. Afterwards the type of the note will be asked.
  • -
  • Similarly, when creating a new tab, type the desired title and press Enter.
  • +
  • Similarly, when creating a new tab, type the desired title and press Enter.
  • Changing the type of a note

    It is possible to change the type of a note after it has been created @@ -30,94 +32,96 @@ edit the source of a note.

    Supported note types

    The following note types are supported by Trilium:

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Note TypeDescription
    Text - The default note type, which allows for rich text formatting, images, - admonitions and right-to-left support.
    Code - 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.
    Saved Search - 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.
    Relation Map - Allows easy creation of notes and relations between them. Can be used - for mainly relational data such as a family tree.
    Note Map - Displays the relationships between the notes, whether via relations or - their hierarchical structure.
    Render Note - Used in Scripting, - 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.
    Collections - Displays the children of the note either as a grid, a list, or for a more - specialized case: a calendar.  -
    -
    Generally useful for easy reading of short notes.
    Mermaid Diagrams - 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.
    Canvas - Allows easy drawing of sketches, diagrams, handwritten content. Uses the - same technology behind excalidraw.com.
    Web View - Displays the content of an external web page, similar to a browser.
    Mind Map - Easy for brainstorming ideas, by placing them in a hierarchical layout.
    Geo Map - 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.
    File - Represents an uploaded file such as PDFs, images, video or audio files.
    \ No newline at end of file +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Note TypeDescription
    Text + The default note type, which allows for rich text formatting, images, + admonitions and right-to-left support.
    Code + 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.
    Saved Search + 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.
    Relation Map + Allows easy creation of notes and relations between them. Can be used + for mainly relational data such as a family tree.
    Note Map + Displays the relationships between the notes, whether via relations or + their hierarchical structure.
    Render Note + Used in Scripting, + 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.
    Collections + Displays the children of the note either as a grid, a list, or for a more + specialized case: a calendar.   +
    +
    Generally useful for easy reading of short notes.
    Mermaid Diagrams + 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.
    Canvas + Allows easy drawing of sketches, diagrams, handwritten content. Uses the + same technology behind excalidraw.com.
    Web View + Displays the content of an external web page, similar to a browser.
    Mind Map + Easy for brainstorming ideas, by placing them in a hierarchical layout.
    Geo Map + 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.
    File + Represents an uploaded file such as PDFs, images, video or audio files.
    +
    \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html index f64ba7eade..152140d5ed 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/File.html @@ -5,7 +5,8 @@ create a File note type directly:

    • Drag a file into the Note Tree.
    • -
    • Right click a note and select Import into note and point it to +
    • Right click a note and select Import into note and point it to one of the supported files.

    Supported file types

    @@ -82,28 +83,30 @@ href="#root/_help_BlN9DFI679QC">Ribbon.
    • Download, which will download the file for local use.
    • -
    • Open, will will open the file with the system-default application.
    • -
    • Upload new revision to replace the file with a new one.
    • +
    • Open, will will open the file with the system-default application.
    • +
    • Upload new revision to replace the file with a new one.
    - -
  • It is not possible to change the note type of a File note.
  • -
  • Convert into an attachment from the note menu.
  • + +
  • It is not possible to change the note type of a File note.
  • +
  • Convert into an attachment from the note menu.
  • Relation with other notes

    • Files are also displayed in the Note List based on their type:

      - -
    • -
    • -

      Non-image files can be embedded into text notes as read-only widgets via - the Include Note functionality.

      -
    • -
    • -

      Image files can be embedded into text notes like normal images via  - Image references.

      +

      + +

    • +
    • Non-image files can be embedded into text notes as read-only widgets via + the Include Note functionality.
    • +
    • Image files can be embedded into text notes like normal images via  + Image references.
    \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Markdown.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Markdown.html new file mode 100644 index 0000000000..74cf113776 --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Markdown.html @@ -0,0 +1,140 @@ +

    Trilium has always supported Markdown through its import feature, + however the file was either transformed to a Text note + (converted to Trilium's internal HTML format) or saved as a Code note + with only syntax highlight.

    +

    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 Note types with split view for + more information.

    +

    Rationale

    +

    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.

    +

    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.

    + +

    Features

    +

    Source view pane

    +
      +
    • Syntax highlighting for the Markdown syntax.
    • +
    • Nested syntax highlighting for code inside code blocks.
    • +
    • When editing larger documents, the preview scrolls along with the source + editor.
    • +
    +

    Preview pane

    +

    The following features are supported by Trilium's Markdown format and + will show up in the preview pane:

    +
      +
    • All standard and GitHub-flavored syntax (basic formatting, tables, blockquotes)
    • +
    • Code blocks with syntax highlight (e.g. ```js) + and automatic syntax highlight
    • +
    • Block quotes & admonitions +
    • +
    • Math Equations +
    • +
    • Mermaid Diagrams using + ```mermaid +
    • +
    • +

      Include Note (no + builtin Markdown syntax, but HTML syntax works just fine):

      <section class="include-note" data-note-id="vJDjQm0VK8Na" data-box-size="expandable">
      +	&nbsp;
      +</section>n
      +
    • +
    • +

      Internal (reference) links via + its HTML syntax, or through a Wikilinks-like format (only  + Note ID):

      [[Hg8TS5ZOxti6]]
      +
    • +
    +

    Creating Markdown notes

    +

    There are two ways to create a Markdown note:

    +
      +
    1. Create a new note (e.g. in the Note Tree) + and select the type Markdown, just like all the other note types.
    2. +
    3. Create a note of type Code and + select as the language either Markdown or GitHub-Flavored Markdown. + This maintains compatibility with your existing notes prior to the introduction + of this feature.
    4. +
    + +

    Import/export

    +
      +
    • +

      By default, when importing a single Markdown file it automatically gets + converted to a Text note. + To avoid that and have it imported as a Markdown note instead:

      +
        +
      • +

        Right click the Note Tree and + select Import into note.

        +
      • +
      • +

        Select the file normally.

        +
      • +
      • +

        Uncheck Import HTML, Markdown and TXT as text notes if it's unclear from the metadata.

        +
      • +
      +
    • +
    • +

      When exporting Markdown files, the extension is preserved and the content + remains the same as in the source view.

      +
    • +
    • +

      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.

      +
    • +
    +

    Conversion between text notes and Markdown notes

    +

    Currently there is no built-in functionality to convert a Text note + into a Markdown note or vice-versa. We do have plans to address this in + the future.

    +

    This can be achieved manually, for a single note:

    +
      +
    1. Export the file as Markdown, with single format.
    2. +
    3. Import the file again, but unchecking Import HTML, Markdown and TXT as text notes if it's unclear from the metadata.
    4. +
    +

    For multiple notes, the process is slightly more involved:

    +
      +
    1. Export the file as Markdown, ZIP.
    2. +
    3. Extract the archive.
    4. +
    5. Remove the !!!meta.json file.
    6. +
    7. Compress the extracted files back into an archive.
    8. +
    9. Import the newly create archive, but unchecking Import HTML, Markdown and TXT as text notes if it's unclear from the metadata.
    10. +
    +

    Sync-scrolling & block highlight

    +

    When scrolling through the editing pane, the preview pane will attempt + to synchronize its position to make it easier to see the preview.

    +

    In addition, the block in the preview matching the position of the cursor + in the source view will appear slightly highlighted.

    +

    The sync is currently one-way only, scrolling the preview will not synchronize + the position of the editor.

    +

    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.

    + \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Mermaid Diagrams.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Mermaid Diagrams.html index 5bf0e4ba32..d6ff44cd93 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Mermaid Diagrams.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Mermaid Diagrams.html @@ -6,11 +6,15 @@ -

    Types of diagrams

    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.

    +

    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 Note types with split view for + more information.

    +

    Sample diagrams

    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:

    @@ -48,30 +52,34 @@
  • The preview can be moved around by holding the left mouse button and dragging.
  • -
  • Zooming can also be done by using the scroll wheel.
  • -
  • The zoom and position on the preview will remain fixed as the diagram - changes, to be able to work more easily with large diagrams.
  • - +
  • Zooming can also be done by using the scroll wheel.
  • +
  • The zoom and position on the preview will remain fixed as the diagram + changes, to be able to work more easily with large diagrams.
  • +
  • The size of the source/preview panes can be adjusted by hovering over the border between them and dragging it with the mouse.
  • In the Floating buttons area:
    • The source/preview can be laid out left-right or bottom-top via the Move editing pane to the left / bottom option.
    • -
    • Press Lock editing to automatically mark the note as read-only. +
    • Press Lock editing 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 Unlock editing to mark a read-only note as editable.
    • -
    • Press the Copy image reference to the clipboard to be able to insert - the image representation of the diagram into a text note. See Image references for more information.
    • -
    • Press the Export diagram as SVG to download a scalable/vector rendering - of the diagram. Can be used to present the diagram without degrading when - zooming.
    • +
    • Press the Copy image reference to the clipboard to be able to insert + the image representation of the diagram into a text note. See Image references for more information.
    • +
    • Press the Export diagram as SVG to download a scalable/vector rendering + of the diagram. Can be used to present the diagram without degrading when + zooming.
    • Press the Export diagram as PNG 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.
    • -
    -
  • + +

    Errors in the diagram

    If there is an error in the source code, the error will be displayed in diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html index 7e6b8cddbe..4a33c30bfe 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html @@ -13,11 +13,13 @@

    1. HTML language for the legacy/vanilla method, with what needs to be displayed (for example <p>Hello world.</p>).
    2. -
    3. JSX for the Preact-based approach (see below).
    4. -
    +
  • JSX for the Preact-based approach (see below).
  • +
  • Create a Render Note.
  • -
  • Assign the renderNote relation to +
  • Assign the renderNote relation to point at the previously created code note.
  • Legacy scripting using jQuery

    @@ -46,10 +48,9 @@ $dateEl.text(new Date()); need to provide a HTML anymore.

    Here are the steps to creating a simple render note:

      -
    1. -

      Create a note of type Render Note.

      -
    2. -
    3. +
    4. Create a note of type Render Note.
    5. +
    6. Create a child Code note with JSX as the language.
      As an example, use the following content:

      export default function() {
      @@ -59,24 +60,21 @@ $dateEl.text(new Date());
      </> ); } -
    7. -
    8. -

      In the parent render note, define a ~renderNote relation - pointing to the newly created child.

      -
    9. -
    10. -

      Refresh the render note and it should display a “Hello world” message.

      -
    11. + +
    12. In the parent render note, define a ~renderNote relation + pointing to the newly created child.
    13. +
    14. Refresh the render note and it should display a “Hello world” message.

    Refreshing the note

    It's possible to refresh the note via:

    Examples

    \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Spreadsheets.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Spreadsheets.html index 010b594aa6..06f944a848 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Spreadsheets.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Spreadsheets.html @@ -64,15 +64,15 @@ yet:

    • Trilium-specific formulas (e.g. to obtain the title of a note).
    • -
    • User-defined formulas
    • -
    • Cross-workbook calculation
    • +
    • User-defined formulas
    • +
    • Cross-workbook calculation

    If you would like us to work on these features, consider supporting us.

    Known limitations

      -
    • -

      It is possible to share a spreadsheet, case in which a best-effort HTML - rendering of the spreadsheet is done.

      +
    • It is possible to share a spreadsheet, case in which a best-effort HTML + rendering of the spreadsheet is done.
      • For more advanced use cases, this will most likely not work as intended. Feel free to report issues, but keep in @@ -80,12 +80,9 @@ the features of Univer.
    • -
    • -

      There is currently no export functionality, as stated previously.

      -
    • -
    • -

      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.

      -
    • +
    • There is currently no export functionality, as stated previously.
    • +
    • 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.
    \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html index 42dbe0fa44..26a0791ace 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html @@ -20,168 +20,171 @@

    Fore more information see Formatting toolbar.

    Features and formatting

    Here's a list of various features supported by text notes:

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Dedicated articleFeature
    General formatting - -
      -
    • Headings (section titles, paragraph)
    • -
    • Font size
    • -
    • Bold, italic, underline, strike-through
    • -
    • Superscript, subscript
    • -
    • Font color & background color
    • -
    • Remove formatting
    • -
    -
    Lists - -
      -
    • Bulleted lists
    • -
    • Numbered lists
    • -
    • To-do lists
    • -
    -
    Block quotes & admonitions - -
      -
    • Block quotes
    • -
    • Admonitions
    • -
    -
    Tables - -
      -
    • Basic tables
    • -
    • Merging cells
    • -
    • Styling tables and cells.
    • -
    • Table captions
    • -
    -
    Developer-specific formatting - -
      -
    • Inline code
    • -
    • Code blocks
    • -
    • Keyboard shortcuts
    • -
    -
    Footnotes - -
      -
    • Footnotes
    • -
    -
    Images - -
      -
    • Images
    • -
    -
    Links - -
      -
    • External links
    • -
    • Internal Trilium links
    • -
    -
    Include Note - -
      -
    • Include note
    • -
    -
    Insert buttons - -
      -
    • Symbols
    • -
    • Math Equations -
    • -
    • Mermaid diagrams
    • -
    • Horizontal ruler
    • -
    • Page break
    • -
    -
    Other features - - -
    Premium features - - -
    -

    Read-Only vs. Editing Mode

    -

    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 Read-Only Notes.

    -

    Keyboard shortcuts

    -

    There are numerous keyboard shortcuts to format the text without having - to use the mouse. For a reference of all the key combinations, see  - Keyboard Shortcuts. In addition, see Markdown-like formatting as an alternative - to the keyboard shortcuts.

    -

    Technical details

    -

    For the text editing functionality, Trilium uses a commercial product - (with an open-source base) called CKEditor. - This brings the benefit of having a powerful WYSIWYG (What You See Is What - You Get) editor.

    \ No newline at end of file +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Dedicated articleFeature
    General formatting + +
      +
    • Headings (section titles, paragraph)
    • +
    • Font size
    • +
    • Bold, italic, underline, strike-through
    • +
    • Superscript, subscript
    • +
    • Font color & background color
    • +
    • Remove formatting
    • +
    +
    Lists + +
      +
    • Bulleted lists
    • +
    • Numbered lists
    • +
    • To-do lists
    • +
    +
    Block quotes & admonitions + +
      +
    • Block quotes
    • +
    • Admonitions
    • +
    +
    Tables + +
      +
    • Basic tables
    • +
    • Merging cells
    • +
    • Styling tables and cells.
    • +
    • Table captions
    • +
    +
    Developer-specific formatting + +
      +
    • Inline code
    • +
    • Code blocks
    • +
    • Keyboard shortcuts
    • +
    +
    Footnotes + +
      +
    • Footnotes
    • +
    +
    Images + +
      +
    • Images
    • +
    +
    Links + +
      +
    • External links
    • +
    • Internal Trilium links
    • +
    +
    Include Note + +
      +
    • Include note
    • +
    +
    Insert buttons + +
      +
    • Symbols
    • +
    • Math Equations +
    • +
    • Mermaid diagrams
    • +
    • Horizontal ruler
    • +
    • Page break
    • +
    +
    Other features + + +
    Premium features + + +
    +
    +

    Read-Only vs. Editing Mode

    +

    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 Read-Only Notes.

    +

    Keyboard shortcuts

    +

    There are numerous keyboard shortcuts to format the text without having + to use the mouse. For a reference of all the key combinations, see  + Keyboard Shortcuts. In addition, see Markdown-like formatting as an alternative + to the keyboard shortcuts.

    +

    Technical details

    +

    For the text editing functionality, Trilium uses a commercial product + (with an open-source base) called CKEditor. + This brings the benefit of having a powerful WYSIWYG (What You See Is What + You Get) editor.

    \ No newline at end of file diff --git a/apps/server/src/services/export/markdown.ts b/apps/server/src/services/export/markdown.ts index 35e08ee11f..c82532a3b3 100644 --- a/apps/server/src/services/export/markdown.ts +++ b/apps/server/src/services/export/markdown.ts @@ -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 = { - note: "NOTE", - tip: "TIP", - important: "IMPORTANT", - caution: "CAUTION", - warning: "WARNING" -}; +export { ADMONITION_TYPE_MAPPINGS }; export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note; diff --git a/apps/server/src/services/export/single.spec.ts b/apps/server/src/services/export/single.spec.ts index a7c8118aee..2d813e197f 100644 --- a/apps/server/src/services/export/single.spec.ts +++ b/apps/server/src/services/export/single.spec.ts @@ -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 + }); + } + }); }); diff --git a/apps/server/src/services/export/single.ts b/apps/server/src/services/export/single.ts index 16d36807e8..9764f22abc 100644 --- a/apps/server/src/services/export/single.ts +++ b/apps/server/src/services/export/single.ts @@ -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, format: ExportFormat) { let payload, extension, mime; @@ -60,7 +75,11 @@ export function mapByNoteType(note: BNote, content: string | Buffer 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"; } diff --git a/apps/server/src/services/import/markdown.ts b/apps/server/src/services/import/markdown.ts index a812567887..85c63c852d 100644 --- a/apps/server/src/services/import/markdown.ts +++ b/apps/server/src/services/import/markdown.ts @@ -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 `

    ${data.text}

    `; - } - - 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 `
    ${text}
    `; - } - - 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(/^
      /, "
        "); - } - - return result; - } - - override checkbox({ checked }: Tokens.Checkbox): string { - return ``; - } - - 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 += `${this.parser.parse(item.tokens.filter(t => t.type !== "checkbox"))}`; - return `
      • `; - } - - 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 = /^

        \[\!([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(/^

        \[\!([A-Z]+)\]\s*/, "

        ") - .replace(/^

        <\/p>/, ""); // Having a heading will generate an empty paragraph that we need to remove. - - return `

        `; - } - } - - return `
        ${body}
        `; - } - - codespan({ text }: Tokens.Codespan): string { - return `${escape(text)}`; - } +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 } { - const codeMap = new Map(); - let id = 0; - const timestamp = Date.now(); - - // Multi-line code block and Inline code - text = text.replace(/```[\s\S]*?```/g, (m) => { - const key = ``; - codeMap.set(key, m); - return key; - }).replace(/`[^`\n]+`/g, (m) => { - const key = ``; - codeMap.set(key, m); - return key; - }); - - return { processedText: text, placeholderMap: codeMap }; -} - -function extractFormulas(text: string): { processedText: string, placeholderMap: Map } { - // Protect the $ signs inside code blocks from being recognized as formulas. - const { processedText: noCodeText, placeholderMap: codeMap } = extractCodeBlocks(text); - - const formulaMap = new Map(); - let id = 0; - const timestamp = Date.now(); - - // Display math and Inline math - let processedText = noCodeText.replace(/(? { - const key = ``; - const rendered = `\\[${formula}\\]`; - formulaMap.set(key, rendered); - return key; - }).replace(/(? { - const key = ``; - const rendered = `\\(${formula}\\)`; - formulaMap.set(key, rendered); - return key; - }); - - processedText = restoreFromMap(processedText, codeMap); - - return { processedText, placeholderMap: formulaMap }; -} - -function restoreFromMap(text: string, map: Map): 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 }; diff --git a/apps/server/src/services/import/mime.spec.ts b/apps/server/src/services/import/mime.spec.ts index 223517d320..9c9fe825b9 100644 --- a/apps/server/src/services/import/mime.spec.ts +++ b/apps/server/src/services/import/mime.spec.ts @@ -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" diff --git a/apps/server/src/services/import/mime.ts b/apps/server/src/services/import/mime.ts index b25e98926d..7a54a1f5a1 100644 --- a/apps/server/src/services/import/mime.ts +++ b/apps/server/src/services/import/mime.ts @@ -97,7 +97,7 @@ function getType(options: TaskData<"importNotes">, mime: string): NoteType { case options?.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown", "text/mdx"].includes(mimeLc): return "text"; - case options?.codeImportedAsCode && CODE_MIME_TYPES.has(mimeLc): + case options?.codeImportedAsCode && (CODE_MIME_TYPES.has(mimeLc) || CODE_MIME_TYPES_OVERRIDE.has(mimeLc)): return "code"; case mime.startsWith("image/"): diff --git a/apps/server/src/services/import/zip.ts b/apps/server/src/services/import/zip.ts index 5d9e00d8ab..e4a0dfbd98 100644 --- a/apps/server/src/services/import/zip.ts +++ b/apps/server/src/services/import/zip.ts @@ -175,8 +175,12 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu } function detectFileTypeAndMime(taskContext: TaskContext<"importNotes">, filePath: string) { - const mime = mimeService.getMime(filePath) || "application/octet-stream"; - const type = mimeService.getType(taskContext.data || {}, mime); + const rawMime = mimeService.getMime(filePath) || "application/octet-stream"; + const type = mimeService.getType(taskContext.data || {}, rawMime); + // Normalize aliased code MIMEs (e.g. `text/markdown` → `text/x-markdown`, + // `application/javascript` → `application/javascript;env=frontend`) so the + // stored MIME matches what the rest of the app expects. + const mime = (type === "code" && mimeService.normalizeMimeType(rawMime)) || rawMime; return { mime, type }; } diff --git a/docs/Developer Guide/Developer Guide/Documentation.md b/docs/Developer Guide/Developer Guide/Documentation.md index 2176a424b1..aae807a49d 100644 --- a/docs/Developer Guide/Developer Guide/Documentation.md +++ b/docs/Developer Guide/Developer Guide/Documentation.md @@ -1,5 +1,5 @@ # Documentation -There are multiple types of documentation for Trilium: +There are multiple types of documentation for Trilium: * The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing F1. * The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers. diff --git a/docs/User Guide/!!!meta.json b/docs/User Guide/!!!meta.json index a70689b577..d1b8f5d265 100644 --- a/docs/User Guide/!!!meta.json +++ b/docs/User Guide/!!!meta.json @@ -3962,6 +3962,83 @@ "attachments": [] } ] + }, + { + "isClone": false, + "noteId": "SL5f1Auq7sVN", + "notePath": [ + "pOsGYCXsbNQG", + "gh7bpGYxajRS", + "Vc8PjrjAGuOp", + "SL5f1Auq7sVN" + ], + "title": "Note types with split view", + "notePosition": 230, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/html", + "attributes": [ + { + "type": "relation", + "name": "internalLink", + "value": "s1aBHPd79XYj", + "isInheritable": false, + "position": 30 + }, + { + "type": "relation", + "name": "internalLink", + "value": "6RM1Q7ppFVoj", + "isInheritable": false, + "position": 40 + }, + { + "type": "relation", + "name": "internalLink", + "value": "CoFPLs3dRlXc", + "isInheritable": false, + "position": 50 + }, + { + "type": "relation", + "name": "internalLink", + "value": "8YBEPzcpUgxw", + "isInheritable": false, + "position": 60 + }, + { + "type": "relation", + "name": "internalLink", + "value": "IjZS7iK5EXtb", + "isInheritable": false, + "position": 70 + }, + { + "type": "relation", + "name": "internalLink", + "value": "XpOYSgsLkTJy", + "isInheritable": false, + "position": 80 + }, + { + "type": "label", + "name": "iconClass", + "value": "bx bx-card", + "isInheritable": false, + "position": 90 + }, + { + "type": "label", + "name": "shareAlias", + "value": "note-types-with-split-view", + "isInheritable": false, + "position": 100 + } + ], + "format": "markdown", + "dataFileName": "Note types with split view.md", + "attachments": [] } ] }, @@ -10094,6 +10171,13 @@ "value": "bx bx-selection", "isInheritable": false, "position": 20 + }, + { + "type": "relation", + "name": "internalLink", + "value": "SL5f1Auq7sVN", + "isInheritable": false, + "position": 40 } ], "format": "markdown", @@ -10739,6 +10823,124 @@ "dataFileName": "Spreadsheets_image.png" } ] + }, + { + "isClone": false, + "noteId": "6RM1Q7ppFVoj", + "notePath": [ + "pOsGYCXsbNQG", + "KSZ04uQ2D1St", + "6RM1Q7ppFVoj" + ], + "title": "Markdown", + "notePosition": 230, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/html", + "attributes": [ + { + "type": "label", + "name": "iconClass", + "value": "bx bxl-markdown", + "isInheritable": false, + "position": 30 + }, + { + "type": "label", + "name": "shareAlias", + "value": "markdown", + "isInheritable": false, + "position": 40 + }, + { + "type": "relation", + "name": "internalLink", + "value": "Oau6X9rCuegd", + "isInheritable": false, + "position": 50 + }, + { + "type": "relation", + "name": "internalLink", + "value": "iPIMuisry3hd", + "isInheritable": false, + "position": 60 + }, + { + "type": "relation", + "name": "internalLink", + "value": "6f9hih2hXXZk", + "isInheritable": false, + "position": 70 + }, + { + "type": "relation", + "name": "internalLink", + "value": "oPVyFC7WL2Lp", + "isInheritable": false, + "position": 80 + }, + { + "type": "relation", + "name": "internalLink", + "value": "wy8So3yZZlH9", + "isInheritable": false, + "position": 150 + }, + { + "type": "relation", + "name": "internalLink", + "value": "SL5f1Auq7sVN", + "isInheritable": false, + "position": 160 + }, + { + "type": "relation", + "name": "internalLink", + "value": "NwBbFdNZ9h7O", + "isInheritable": false, + "position": 170 + }, + { + "type": "relation", + "name": "internalLink", + "value": "YfYAtQBcfo5V", + "isInheritable": false, + "position": 180 + }, + { + "type": "relation", + "name": "internalLink", + "value": "s1aBHPd79XYj", + "isInheritable": false, + "position": 190 + }, + { + "type": "relation", + "name": "internalLink", + "value": "nBAXQFj20hS1", + "isInheritable": false, + "position": 200 + }, + { + "type": "relation", + "name": "internalLink", + "value": "hrZ1D00cLbal", + "isInheritable": false, + "position": 210 + }, + { + "type": "relation", + "name": "internalLink", + "value": "m1lbrzyKDaRB", + "isInheritable": false, + "position": 220 + } + ], + "format": "markdown", + "dataFileName": "Markdown.md", + "attachments": [] } ] }, diff --git a/docs/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note types with split view.md b/docs/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note types with split view.md new file mode 100644 index 0000000000..6595e897fd --- /dev/null +++ b/docs/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note types with split view.md @@ -0,0 +1,21 @@ +# Note types with split view +Split view is a feature of Mermaid Diagrams and Markdown notes which displays both the source code on one side and the preview of the content on the other. + +Mermaid Diagrams also allow changing between a horizontal or a vertical split, to accommodate for the various sizes of diagrams. + +## Display modes and interaction + +The split comes with three different display modes: + +* _Split view_, in which both the source code is available on one side and can be edited, and the preview is available on the other side. + * In this mode, the size of either the source pane or the preview pane can be adjusted by dragging the small border between them. +* _Source view_ which shows the source code on the entire screen for a more focused editing experience. +* _Preview_ which displays only the rendering of the diagram or text in full screen, especially useful for read-only notes. + +These buttons can be found near the Note buttons section on the New Layout, or in the Floating buttons on the old layout. + +The display node is stored at note level. + +## Relation to read-only notes + +If a note is marked as [read-only](../Notes/Read-Only%20Notes.md), 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. \ No newline at end of file diff --git a/docs/User Guide/User Guide/Note Types.md b/docs/User Guide/User Guide/Note Types.md index 71ac53e644..8505e4fe8e 100644 --- a/docs/User Guide/User Guide/Note Types.md +++ b/docs/User Guide/User Guide/Note Types.md @@ -33,7 +33,7 @@ The following note types are supported by Trilium: | Relation Map | Allows easy creation of notes and relations between them. Can be used for mainly relational data such as a family tree. | | Note Map | Displays the relationships between the notes, whether via relations or their hierarchical structure. | | Render Note | Used in Scripting, 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. | -| Collections | Displays the children of the note either as a grid, a list, or for a more specialized case: a calendar. 

        Generally useful for easy reading of short notes. | +| Collections | Displays the children of the note either as a grid, a list, or for a more specialized case: a calendar.  

        Generally useful for easy reading of short notes. | | Mermaid Diagrams | 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. | | Canvas | Allows easy drawing of sketches, diagrams, handwritten content. Uses the same technology behind [excalidraw.com](https://excalidraw.com). | | Web View | Displays the content of an external web page, similar to a browser. | diff --git a/docs/User Guide/User Guide/Note Types/Markdown.md b/docs/User Guide/User Guide/Note Types/Markdown.md new file mode 100644 index 0000000000..b96b627329 --- /dev/null +++ b/docs/User Guide/User Guide/Note Types/Markdown.md @@ -0,0 +1,93 @@ +# Markdown +Trilium has always supported Markdown through its [import feature](../Basic%20Concepts%20and%20Features/Import%20%26%20Export/Markdown.md), however the file was either transformed to a Text note (converted to Trilium's internal HTML format) or saved as a Code note with only syntax highlight. + +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 Note types with split view for more information. + +## Rationale + +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. + +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. + +> [!NOTE] +> 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. + +## Features + +### Source view pane + +* Syntax highlighting for the Markdown syntax. +* Nested syntax highlighting for code inside code blocks. +* When editing larger documents, the preview scrolls along with the source editor. + +### Preview pane + +The following features are supported by Trilium's Markdown format and will show up in the preview pane: + +* All standard and GitHub-flavored syntax (basic formatting, tables, blockquotes) +* Code blocks with syntax highlight (e.g. ` ```js `) and automatic syntax highlight +* Block quotes & admonitions +* Math Equations +* Mermaid Diagrams using ` ```mermaid ` +* Include Note (no builtin Markdown syntax, but HTML syntax works just fine): + + ``` +
        +   +
        n + ``` +* Internal (reference) links via its HTML syntax, or through a _Wikilinks_\-like format (only Note ID): + + ``` + [[Hg8TS5ZOxti6]] + ``` + +## Creating Markdown notes + +There are two ways to create a Markdown note: + +1. Create a new note (e.g. in the Note Tree) and select the type _Markdown_, just like all the other note types. +2. Create a note of type Code and select as the language either _Markdown_ or _GitHub-Flavored Markdown_. This maintains compatibility with your existing notes prior to the introduction of this feature. + +> [!NOTE] +> There is no distinction between the new Markdown note type and code notes of type Markdown; internally both are represented as Code notes with the proper MIME type (e.g. `text/x-markdown`). + +## Import/export + +* By default, when importing a single Markdown file it automatically gets converted to a Text note. To avoid that and have it imported as a Markdown note instead: + + * Right click the Note Tree and select _Import into note_. + * Select the file normally. + * Uncheck _Import HTML, Markdown and TXT as text notes if it's unclear from the metadata_. +* When exporting Markdown files, the extension is preserved and the content remains the same as in the source view. +* 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. + +## Conversion between text notes and Markdown notes + +Currently there is no built-in functionality to convert a Text note into a Markdown note or vice-versa. We do have plans to address this in the future. + +This can be achieved manually, for a single note: + +1. Export the file as Markdown, with single format. +2. Import the file again, but unchecking _Import HTML, Markdown and TXT as text notes if it's unclear from the metadata_. + +For multiple notes, the process is slightly more involved: + +1. Export the file as Markdown, ZIP. +2. Extract the archive. +3. Remove the `!!!meta.json` file. +4. Compress the extracted files back into an archive. +5. Import the newly create archive, but unchecking _Import HTML, Markdown and TXT as text notes if it's unclear from the metadata_. + +## Sync-scrolling & block highlight + +When scrolling through the editing pane, the preview pane will attempt to synchronize its position to make it easier to see the preview. + +In addition, the block in the preview matching the position of the cursor in the source view will appear slightly highlighted. + +The sync is currently one-way only, scrolling the preview will not synchronize the position of the editor. + +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. + +> [!NOTE] +> 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 [report issues](../Troubleshooting/Reporting%20issues.md), but always provide a sample Markdown file to be able to reproduce it. \ No newline at end of file diff --git a/docs/User Guide/User Guide/Note Types/Mermaid Diagrams.md b/docs/User Guide/User Guide/Note Types/Mermaid Diagrams.md index 14b0729875..48c36e6f1e 100644 --- a/docs/User Guide/User Guide/Note Types/Mermaid Diagrams.md +++ b/docs/User Guide/User Guide/Note Types/Mermaid Diagrams.md @@ -4,10 +4,12 @@
        -## Types of diagrams - 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. +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 Note types with split view for more information. + +## Sample diagrams + 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: * Simply click on any of the samples to apply it. diff --git a/docs/User Guide/User Guide/Note Types/Render Note.md b/docs/User Guide/User Guide/Note Types/Render Note.md index 60387ce507..7bed5b4b79 100644 --- a/docs/User Guide/User Guide/Note Types/Render Note.md +++ b/docs/User Guide/User Guide/Note Types/Render Note.md @@ -69,4 +69,4 @@ It's possible to refresh the note via: ## Examples -* Weight Tracker which is present in the Demo Notes. \ No newline at end of file +* [missing note] which is present in the [missing note]. \ No newline at end of file diff --git a/docs/User Guide/User Guide/Note Types/Spreadsheets.md b/docs/User Guide/User Guide/Note Types/Spreadsheets.md index 70fa55a757..7728206945 100644 --- a/docs/User Guide/User Guide/Note Types/Spreadsheets.md +++ b/docs/User Guide/User Guide/Note Types/Spreadsheets.md @@ -60,7 +60,6 @@ If you would like us to work on these features, consider [supporting us](https:/ ## Known limitations * It is possible to share a spreadsheet, case in which a best-effort HTML rendering of the spreadsheet is done. - * For more advanced use cases, this will most likely not work as intended. Feel free to [report issues](../Troubleshooting/Reporting%20issues.md), but keep in mind that we might not be able to have a complete feature parity with all the features of Univer. * There is currently no export functionality, as stated previously. * 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. \ No newline at end of file diff --git a/packages/ckeditor5/tsconfig.lib.json b/packages/ckeditor5/tsconfig.lib.json index 748d57ae0b..613c7845e1 100644 --- a/packages/ckeditor5/tsconfig.lib.json +++ b/packages/ckeditor5/tsconfig.lib.json @@ -8,7 +8,7 @@ "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", "emitDeclarationOnly": true, "forceConsistentCasingInFileNames": true, - "lib": ["DOM", "ES2020"], + "lib": ["DOM", "ES2023"], "types": [ "vite/client", "jquery" diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 837ea1db6f..6d947aa4c3 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -12,6 +12,7 @@ "@codemirror/lang-javascript": "6.2.5", "@codemirror/lang-json": "6.0.2", "@codemirror/lang-markdown": "6.5.0", + "@codemirror/language-data": "6.5.1", "@codemirror/lang-php": "6.0.2", "@codemirror/lang-vue": "0.1.3", "@codemirror/lang-xml": "6.1.0", diff --git a/packages/codemirror/src/index.ts b/packages/codemirror/src/index.ts index 0fa8023ddb..de4f9f7a58 100644 --- a/packages/codemirror/src/index.ts +++ b/packages/codemirror/src/index.ts @@ -104,16 +104,14 @@ export default class CodeMirror extends EditorView { ]; } + extensions.push(EditorView.updateListener.of((v) => this.#onDocumentUpdated(v))); + if (!config.readOnly) { // Logic specific to editable notes if (config.placeholder) { extensions.push(placeholder(config.placeholder)); } - if (config.onContentChanged) { - extensions.push(EditorView.updateListener.of((v) => this.#onDocumentUpdated(v))); - } - extensions.push(historyCompartment.of(history())); } else { // Logic specific to read-only notes @@ -142,6 +140,21 @@ export default class CodeMirror extends EditorView { if (v.docChanged) { this.config.onContentChanged?.(); } + for (const listener of this.#updateListeners) listener(v); + } + + #updateListeners: Array<(v: ViewUpdate) => void> = []; + + /** + * Subscribe to view updates (doc changes, selection changes, viewport changes, etc.). + * Returns an unsubscribe function. The listener will not fire after the view is destroyed. + */ + addUpdateListener(listener: (v: ViewUpdate) => void): () => void { + this.#updateListeners.push(listener); + return () => { + const i = this.#updateListeners.indexOf(listener); + if (i >= 0) this.#updateListeners.splice(i, 1); + }; } getText() { diff --git a/packages/codemirror/src/syntax_highlighting.ts b/packages/codemirror/src/syntax_highlighting.ts index 518c84732b..d5c862f254 100644 --- a/packages/codemirror/src/syntax_highlighting.ts +++ b/packages/codemirror/src/syntax_highlighting.ts @@ -101,8 +101,10 @@ const byMimeType: Record Promise (await import('./languages/gdscript.js')).gdscript, "text/x-gfm": async () => { const { markdown, markdownLanguage } = (await import('@codemirror/lang-markdown')); + const { languages } = (await import('@codemirror/language-data')); return markdown({ - base: markdownLanguage + base: markdownLanguage, + codeLanguages: languages }); }, "text/x-go": async () => (await import('@codemirror/legacy-modes/mode/go')).go, @@ -124,7 +126,11 @@ const byMimeType: Record Promise (await import('@codemirror/legacy-modes/mode/livescript')).liveScript, "text/x-lua": async () => (await import('@codemirror/legacy-modes/mode/lua')).lua, "text/x-mariadb": async () => (await import('@codemirror/legacy-modes/mode/sql')).sqlite, - "text/x-markdown": async () => ((await import('@codemirror/lang-markdown')).markdown()), + "text/x-markdown": async () => { + const { markdown } = (await import('@codemirror/lang-markdown')); + const { languages } = (await import('@codemirror/language-data')); + return markdown({ codeLanguages: languages }); + }, "text/x-mathematica": async () => (await import('@codemirror/legacy-modes/mode/mathematica')).mathematica, "text/x-modelica": async () => (await import('@codemirror/legacy-modes/mode/modelica')).modelica, "text/x-mscgen": async () => (await import('@codemirror/legacy-modes/mode/mscgen')).mscgen, diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index f9e000aeba..71a451468a 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -18,3 +18,4 @@ export { default as BUILTIN_ATTRIBUTES } from "./lib/builtin_attributes.js"; export * from "./lib/spreadsheet/render_to_html.js"; export * from "./lib/llm_api.js"; export * from "./lib/marked_extensions.js"; +export * from "./lib/markdown_renderer.js"; diff --git a/packages/commons/src/lib/attribute_names.ts b/packages/commons/src/lib/attribute_names.ts index 9b53e784e3..e5081f6c39 100644 --- a/packages/commons/src/lib/attribute_names.ts +++ b/packages/commons/src/lib/attribute_names.ts @@ -70,6 +70,7 @@ type Labels = { webViewSrc: string; "disabled:webViewSrc": string; readOnly: boolean; + displayMode: string; tabWidth: number; indentWithTabs: boolean; wrapLines: boolean; diff --git a/packages/commons/src/lib/markdown_renderer.ts b/packages/commons/src/lib/markdown_renderer.ts new file mode 100644 index 0000000000..d6d3f38a99 --- /dev/null +++ b/packages/commons/src/lib/markdown_renderer.ts @@ -0,0 +1,307 @@ +import { Marked, Renderer, type Tokens } from "marked"; + +import { getMimeTypeFromMarkdownName, MIME_TYPE_AUTO, normalizeMimeTypeForCKEditor } from "./mime_type.js"; +import { + createTransclusionExtension, + createWikiLinkExtension, + transclusionExtension, + type TransclusionOptions, + wikiLinkExtension, + type WikiLinkOptions +} from "./marked_extensions.js"; + +/** + * Mapping from markdown admonition keywords (case-insensitive) to the ids + * used in the rendered `