diff --git a/README.md b/README.md
index 6b843ad29..b40ffa990 100644
--- a/README.md
+++ b/README.md
@@ -256,7 +256,7 @@ OfficeCLI is self-contained. The capabilities below ship inside the binary — *
#### Rendering engine — high-fidelity, built-in
-OfficeCLI's keystone: a from-scratch, high-fidelity HTML rendering engine that lets an AI agent *see* the rendered document instead of guessing from the DOM. It covers shapes, charts (trendlines, error bars, waterfall, candlestick, sparklines), equations (OMML → LaTeX, rendered with KaTeX), 3D `.glb` models via Three.js, morph transitions, slide zoom, and shape effects. Per-page PNG screenshots are produced by piping the rendered HTML through a headless browser. Three modes:
+OfficeCLI's keystone: a from-scratch, high-fidelity HTML rendering engine that lets an AI agent *see* the rendered document instead of guessing from the DOM. It covers shapes, charts (trendlines, error bars, waterfall, candlestick, sparklines), equations (OMML → LaTeX, rendered with KaTeX), 3D `.glb` models via Three.js, morph transitions, slide zoom, and shape effects. Per-page PNG screenshots are produced by piping the rendered HTML through a headless browser. `diagram --prop render=image` renders with the real mermaid.js in that same browser, caching mermaid.min.js locally on first use (mirror → CDN); set `OFFICECLI_MERMAID_JS=/path/to/mermaid.min.js` to point it at a local copy instead and skip the network entirely (offline / sandboxed hosts). Three modes:
- **`view html`** — standalone HTML file, assets inlined. Open in any browser.
- **`view screenshot`** — per-page PNG, ready for multimodal agents to read.
diff --git a/src/officecli/Core/Diagram/MermaidImageRenderer.cs b/src/officecli/Core/Diagram/MermaidImageRenderer.cs
index d766672f2..7f5c21b24 100644
--- a/src/officecli/Core/Diagram/MermaidImageRenderer.cs
+++ b/src/officecli/Core/Diagram/MermaidImageRenderer.cs
@@ -23,7 +23,9 @@ namespace OfficeCli.Core.Diagram;
/// - Chrome-family browser the user already has (via
/// ): render mermaid.js in a page and screenshot it.
/// Only mermaid.min.js (~3.5 MB) is fetched to a local cache on first use
-/// (mirror → CDN); if that fails the page loads mermaid from the CDN live.
+/// (mirror → CDN); if that fails the page loads mermaid from the CDN live.
+/// OFFICECLI_MERMAID_JS points at a local mermaid.min.js and skips the
+/// network entirely, for offline / sandboxed hosts.
/// - otherwise the caller falls back to the native synthesizer
/// () — zero dependencies, fully editable shapes.
///
@@ -180,6 +182,18 @@ private static bool SourceNeedsElk(string source)
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".officecli", "cache");
private static string CachedJsPath => Path.Combine(CacheDir, $"mermaid-{MermaidVersion}.min.js");
+ /// Explicit local mermaid.min.js override for offline / sandboxed hosts
+ /// that cannot reach the mirror or the CDN. Wins over the cache and skips the
+ /// download + refresh path entirely.
+ private static string? MermaidJsOverride
+ {
+ get
+ {
+ var p = Environment.GetEnvironmentVariable("OFFICECLI_MERMAID_JS");
+ return !string.IsNullOrWhiteSpace(p) && File.Exists(p) ? p : null;
+ }
+ }
+
/// True when any image backend is available: mmdc, or a chrome-family browser.
public static bool IsAvailable() => TryLocateMmdc(out _) || HtmlScreenshot.HasChromeFamily();
@@ -195,6 +209,7 @@ public static void RefreshCacheIfPresent()
{
try
{
+ if (MermaidJsOverride != null) return; // explicit local asset — never touch the network
if (!File.Exists(CachedJsPath)) return; // refresh only what the user actually uses
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
using var req = new HttpRequestMessage(HttpMethod.Get, MirrorUrl);
@@ -396,6 +411,9 @@ private static string RenderViaChrome(string mermaid, string? background)
/// <script src> (a file:// for a cached/downloaded copy, else the CDN).
private static string ResolveMermaidJsRef()
{
+ if (MermaidJsOverride is { } overridePath)
+ return new Uri(Path.GetFullPath(overridePath)).AbsoluteUri;
+
try
{
if (File.Exists(CachedJsPath) && new FileInfo(CachedJsPath).Length > 500_000)