From 8caaa99415726f9b890a319b7663fb5efd2bce80 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 20 Sep 2025 13:27:58 +0300 Subject: [PATCH] chore(react/type_widget): basic SVG rendering --- .../src/widgets/type_widgets/Mermaid.tsx | 30 +++++++++++++++- .../src/widgets/type_widgets/code/Code.tsx | 18 ++++++---- .../type_widgets/helpers/SplitEditor.css | 6 +++- .../type_widgets/helpers/SplitEditor.tsx | 12 ++++--- .../type_widgets/helpers/SvgSplitEditor.tsx | 35 ++++++++++++++++++- .../abstract_svg_split_type_widget.ts | 15 -------- .../src/widgets/type_widgets_old/mermaid.ts | 16 --------- 7 files changed, 87 insertions(+), 45 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/Mermaid.tsx b/apps/client/src/widgets/type_widgets/Mermaid.tsx index a82c969d4..81406b553 100644 --- a/apps/client/src/widgets/type_widgets/Mermaid.tsx +++ b/apps/client/src/widgets/type_widgets/Mermaid.tsx @@ -1,6 +1,34 @@ +import { useCallback } from "preact/hooks"; import SvgSplitEditor from "./helpers/SvgSplitEditor"; import { TypeWidgetProps } from "./type_widget"; +import { getMermaidConfig, loadElkIfNeeded, postprocessMermaidSvg } from "../../services/mermaid"; + +let idCounter = 1; +let registeredErrorReporter = false; export default function Mermaid(props: TypeWidgetProps) { - return ; + const renderSvg = useCallback(async (content: string) => { + const mermaid = (await import("mermaid")).default; + await loadElkIfNeeded(mermaid, content); + if (!registeredErrorReporter) { + // (await import("./linters/mermaid.js")).default(); + registeredErrorReporter = true; + } + + mermaid.initialize({ + startOnLoad: false, + ...(getMermaidConfig() as any), + }); + + idCounter++; + const { svg } = await mermaid.render(`mermaid-graph-${idCounter}`, content); + return postprocessMermaidSvg(svg); + }, []); + + return ( + + ); } diff --git a/apps/client/src/widgets/type_widgets/code/Code.tsx b/apps/client/src/widgets/type_widgets/code/Code.tsx index 6fed6a889..d877af29b 100644 --- a/apps/client/src/widgets/type_widgets/code/Code.tsx +++ b/apps/client/src/widgets/type_widgets/code/Code.tsx @@ -12,6 +12,14 @@ import keyboard_actions from "../../../services/keyboard_actions"; import { refToJQuerySelector } from "../../react/react_utils"; import { CODE_THEME_DEFAULT_PREFIX as DEFAULT_PREFIX } from "../constants"; +export interface EditableCodeProps extends TypeWidgetProps { + // if true, the update will be debounced to prevent excessive updates. Especially useful if the editor is linked to a live preview. + debounceUpdate?: boolean; + lineWrapping?: boolean; + updateInterval?: number; + onContentChanged?: (content: string) => void; +} + export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent }: TypeWidgetProps) { const [ content, setContent ] = useState(""); const blob = useNoteBlob(note); @@ -35,12 +43,7 @@ export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent }: TypeWi ) } -export function EditableCode({ note, ntxId, debounceUpdate, parentComponent, updateInterval, ...editorProps }: TypeWidgetProps & { - // if true, the update will be debounced to prevent excessive updates. Especially useful if the editor is linked to a live preview. - debounceUpdate?: boolean; - lineWrapping?: boolean; - updateInterval?: number; -}) { +export function EditableCode({ note, ntxId, debounceUpdate, parentComponent, updateInterval, onContentChanged, ...editorProps }: EditableCodeProps) { const editorRef = useRef(null); const containerRef = useRef(null); const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled"); @@ -78,6 +81,9 @@ export function EditableCode({ note, ntxId, debounceUpdate, parentComponent, upd spacedUpdate.resetUpdateTimer(); } spacedUpdate.scheduleUpdate(); + if (editorRef.current && onContentChanged) { + onContentChanged(editorRef.current.getText()); + } }} {...editorProps} /> diff --git a/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css b/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css index 9ba4e444b..bfa9f9824 100644 --- a/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css +++ b/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css @@ -77,4 +77,8 @@ .note-detail-split.split-read-only .note-detail-split-preview-col { width: 100%; -} \ No newline at end of file +} + +/* #region SVG */ + +/* #endregion */ \ No newline at end of file diff --git a/apps/client/src/widgets/type_widgets/helpers/SplitEditor.tsx b/apps/client/src/widgets/type_widgets/helpers/SplitEditor.tsx index 74f954531..42d0d5ecf 100644 --- a/apps/client/src/widgets/type_widgets/helpers/SplitEditor.tsx +++ b/apps/client/src/widgets/type_widgets/helpers/SplitEditor.tsx @@ -2,17 +2,17 @@ import { useEffect, useRef } from "preact/hooks"; import utils, { isMobile } from "../../../services/utils"; import Admonition from "../../react/Admonition"; import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks"; -import { TypeWidgetProps } from "../type_widget"; import "./SplitEditor.css"; import Split from "split.js"; import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer"; -import { EditableCode } from "../code/Code"; +import { EditableCode, EditableCodeProps } from "../code/Code"; import { ComponentChildren } from "preact"; import ActionButton, { ActionButtonProps } from "../../react/ActionButton"; -export interface SplitEditorProps extends TypeWidgetProps { +export interface SplitEditorProps extends EditableCodeProps { error?: string | null; splitOptions?: Split.Options; + previewContent: ComponentChildren; previewButtons?: ComponentChildren; } @@ -25,7 +25,7 @@ export interface SplitEditorProps extends TypeWidgetProps { * - Can display errors to the user via {@link setError}. * - Horizontal or vertical orientation for the editor/preview split, adjustable via the switch split orientation button floating button. */ -export default function SplitEditor({ note, error, splitOptions, previewButtons, ...editorProps }: SplitEditorProps) { +export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, ...editorProps }: SplitEditorProps) { const splitEditorOrientation = useSplitOrientation(); const [ readOnly ] = useNoteLabelBoolean(note, "readOnly"); const containerRef = useRef(null); @@ -48,7 +48,9 @@ export default function SplitEditor({ note, error, splitOptions, previewButtons, const preview = (
-
Preview goes here
+
+ {previewContent} +
{previewButtons}
diff --git a/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx b/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx index a1362313a..8f45d806c 100644 --- a/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx +++ b/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx @@ -1,10 +1,43 @@ +import { useState } from "preact/hooks"; import { t } from "../../../services/i18n"; import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor"; +import { RawHtmlBlock } from "../../react/RawHtml"; + +interface SvgSplitEditorProps extends Omit { + /** + * Called upon when the SVG preview needs refreshing, such as when the editor has switched to a new note or the content has switched. + * + * The method must return a valid SVG string that will be automatically displayed in the preview. + * + * @param content the content of the note, in plain text. + */ + renderSvg(content: string): string | Promise; +} + +export default function SvgSplitEditor({ renderSvg, ...props }: SvgSplitEditorProps) { + const [ svg, setSvg ] = useState(); + const [ error, setError ] = useState(); + + async function onContentChanged(content: string) { + try { + const svg = await renderSvg(content); + + // Rendering was successful. + setError(null); + setSvg(svg); + } catch (e) { + // Rendering failed. + setError((e as Error)?.message); + } + } -export default function SvgSplitEditor(props: SplitEditorProps) { return ( + )} previewButtons={ <> ; - /** * Called to obtain the name of the note attachment (without .svg extension) that will be used for storing the preview. */ diff --git a/apps/client/src/widgets/type_widgets_old/mermaid.ts b/apps/client/src/widgets/type_widgets_old/mermaid.ts index c8642c993..f3ef73abe 100644 --- a/apps/client/src/widgets/type_widgets_old/mermaid.ts +++ b/apps/client/src/widgets/type_widgets_old/mermaid.ts @@ -2,8 +2,6 @@ import type { EditorConfig } from "@triliumnext/codemirror"; import { getMermaidConfig, loadElkIfNeeded, postprocessMermaidSvg } from "../../services/mermaid.js"; import AbstractSvgSplitTypeWidget from "./abstract_svg_split_type_widget.js"; -let idCounter = 1; -let registeredErrorReporter = false; export class MermaidTypeWidget extends AbstractSvgSplitTypeWidget { @@ -16,21 +14,7 @@ export class MermaidTypeWidget extends AbstractSvgSplitTypeWidget { } async renderSvg(content: string) { - const mermaid = (await import("mermaid")).default; - await loadElkIfNeeded(mermaid, content); - if (!registeredErrorReporter) { - // (await import("./linters/mermaid.js")).default(); - registeredErrorReporter = true; - } - mermaid.initialize({ - startOnLoad: false, - ...(getMermaidConfig() as any), - }); - - idCounter++; - const { svg } = await mermaid.render(`mermaid-graph-${idCounter}`, content); - return postprocessMermaidSvg(svg); } }