Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions site/src/pages/docs.astro
Original file line number Diff line number Diff line change
Expand Up @@ -819,9 +819,15 @@ flowchart TD
input: <span class="t-kw">string</span>,
target: <span class="t-kw">HTMLElement</span>,
options?: <span class="t-kw">FloraOptions</span>
): &#123; warnings: <span class="t-kw">ParseWarning</span>[] &#125;</code></pre>
): &#123; warnings: <span class="t-kw">ParseWarning</span>[]; error?: <span class="t-kw">Error</span> &#125;</code></pre>
</div>
<p>This is the primary API. It handles the full pipeline and attaches an interactive SVG to the target element. Subgraph collapse/expand is wired up automatically.</p>
<p>
<code>render()</code> never leaves the target element empty. If layout or rendering throws
internally, it shows an error-state SVG and returns the caught <code>error</code> instead of
propagating it. (With <code>strict: true</code>, parse errors still throw
<code>FloraParseError</code> before rendering starts.)
</p>
<pre><code><span class="t-kw">import</span> &#123; render &#125; <span class="t-kw">from</span> <span class="t-str">"@topspinj/flora"</span>;

<span class="t-kw">const</span> &#123; warnings &#125; = <span class="t-fn">render</span>(<span class="t-str">`flowchart LR
Expand Down Expand Up @@ -1072,7 +1078,7 @@ console.<span class="t-fn">log</span>(ast.edges);
</thead>
<tbody>
<tr><td><code>FloraOptions</code></td><td>Options passed to <code>render()</code>, <code>toSVGElement()</code>, etc.</td></tr>
<tr><td><code>RenderResult</code></td><td>Return type of <code>render()</code> &mdash; <code>&#123; warnings, unsupportedType? &#125;</code></td></tr>
<tr><td><code>RenderResult</code></td><td>Return type of <code>render()</code> &mdash; <code>&#123; warnings, unsupportedType?, error? &#125;</code></td></tr>
<tr><td><code>ParseResult</code></td><td>Return type of <code>toAST()</code> &mdash; <code>&#123; ast, warnings &#125;</code></td></tr>
<tr><td><code>ParseWarning</code></td><td>A structured diagnostic: <code>&#123; line, col, message, severity &#125;</code></td></tr>
<tr><td><code>ParseWarningSeverity</code></td><td><code>"error" | "info"</code> &mdash; see <a href="#fault-tolerance">Error Handling</a></td></tr>
Expand Down
55 changes: 48 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,38 @@ function renderParseFailureSVGString(warnings: ParseWarning[], theme: FloraTheme
+ `</svg>`;
}

function renderInternalErrorSVG(error: Error, theme: FloraTheme): SVGSVGElement {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("width", "100%");
svg.setAttribute("height", "100%");
svg.setAttribute("viewBox", "0 0 560 100");
svg.style.background = theme.background;

const title = document.createElementNS("http://www.w3.org/2000/svg", "text");
title.setAttribute("x", "280");
title.setAttribute("y", "42");
title.setAttribute("text-anchor", "middle");
title.setAttribute("font-family", theme.fontFamily);
title.setAttribute("font-size", String(theme.fontSize));
title.setAttribute("font-weight", "600");
title.setAttribute("fill", theme.nodeColors.text);
title.textContent = "Diagram failed to render";
svg.appendChild(title);

const detail = document.createElementNS("http://www.w3.org/2000/svg", "text");
detail.setAttribute("x", "280");
detail.setAttribute("y", "68");
detail.setAttribute("text-anchor", "middle");
detail.setAttribute("font-family", theme.fontFamily);
detail.setAttribute("font-size", String(theme.fontSize - 2));
detail.setAttribute("fill", theme.edgeColors.label);
detail.textContent = error.message;
svg.appendChild(detail);

return svg;
}

function isParseFailure(ast: DiagramAST, warnings: ParseWarning[]): boolean {
return (
ast.type === "flowchart" &&
Expand All @@ -125,6 +157,8 @@ function isParseFailure(ast: DiagramAST, warnings: ParseWarning[]): boolean {
export interface RenderResult {
warnings: ParseWarning[];
unsupportedType?: string;
/** Set when layout or rendering threw internally; an error-state SVG is shown instead. */
error?: Error;
}

export function render(input: string, target: HTMLElement, options: FloraOptions = {}): RenderResult {
Expand All @@ -145,13 +179,20 @@ export function render(input: string, target: HTMLElement, options: FloraOptions
return { warnings };
}

const layout = computeLayout(ast, theme);
const svg = renderSVG(layout, options);

target.innerHTML = "";
target.appendChild(svg);

return { warnings };
// Never leave the target blank: fault tolerance in the parser is no help
// if an internal layout/render bug throws halfway through.
try {
const layout = computeLayout(ast, theme);
const svg = renderSVG(layout, options);
target.innerHTML = "";
target.appendChild(svg);
return { warnings };
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown));
target.innerHTML = "";
target.appendChild(renderInternalErrorSVG(error, theme));
return { warnings, error };
}
}

export function toAST(input: string, options: { strict?: boolean } = {}): { ast: DiagramAST; warnings: ParseWarning[] } {
Expand Down
34 changes: 34 additions & 0 deletions tests/renderer/error-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @vitest-environment jsdom
import { describe, it, expect, vi } from "vitest";

vi.mock("../../src/layout/index.js", () => ({
computeLayout: () => {
throw new Error("forced layout failure");
},
}));

import { render } from "../../src/index.js";

describe("render() error boundary", () => {
it("renders an error-state SVG instead of throwing when layout fails", () => {
const target = document.createElement("div");

const result = render(`flowchart LR\n A --> B`, target);

expect(result.error).toBeInstanceOf(Error);
expect(result.error!.message).toBe("forced layout failure");
const svg = target.querySelector("svg");
expect(svg).not.toBeNull();
expect(svg!.textContent).toContain("Diagram failed to render");
});

it("still returns parse warnings alongside the internal error", () => {
const target = document.createElement("div");

const result = render(`flowchart LR\n A --> B\n style A fill:#f9f`, target);

expect(result.error).toBeInstanceOf(Error);
expect(result.warnings.some((w) => w.severity === "info")).toBe(true);
expect(target.querySelector("svg")).not.toBeNull();
});
});
Loading