Skip to content

Commit 170c657

Browse files
authored
Merge pull request #117 from beNative/codex/fix-offline-rendering-issue-for-plantuml-6rwc3j
Assume bundled PlantUML jar instead of downloading
2 parents 1f2b073 + 2e20b71 commit 170c657

11 files changed

Lines changed: 106 additions & 122 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ assets/icon*.png
1515
assets/icon.ico
1616
assets/icon.icns
1717
assets/favicon.ico
18+
assets/plantuml/plantuml.jar
1819

1920
# Editor directories and files
2021
.vscode/*

FUNCTIONAL_MANUAL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ Accessed via the info icon in the title bar. This view contains tabs for reading
181181
The **General** settings category includes a **PlantUML Rendering** selector. Choose between:
182182

183183
- **Remote (plantuml.com):** Encodes the diagram and requests the SVG from the public PlantUML server.
184-
- **Offline (local renderer):** Invokes the bundled PlantUML engine inside the desktop application. This mode requires a local Java Runtime Environment and access to Graphviz (or the bundled `viz.js` assets) so the renderer can generate diagrams without contacting plantuml.com.
184+
- **Offline (local renderer):** Invokes the PlantUML jar bundled with the application (`assets/plantuml/plantuml.jar`) through the local Java Runtime Environment, letting the app render diagrams without contacting plantuml.com.
185185

186186
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.
187187

TECHNICAL_MANUAL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This document provides a technical overview of the DocForge application's archit
1414
- **Bundler:** [esbuild](https://esbuild.github.io/) for fast and efficient bundling of the application's source code.
1515
- **Styling:** [Tailwind CSS](https://tailwindcss.com/) for a utility-first CSS framework.
1616
- **Packaging:** [electron-builder](https://www.electron.build/) for creating distributable application packages.
17-
- **Diagram Rendering:** [PlantUML](https://plantuml.com/) via either the public plantuml.com service or the bundled [`node-plantuml`](https://www.npmjs.com/package/node-plantuml) renderer. Offline rendering requires a locally installed Java Runtime Environment and access to Graphviz (or the cached `viz.js` binary) so that diagrams can be rendered without network access.
17+
- **Diagram Rendering:** [PlantUML](https://plantuml.com/) via either the public plantuml.com service or a PlantUML jar bundled with the application (`assets/plantuml/plantuml.jar`). Offline rendering invokes the jar through the system Java Runtime Environment, so diagrams render without any network connectivity.
1818

1919
---
2020

@@ -95,7 +95,7 @@ This system provides a consistent and extensible editing experience for all docu
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').
9797
- **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.
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 Java-based IPC bridge depending on the active setting.
9999

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

assets/plantuml/.gitkeep

Whitespace-only changes.

docs/FUNCTIONAL_MANUAL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ Accessed via the info icon in the title bar. This view contains tabs for reading
181181
The **General** settings category includes a **PlantUML Rendering** selector. Choose between:
182182

183183
- **Remote (plantuml.com):** Encodes the diagram and requests the SVG from the public PlantUML server.
184-
- **Offline (local renderer):** Invokes the bundled PlantUML engine inside the desktop application. This mode requires a local Java Runtime Environment and access to Graphviz (or the bundled `viz.js` assets) so the renderer can generate diagrams without contacting plantuml.com.
184+
- **Offline (local renderer):** Invokes the PlantUML jar bundled with the application (`assets/plantuml/plantuml.jar`) through the local Java Runtime Environment, letting the app render diagrams without contacting plantuml.com.
185185

186186
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.
187187

docs/TECHNICAL_MANUAL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This document provides a technical overview of the DocForge application's archit
1414
- **Bundler:** [esbuild](https://esbuild.github.io/) for fast and efficient bundling of the application's source code.
1515
- **Styling:** [Tailwind CSS](https://tailwindcss.com/) for a utility-first CSS framework.
1616
- **Packaging:** [electron-builder](https://www.electron.build/) for creating distributable application packages.
17-
- **Diagram Rendering:** [PlantUML](https://plantuml.com/) via either the public plantuml.com service or the bundled [`node-plantuml`](https://www.npmjs.com/package/node-plantuml) renderer. Offline rendering requires a locally installed Java Runtime Environment and access to Graphviz (or the cached `viz.js` binary) so that diagrams can be rendered without network access.
17+
- **Diagram Rendering:** [PlantUML](https://plantuml.com/) via either the public plantuml.com service or a PlantUML jar bundled with the application (`assets/plantuml/plantuml.jar`). Offline rendering invokes the jar through the system Java Runtime Environment, so diagrams render without any network connectivity.
1818

1919
---
2020

@@ -95,7 +95,7 @@ This system provides a consistent and extensible editing experience for all docu
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').
9797
- **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.
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 Java-based IPC bridge depending on the active setting.
9999

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

electron/main.ts

Lines changed: 97 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as zlib from 'zlib';
1313
import * as os from 'os';
1414
import * as stream from 'stream';
1515
import { promisify } from 'util';
16-
import { generate as plantumlGenerate } from 'node-plantuml';
16+
import { spawn } from 'child_process';
1717

1818
// Fix: Inform TypeScript about the __dirname global variable provided by Node.js, which is present in a CommonJS-like environment.
1919
declare const __dirname: string;
@@ -435,58 +435,72 @@ ipcMain.handle('plantuml:render-svg', async (_, diagram: string, format: 'svg' =
435435
}
436436

437437
try {
438-
const generator = plantumlGenerate(trimmed, { format: 'svg' });
439-
generator.out.setEncoding('utf-8');
440-
generator.err.setEncoding('utf-8');
438+
const jarPath = await resolvePlantUmlJar();
441439

442440
return await new Promise<{ success: boolean; svg?: string; error?: string; details?: string }>((resolve) => {
441+
const child = spawn('java', ['-Djava.awt.headless=true', '-jar', jarPath, '-pipe', '-tsvg'], {
442+
stdio: ['pipe', 'pipe', 'pipe'],
443+
});
444+
443445
let svgOutput = '';
444446
let errorOutput = '';
447+
let resolved = false;
445448

446-
const cleanup = () => {
447-
generator.out.removeAllListeners();
448-
generator.err.removeAllListeners();
449+
const finalize = (payload: { success: boolean; svg?: string; error?: string; details?: string }) => {
450+
if (resolved) {
451+
return;
452+
}
453+
resolved = true;
454+
resolve(payload);
449455
};
450456

451-
const resolveWithError = (message: string) => {
452-
cleanup();
453-
resolve({
454-
success: false,
455-
error: message,
456-
details: errorOutput.trim() || undefined,
457-
});
458-
};
457+
child.stdout.setEncoding('utf-8');
458+
child.stderr.setEncoding('utf-8');
459+
460+
child.stdout.on('data', (chunk) => {
461+
svgOutput += chunk.toString();
462+
});
459463

460-
generator.err.on('data', (chunk) => {
464+
child.stderr.on('data', (chunk) => {
461465
errorOutput += chunk.toString();
462466
});
463467

464-
generator.out.on('data', (chunk) => {
465-
svgOutput += chunk.toString();
468+
child.on('error', (err) => {
469+
const message =
470+
err instanceof Error
471+
? err.message
472+
: 'Failed to start the local PlantUML renderer process.';
473+
finalize({
474+
success: false,
475+
error: message,
476+
details: errorOutput.trim() || undefined,
477+
});
466478
});
467479

468-
generator.out.on('end', () => {
469-
cleanup();
470-
if (svgOutput.trim()) {
471-
resolve({ success: true, svg: svgOutput });
472-
} else {
473-
resolve({
474-
success: false,
475-
error: 'PlantUML renderer produced no SVG output.',
476-
details: errorOutput.trim() || undefined,
477-
});
480+
child.on('close', (code) => {
481+
if (code === 0 && svgOutput.trim()) {
482+
finalize({ success: true, svg: svgOutput });
483+
return;
478484
}
479-
});
480485

481-
generator.out.on('error', (streamError) => {
482-
const message = streamError instanceof Error ? streamError.message : String(streamError);
483-
resolveWithError(message);
486+
const exitDetails = errorOutput.trim() || `Renderer exited with code ${code}.`;
487+
finalize({
488+
success: false,
489+
error: 'Local PlantUML renderer failed to produce output.',
490+
details: exitDetails,
491+
});
484492
});
485493

486-
generator.err.on('error', (streamError) => {
487-
const message = streamError instanceof Error ? streamError.message : String(streamError);
488-
resolveWithError(message);
494+
child.stdin.on('error', (err) => {
495+
const message = err instanceof Error ? err.message : String(err);
496+
finalize({
497+
success: false,
498+
error: 'Failed to send diagram to the PlantUML renderer.',
499+
details: message,
500+
});
489501
});
502+
503+
child.stdin.end(trimmed);
490504
});
491505
} catch (error) {
492506
const message = error instanceof Error ? error.message : String(error);
@@ -495,6 +509,54 @@ ipcMain.handle('plantuml:render-svg', async (_, diagram: string, format: 'svg' =
495509
}
496510
});
497511

512+
let cachedPlantumlJarPath: string | null = null;
513+
let plantumlJarLookupPromise: Promise<string> | null = null;
514+
515+
const PLANTUML_JAR_RELATIVE_PATH = path.join('assets', 'plantuml', 'plantuml.jar');
516+
517+
async function resolvePlantUmlJar(): Promise<string> {
518+
if (cachedPlantumlJarPath) {
519+
return cachedPlantumlJarPath;
520+
}
521+
if (plantumlJarLookupPromise) {
522+
return plantumlJarLookupPromise;
523+
}
524+
525+
const candidates = [
526+
path.join(app.getAppPath(), PLANTUML_JAR_RELATIVE_PATH),
527+
path.join(process.resourcesPath ?? '', PLANTUML_JAR_RELATIVE_PATH),
528+
path.join(__dirname, '..', PLANTUML_JAR_RELATIVE_PATH),
529+
path.join(__dirname, PLANTUML_JAR_RELATIVE_PATH),
530+
].reduce<string[]>((paths, candidate) => {
531+
const normalized = path.normalize(candidate);
532+
if (!paths.includes(normalized)) {
533+
paths.push(normalized);
534+
}
535+
return paths;
536+
}, []);
537+
538+
plantumlJarLookupPromise = (async () => {
539+
for (const candidate of candidates) {
540+
try {
541+
await fs.access(candidate);
542+
cachedPlantumlJarPath = candidate;
543+
return candidate;
544+
} catch {
545+
// Continue searching
546+
}
547+
}
548+
throw new Error(
549+
'Bundled PlantUML renderer is missing. Ensure assets/plantuml/plantuml.jar is included alongside the application.'
550+
);
551+
})();
552+
553+
try {
554+
return await plantumlJarLookupPromise;
555+
} finally {
556+
plantumlJarLookupPromise = null;
557+
}
558+
}
559+
498560
// Python environments & execution
499561
ipcMain.handle('python:list-envs', async () => {
500562
return pythonManager.listEnvironments();

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', 'node-plantuml'],
37+
external: ['electron', 'better-sqlite3'],
3838
}),
3939
buildOrWatch('preload', {
4040
...sharedConfig,

package-lock.json

Lines changed: 1 addition & 62 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
"electron-log": "^5.1.5",
3636
"electron-squirrel-startup": "^1.0.1",
3737
"electron-updater": "^6.2.1",
38-
"node-plantuml": "^0.9.0",
3938
"uuid": "^10.0.0"
4039
},
4140
"devDependencies": {

0 commit comments

Comments
 (0)