Skip to content

Commit 0ef52f5

Browse files
committed
Add standalone PlantUML preview support
1 parent dc43000 commit 0ef52f5

11 files changed

Lines changed: 308 additions & 195 deletions

FUNCTIONAL_MANUAL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ The **General** settings category includes a **PlantUML Rendering** selector. Ch
150150

151151
If the Java runtime is unavailable, DocForge will report the error in the preview and you can switch back to remote rendering at any time.
152152

153+
The chosen rendering mode is used for PlantUML code blocks inside Markdown documents *and* for standalone `.puml` documents rendered through the dedicated PlantUML previewer.
154+
153155
### Logger Panel
154156

155157
Accessed via the terminal icon in the title bar, this panel is your primary tool for debugging and monitoring application activity.

TECHNICAL_MANUAL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ This system provides a consistent and extensible editing experience for all docu
9494
- **`CodeEditor.tsx`:** A React component that wraps and configures the Monaco Editor instance. It's responsible for managing the editor's content, theme, and language for syntax highlighting based on props.
9595
- **`PreviewPane.tsx`:** This component is responsible for displaying the rendered output of a document. It debounces content updates for performance and uses the `PreviewService` to get the correct output.
9696
- **`services/previewService.ts`:** This service acts as a registry for all available renderer "plugins." It exposes a method, `getRendererForLanguage()`, which finds and returns the appropriate renderer for a given language ID (e.g., 'markdown').
97-
- **Renderer Plugins (`services/preview/`):** Each file format with a preview is supported by a dedicated renderer class that implements the `IRenderer` interface. This makes the system highly extensible: to support a new format, one only needs to create a new renderer class and add it to the `previewService` registry. Currently, renderers for Markdown, HTML, and plaintext (fallback) are implemented.
98-
- The Markdown renderer now integrates an offline PlantUML path. When users select the offline mode, the renderer invokes the main-process `node-plantuml` bridge to generate SVG output locally; otherwise it falls back to the remote plantuml.com service.
97+
- **Renderer Plugins (`services/preview/`):** Each file format with a preview is supported by a dedicated renderer class that implements the `IRenderer` interface. This makes the system highly extensible: to support a new format, one only needs to create a new renderer class and add it to the `previewService` registry. The bundled plugins cover Markdown (with Mermaid + PlantUML support), standalone PlantUML documents, HTML, PDFs, common image formats, and a plaintext fallback renderer.
98+
- Both the Markdown renderer and the standalone PlantUML renderer share the `PlantUMLDiagram` component, which routes diagrams through either the remote plantuml.com server or the offline `node-plantuml` IPC bridge depending on the active setting.
9999

100100
### LLM Service (`services/llmService.ts`)
101101

components/PromptEditor.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ interface DocumentEditorProps {
2929
const PREVIEWABLE_LANGUAGES = new Set<string>([
3030
'markdown',
3131
'html',
32+
'plantuml',
33+
'puml',
34+
'uml',
3235
'pdf',
3336
'application/pdf',
3437
'image',
@@ -62,6 +65,9 @@ const resolveDefaultViewMode = (mode: ViewMode | null | undefined, languageHint:
6265
if (normalizedHint === 'image' || normalizedHint.startsWith('image/')) {
6366
return 'preview';
6467
}
68+
if (normalizedHint === 'plantuml' || normalizedHint === 'puml' || normalizedHint === 'uml') {
69+
return 'preview';
70+
}
6571
return 'edit';
6672
};
6773

docs/FUNCTIONAL_MANUAL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ The **General** settings category includes a **PlantUML Rendering** selector. Ch
150150

151151
If the Java runtime is unavailable, DocForge will report the error in the preview and you can switch back to remote rendering at any time.
152152

153+
The chosen rendering mode is used for PlantUML code blocks inside Markdown documents *and* for standalone `.puml` documents rendered through the dedicated PlantUML previewer.
154+
153155
### Logger Panel
154156

155157
Accessed via the terminal icon in the title bar, this panel is your primary tool for debugging and monitoring application activity.

docs/TECHNICAL_MANUAL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ This system provides a consistent and extensible editing experience for all docu
9494
- **`CodeEditor.tsx`:** A React component that wraps and configures the Monaco Editor instance. It's responsible for managing the editor's content, theme, and language for syntax highlighting based on props.
9595
- **`PreviewPane.tsx`:** This component is responsible for displaying the rendered output of a document. It debounces content updates for performance and uses the `PreviewService` to get the correct output.
9696
- **`services/previewService.ts`:** This service acts as a registry for all available renderer "plugins." It exposes a method, `getRendererForLanguage()`, which finds and returns the appropriate renderer for a given language ID (e.g., 'markdown').
97-
- **Renderer Plugins (`services/preview/`):** Each file format with a preview is supported by a dedicated renderer class that implements the `IRenderer` interface. This makes the system highly extensible: to support a new format, one only needs to create a new renderer class and add it to the `previewService` registry. Currently, renderers for Markdown, HTML, and plaintext (fallback) are implemented.
98-
- The Markdown renderer now integrates an offline PlantUML path. When users select the offline mode, the renderer invokes the main-process `node-plantuml` bridge to generate SVG output locally; otherwise it falls back to the remote plantuml.com service.
97+
- **Renderer Plugins (`services/preview/`):** Each file format with a preview is supported by a dedicated renderer class that implements the `IRenderer` interface. This makes the system highly extensible: to support a new format, one only needs to create a new renderer class and add it to the `previewService` registry. The bundled plugins cover Markdown (with Mermaid + PlantUML support), standalone PlantUML documents, HTML, PDFs, common image formats, and a plaintext fallback renderer.
98+
- Both the Markdown renderer and the standalone PlantUML renderer share the `PlantUMLDiagram` component, which routes diagrams through either the remote plantuml.com server or the offline `node-plantuml` IPC bridge depending on the active setting.
9999

100100
### LLM Service (`services/llmService.ts`)
101101

esbuild.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const buildOrWatch = async (name, config) => {
3434
platform: 'node',
3535
entryPoints: ['electron/main.ts'],
3636
outfile: 'dist/main.js',
37-
external: ['electron', 'better-sqlite3'],
37+
external: ['electron', 'better-sqlite3', 'node-plantuml'],
3838
}),
3939
buildOrWatch('preload', {
4040
...sharedConfig,

services/languageService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const SUPPORTED_LANGUAGES = [
99
{ id: 'css', label: 'CSS' },
1010
{ id: 'json', label: 'JSON' },
1111
{ id: 'markdown', label: 'Markdown' },
12+
{ id: 'plantuml', label: 'PlantUML' },
1213
{ id: 'java', label: 'Java' },
1314
{ id: 'csharp', label: 'C#' },
1415
{ id: 'cpp', label: 'C++' },
@@ -46,6 +47,9 @@ export const mapExtensionToLanguageId = (extension: string | null): string => {
4647
case 'md':
4748
case 'markdown':
4849
return 'markdown';
50+
case 'puml':
51+
case 'plantuml':
52+
return 'plantuml';
4953
case 'java':
5054
return 'java';
5155
case 'cs':

services/preview/markdownRenderer.tsx

Lines changed: 1 addition & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import rehypeKatex from 'rehype-katex';
88
import type { Components } from 'react-markdown';
99
import type { Highlighter } from 'shiki';
1010
import mermaid from 'mermaid';
11-
import plantumlEncoder from 'plantuml-encoder';
1211
import type { IRenderer } from './IRenderer';
1312
import type { LogLevel, Settings } from '../../types';
1413
import { DEFAULT_SETTINGS } from '../../constants';
1514
import { useTheme } from '../../hooks/useTheme';
1615
import { getSharedHighlighter } from './shikiHighlighter';
16+
import { PlantUMLDiagram, PLANTUML_LANGS } from './plantumlDiagram';
1717

1818
import 'katex/dist/katex.min.css';
1919

@@ -101,195 +101,6 @@ const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ code, theme }) => {
101101
);
102102
};
103103

104-
interface PlantUMLDiagramProps {
105-
code: string;
106-
mode: Settings['plantumlRendererMode'];
107-
}
108-
109-
const PLANTUML_LANGS = ['plantuml', 'puml', 'uml'];
110-
const PLANTUML_SERVER = 'https://www.plantuml.com/plantuml/svg';
111-
112-
interface PlantUMLErrorProps {
113-
message: string;
114-
details?: string | null;
115-
}
116-
117-
const PlantUMLError: React.FC<PlantUMLErrorProps> = ({ message, details }) => (
118-
<div className="df-plantuml" role="alert">
119-
<div className="df-plantuml-error">
120-
<div className="df-plantuml-error__message">{message}</div>
121-
{details && details.trim() && (
122-
<details className="df-plantuml-error__details">
123-
<summary>Technical details</summary>
124-
<code>{details}</code>
125-
</details>
126-
)}
127-
</div>
128-
</div>
129-
);
130-
131-
const PlantUMLRemoteDiagram: React.FC<{ code: string }> = ({ code }) => {
132-
const { encoded, reason, error } = useMemo(() => {
133-
const trimmed = code.trim();
134-
if (!trimmed) {
135-
return { encoded: null, reason: 'empty' as const, error: 'The PlantUML code block is empty.' };
136-
}
137-
try {
138-
return { encoded: plantumlEncoder.encode(trimmed), reason: 'ok' as const, error: null };
139-
} catch (err) {
140-
return {
141-
encoded: null,
142-
reason: 'encode-error' as const,
143-
error: err instanceof Error ? err.message : String(err),
144-
};
145-
}
146-
}, [code]);
147-
148-
const [hasError, setHasError] = useState(false);
149-
const [errorDetails, setErrorDetails] = useState<string | null>(error);
150-
151-
useEffect(() => {
152-
setHasError(false);
153-
setErrorDetails(error);
154-
}, [error, encoded, code]);
155-
156-
if (!encoded) {
157-
const message =
158-
reason === 'empty'
159-
? 'PlantUML diagram is empty.'
160-
: 'Unable to encode PlantUML diagram.';
161-
return <PlantUMLError message={message} details={errorDetails} />;
162-
}
163-
164-
if (hasError) {
165-
return (
166-
<PlantUMLError
167-
message="Failed to load PlantUML diagram from remote server."
168-
details={errorDetails ?? `Request URL: ${PLANTUML_SERVER}/${encoded}`}
169-
/>
170-
);
171-
}
172-
173-
return (
174-
<div className="df-plantuml">
175-
<img
176-
src={`${PLANTUML_SERVER}/${encoded}`}
177-
alt="PlantUML diagram"
178-
loading="lazy"
179-
onError={() => {
180-
setHasError(true);
181-
setErrorDetails(`Request URL: ${PLANTUML_SERVER}/${encoded}`);
182-
}}
183-
/>
184-
</div>
185-
);
186-
};
187-
188-
interface OfflineRenderState {
189-
status: 'idle' | 'loading' | 'success' | 'error';
190-
svg?: string;
191-
error?: string;
192-
details?: string | null;
193-
}
194-
195-
const PlantUMLOfflineDiagram: React.FC<{ code: string }> = ({ code }) => {
196-
const [state, setState] = useState<OfflineRenderState>({ status: 'idle' });
197-
198-
useEffect(() => {
199-
let cancelled = false;
200-
const trimmed = code.trim();
201-
202-
if (!trimmed) {
203-
setState({ status: 'error', error: 'PlantUML diagram is empty.', details: null });
204-
return () => {
205-
cancelled = true;
206-
};
207-
}
208-
209-
if (typeof window === 'undefined' || !window.electronAPI?.renderPlantUML) {
210-
setState({
211-
status: 'error',
212-
error: 'Local PlantUML renderer is not available in this environment.',
213-
details: 'Switch to remote rendering or run the desktop app with a Java runtime installed.',
214-
});
215-
return () => {
216-
cancelled = true;
217-
};
218-
}
219-
220-
setState({ status: 'loading' });
221-
222-
window.electronAPI
223-
.renderPlantUML(trimmed, 'svg')
224-
.then((result) => {
225-
if (cancelled) {
226-
return;
227-
}
228-
if (result?.success && result.svg) {
229-
setState({ status: 'success', svg: result.svg });
230-
} else {
231-
setState({
232-
status: 'error',
233-
error: result?.error || 'The local PlantUML renderer returned no output.',
234-
details: result?.details ?? null,
235-
});
236-
}
237-
})
238-
.catch((err) => {
239-
if (cancelled) {
240-
return;
241-
}
242-
const details = err instanceof Error ? err.message : String(err);
243-
setState({
244-
status: 'error',
245-
error: 'Unable to render PlantUML diagram locally.',
246-
details,
247-
});
248-
});
249-
250-
return () => {
251-
cancelled = true;
252-
};
253-
}, [code]);
254-
255-
if (state.status === 'loading' || state.status === 'idle') {
256-
return (
257-
<div className="df-plantuml">
258-
<div className="df-plantuml-loading">Rendering diagram locally...</div>
259-
</div>
260-
);
261-
}
262-
263-
if (state.status === 'error') {
264-
return <PlantUMLError message={state.error ?? 'Unable to render PlantUML diagram locally.'} details={state.details} />;
265-
}
266-
267-
if (state.status === 'success' && state.svg) {
268-
return (
269-
<div
270-
className="df-plantuml"
271-
role="img"
272-
aria-label="PlantUML diagram"
273-
dangerouslySetInnerHTML={{ __html: state.svg }}
274-
/>
275-
);
276-
}
277-
278-
return (
279-
<PlantUMLError
280-
message="Local PlantUML renderer did not return any SVG output."
281-
details={state.details}
282-
/>
283-
);
284-
};
285-
286-
const PlantUMLDiagram: React.FC<PlantUMLDiagramProps> = ({ code, mode }) => {
287-
if (mode === 'offline') {
288-
return <PlantUMLOfflineDiagram code={code} />;
289-
}
290-
return <PlantUMLRemoteDiagram code={code} />;
291-
};
292-
293104
const MarkdownViewer = forwardRef<HTMLDivElement, MarkdownViewerProps>(({ content, settings, onScroll }, ref) => {
294105
const { theme } = useTheme();
295106
const viewTheme: 'light' | 'dark' = theme === 'dark' ? 'dark' : 'light';

0 commit comments

Comments
 (0)