GraphVizEngineJs.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2025, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 *
 * If you like this project or if you find it useful, you can support us at:
 *
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 *
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques
 *
 */
package net.sourceforge.plantuml.teavm.js;

import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject;

import net.sourceforge.plantuml.teavm.js.browser.PendingResources;
import net.sourceforge.plantuml.teavm.js.browser.ScriptLoaderJs;

/**
 * Renders DOT graphs to SVG with Viz.js, synchronously.
 * <p>
 * In {@code @viz-js/viz} 3.x, only {@code Viz.instance()} is asynchronous (it
 * loads the WebAssembly module); {@code renderString} on the instance is
 * synchronous. So the instance is created once, from JavaScript
 * ({@link #prepare(ScriptLoaderJs.Ok)}), and kept on the global object as
 * {@code __plantumlVizInstance}; rendering then just calls it. Nothing waits
 * in Java: when the instance is not ready yet, rendering notes it as missing
 * and fails, and the diagram is rendered again once it is ready (see
 * {@link PendingResources}).
 * <p>
 * Usage:
 * <pre>
 * String svg = GraphVizEngineJs.renderDotToSvg("digraph G { A -> B; }");
 * </pre>
 */
public class GraphVizEngineJs {

	/**
	 * Where the Viz.js object comes from. A host that bundles viz-global.js (a UMD)
	 * into its own file does not get a global {@code Viz}: the UMD attaches it to
	 * the bundle's exports. Such a host sets {@code globalThis.PLANTUML_VIZ = Viz}
	 * itself; it takes precedence over the global {@code Viz} that loading
	 * viz-global.js with a script tag defines.
	 */
	private static final String FIND_VIZ_STMT = "var V = globalThis.PLANTUML_VIZ || (typeof Viz !== 'undefined' ? Viz : null);";

	private static final String NOT_LOADED = "Viz is not loaded: this diagram type needs the Graphviz layout engine (viz-global.js). Load viz-global.js before rendering.";

	// ::remove file when JAVA8

	/**
	 * True when the Viz.js bridge (viz-global.js) is NOT loaded on the page,
	 * so the caller should fall back to the Smetana layout engine. Logs a
	 * one-time console note the first time the fallback is taken.
	 * <p>
	 * A Viz.js that is loaded but whose instance cannot be created (the
	 * WebAssembly module fails to load) is not a fallback: the diagram reports
	 * that failure, naming Viz.
	 */
	@JSBody(script = FIND_VIZ_STMT + "var missing = !V || typeof V.instance !== 'function';" +
			"if (missing && !globalThis.__plantumlSmetanaFallbackNoted) {" +
			"  globalThis.__plantumlSmetanaFallbackNoted = true;" +
			"  console.info('PlantUML: viz-global.js is not loaded, falling back to the Smetana layout engine');" +
			"}" +
			"return missing;")
	public static native boolean vizMissingFallback();

	/**
	 * Whether a layout can call {@link #renderDotToSvg(String)} now: the
	 * instance is there, or creating it failed (which {@code renderDotToSvg}
	 * then reports). When it is not there yet, it is noted as missing and this
	 * returns <code>false</code>: the attempt will be discarded and rendered
	 * again once the instance is ready, so the caller should stop at once rather
	 * than compute a layout for nothing.
	 */
	public static boolean isAvailable() {
		if (isReady() || getFailure(NOT_LOADED) != null)
			return true;
		PendingResources.requireViz();
		return false;
	}

	/**
	 * Forgets why creating the instance failed, so that the next request tries
	 * again: a network failure while loading the WebAssembly module may be
	 * transient. Called at the start of each request.
	 */
	@JSBody(script = "delete globalThis.__plantumlVizError;")
	public static native void forgetFailure();

	/**
	 * Renders a DOT graph description to SVG format, with the {@code dot}
	 * engine.
	 *
	 * @param dotSource the graph description in DOT format
	 * @return the rendered SVG as a string
	 * @throws RuntimeException when Viz.js is not loaded, failed to load, is
	 *                          still loading, or fails on this graph
	 */
	public static String renderDotToSvg(String dotSource) {
		return renderDotToSvg(dotSource, "dot");
	}

	/**
	 * Renders a DOT graph using a specific GraphViz engine.
	 *
	 * @param dotSource the graph description in DOT format
	 * @param engine    the GraphViz engine to use (dot, neato, fdp, sfdp, twopi, circo)
	 * @return the rendered SVG as a string
	 * @throws RuntimeException when Viz.js is not loaded, failed to load, is
	 *                          still loading, or fails on this graph
	 */
	public static String renderDotToSvg(String dotSource, String engine) {
		if (isReady() == false) {
			final String failure = getFailure(NOT_LOADED);
			if (failure != null)
				throw new RuntimeException(failure);
			// The instance is being created: this attempt is discarded, and the diagram
			// is rendered again once it is ready.
			PendingResources.requireViz();
			throw new RuntimeException("Viz is loading");
		}
		final JSObject result = renderString(dotSource, engine);
		final String error = getProperty(result, "error");
		if (error != null)
			throw new RuntimeException(error);
		return getProperty(result, "svg");
	}

	/** Whether the Viz.js instance has been created. */
	@JSBody(script = "return !!globalThis.__plantumlVizInstance;")
	private static native boolean isReady();

	/**
	 * Why there is no instance: Viz.js is not on the page, or creating the
	 * instance failed; <code>null</code> when it is being created, or was never
	 * asked for.
	 */
	@JSBody(params = "notLoaded", script = FIND_VIZ_STMT
			+ "if (!V || typeof V.instance !== 'function') return notLoaded;"
			+ "var e = globalThis.__plantumlVizError;"
			+ "return (typeof e === 'string') ? e : null;")
	private static native String getFailure(String notLoaded);

	/**
	 * Creates the Viz.js instance, once, and calls <code>done</code> when it is
	 * there or when creating it failed (the reason is then kept as
	 * {@code __plantumlVizError}, until the next request: see
	 * {@link #forgetFailure()}), or at once when there is nothing to wait for.
	 * Concurrent calls share the same creation.
	 */
	@JSBody(params = "done", script = FIND_VIZ_STMT
			+ "var g = globalThis;"
			+ "if (g.__plantumlVizInstance || typeof g.__plantumlVizError === 'string'"
			+ "    || !V || typeof V.instance !== 'function') { done(); return; }"
			+ "if (g.__plantumlVizWaiting) { g.__plantumlVizWaiting.push(done); return; }"
			+ "g.__plantumlVizWaiting = [done];"
			+ "var flush = function() {"
			+ "  var list = g.__plantumlVizWaiting; g.__plantumlVizWaiting = null;"
			+ "  for (var i = 0; i < list.length; i++) list[i]();"
			+ "};"
			+ "try {"
			+ "  Promise.resolve(V.instance()).then(function(viz) {"
			+ "    if (viz && typeof viz.renderString === 'function') g.__plantumlVizInstance = viz;"
			+ "    else g.__plantumlVizError = 'Viz.instance() did not give a Viz instance';"
			+ "    flush();"
			+ "  }, function(err) {"
			+ "    g.__plantumlVizError = String(err);"
			+ "    flush();"
			+ "  });"
			+ "} catch (e) {"
			+ "  g.__plantumlVizError = String(e);"
			+ "  flush();"
			+ "}")
	public static native void prepare(ScriptLoaderJs.Ok done);

	/**
	 * Renders with the instance, synchronously. Viz.js throws on a graph it
	 * cannot render; the exception is caught here, so that nothing thrown by
	 * JavaScript crosses into Java, and returned as {@code error}.
	 */
	@JSBody(params = { "dotSource", "engine" }, script = "try {"
			+ "  return { svg: globalThis.__plantumlVizInstance.renderString(dotSource, { format: 'svg', engine: engine }) };"
			+ "} catch (e) {"
			+ "  return { error: String(e) };"
			+ "}")
	private static native JSObject renderString(String dotSource, String engine);

	@JSBody(params = { "obj", "key" }, script = "var v = obj[key]; return (typeof v === 'string') ? v : null;")
	private static native String getProperty(JSObject obj, String key);

}