PlantUMLBrowser.java

package net.sourceforge.plantuml.teavm.browser;

// ::uncomment when __TEAVM__
//import org.teavm.jso.JSBody;
//import org.teavm.jso.JSFunctor;
//import org.teavm.jso.JSObject;
//import org.teavm.jso.dom.html.HTMLDocument;
//import org.teavm.jso.dom.html.HTMLElement;
//import org.teavm.jso.dom.xml.Element;
//
//import net.sourceforge.plantuml.FileFormat;
//import net.sourceforge.plantuml.FileFormatOption;
//import net.sourceforge.plantuml.core.Diagram;
//import net.sourceforge.plantuml.klimt.color.ColorMapper;
//import net.sourceforge.plantuml.klimt.color.HColor;
//import net.sourceforge.plantuml.klimt.color.HColors;
//import net.sourceforge.plantuml.klimt.font.StringBounder;
//import net.sourceforge.plantuml.teavm.PSystemBuilder2;
//import net.sourceforge.plantuml.teavm.StringBounderTeaVM;
//import net.sourceforge.plantuml.teavm.SvgGraphicsTeaVM;
//import net.sourceforge.plantuml.teavm.UGraphicTeaVM;
//:: done

/**
 * PlantUML rendering engine for browser environments, compiled to JavaScript via TeaVM.
 * 
 * <h2>Overview</h2>
 * 
 * This class provides a bridge between JavaScript code running in a browser and the
 * PlantUML Java rendering engine. It exposes a global JavaScript function
 * {@code plantumlRender(lines, elementId)} that web pages can call to render diagrams.
 * 
 * <h2>Architecture: JavaScript-driven paradigm</h2>
 * 
 * The design follows a "JS-driven" architecture where:
 * <ul>
 *   <li><b>JavaScript handles:</b> UI events, user input, debouncing, line splitting, 
 *       DOM element selection, and overall application flow</li>
 *   <li><b>Java handles:</b> PlantUML parsing and SVG rendering only</li>
 * </ul>
 * 
 * This separation keeps the Java code minimal and allows maximum flexibility for
 * web developers to integrate PlantUML however they want.
 * 
 * <h2>Why we need a worker thread</h2>
 * 
 * TeaVM compiles Java to JavaScript, but JavaScript is single-threaded and event-driven.
 * To support Java's synchronous blocking APIs (like {@code Thread.sleep()} or 
 * {@code Object.wait()}), TeaVM uses a coroutine-based approach that transforms
 * blocking calls into asynchronous JavaScript Promises.
 * 
 * <h3>The Viz.js constraint</h3>
 * 
 * PlantUML uses Viz.js (a JavaScript port of GraphViz) to render class diagrams,
 * component diagrams, and other diagrams that require graph layout. Viz.js has an
 * asynchronous API:
 * 
 * <pre>
 * Viz.instance().then(viz => viz.renderString(dot, options))
 * </pre>
 * 
 * Our {@code GraphVizjsTeaVMEngine} class uses TeaVM's {@code @Async} annotation to
 * make this async call appear synchronous to Java code. However, this only works
 * when called from a "TeaVM coroutine context" - essentially, from within a TeaVM
 * thread.
 * 
 * <h3>What happens without the worker thread</h3>
 * 
 * If JavaScript calls our render function directly (e.g., from a {@code setTimeout}
 * callback or an event listener), the call happens in a "native JS context", not a
 * TeaVM coroutine context. When the code reaches the Viz.js async call, TeaVM throws:
 * 
 * <pre>
 * Error: Suspension point reached from non-threading context
 * (perhaps, from native JS method).
 * See https://teavm.org/docs/runtime/coroutines.html
 * </pre>
 * 
 * <h3>The solution: a dedicated worker thread</h3>
 * 
 * We solve this by:
 * <ol>
 *   <li>Starting a background thread at initialization (in {@code main()})</li>
 *   <li>Having the JS-callable function just queue a render request and wake the thread</li>
 *   <li>The worker thread performs the actual rendering in the correct coroutine context</li>
 * </ol>
 * 
 * This pattern ensures all PlantUML rendering (including Viz.js calls) happens in a
 * context where TeaVM's async-to-sync transformation works correctly.
 * 
 * <h2>Usage from JavaScript</h2>
 * 
 * <pre>
 * // Initialize (call once when page loads)
 * main();
 * 
 * // Render a diagram
 * const source = "@startuml\nAlice -> Bob : hello\n@enduml";
 * const lines = source.split(/\r\n|\r|\n/);
 * plantumlRender(lines, "output-div-id");
 * </pre>
 * 
 * <h2>Thread safety</h2>
 * 
 * The class uses a simple producer-consumer pattern:
 * <ul>
 *   <li>Producer: {@code requestRender()} called from JS, sets pending request and notifies</li>
 *   <li>Consumer: {@code workerLoop()} waits for requests, processes them one at a time</li>
 * </ul>
 * 
 * If multiple render requests arrive while one is being processed, only the latest
 * request is kept (the previous pending request is overwritten). This is intentional:
 * when a user is typing, we only care about rendering the latest version.
 * 
 * @see net.sourceforge.plantuml.teavm.GraphVizjsTeaVMEngine
 */
public class PlantUMLBrowser {

	// ::uncomment when __TEAVM__

//    // =========================================================================
//    // Rendering configuration
//    // =========================================================================
//
//    private static final StringBounder STRING_BOUNDER = new StringBounderTeaVM();
//    private static final ColorMapper COLOR_MAPPER = ColorMapper.TEAVM;
//    private static final HColor BACK = HColors.WHITE;
//    private static final PSystemBuilder2 BUILDER = new PSystemBuilder2();
//
//    // =========================================================================
//    // Worker thread synchronization
//    // 
//    // We use a simple wait/notify pattern. The worker thread waits on LOCK until
//    // pendingLines becomes non-null, then processes the request and sets it back
//    // to null.
//    // =========================================================================
//
//    /** Lock object for synchronizing between JS requests and worker thread. */
//    private static final Object LOCK = new Object();
//
//    /** 
//     * The PlantUML source lines to render, or null if no request is pending.
//     * Set by requestRender(), cleared by workerLoop() after processing.
//     */
//    private static volatile String[] pendingLines;
//
//    /** The DOM element ID where the SVG should be inserted. */
//    private static volatile String pendingElementId;
//
//    // =========================================================================
//    // Initialization
//    // =========================================================================
//
//    /**
//     * Entry point called from JavaScript to initialize the PlantUML renderer.
//     * 
//     * This method:
//     * <ol>
//     *   <li>Starts the background worker thread that will handle all rendering</li>
//     *   <li>Registers the global {@code window.plantumlRender} function</li>
//     * </ol>
//     * 
//     * Must be called once before any rendering can occur.
//     */
//    public static void main(String[] args) {
//        // Start worker thread FIRST - it needs to be ready to receive requests.
//        // The thread runs forever, waiting for render requests.
//        new Thread(PlantUMLBrowser::workerLoop, "plantuml-render").start();
//
//        // Register the JS-callable function. After this, JavaScript can call
//        // plantumlRender(lines, elementId) to request a diagram rendering.
//        registerRender(PlantUMLBrowser::requestRender);
//
//        consoleLog("PlantUML TeaVM loaded.");
//        consoleLog("Version:  " + net.sourceforge.plantuml.version.CompilationInfo.VERSION);
//        consoleLog("Version:  " + net.sourceforge.plantuml.version.Version.fullDescription());
//        consoleLog("Commit:   " + net.sourceforge.plantuml.version.CompilationInfo.COMMIT);
//    }
//
//    // =========================================================================
//    // JavaScript API registration
//    // =========================================================================
//
//    /**
//     * Registers a callback function as {@code window.plantumlRender} in JavaScript.
//     * 
//     * After this call, JavaScript code can invoke:
//     * <pre>
//     * plantumlRender(["@startuml", "Alice -> Bob", "@enduml"], "out");
//     * </pre>
//     */
//    @JSBody(params = "cb", script = "window.plantumlRender = cb;")
//    private static native void registerRender(RenderCallback cb);
//
//    /**
//     * Functional interface for the render callback, compatible with TeaVM's JS interop.
//     * 
//     * The @JSFunctor annotation tells TeaVM to generate a JavaScript function that
//     * can be stored and called from JS code.
//     */
//    @JSFunctor
//    public interface RenderCallback extends JSObject {
//        void call(String[] lines, String elementId);
//    }
//
//    // =========================================================================
//    // Request handling (called from JavaScript)
//    // =========================================================================
//
//    /**
//     * Called from JavaScript to request a diagram rendering.
//     * 
//     * This method does NOT perform the rendering itself - it only queues the request
//     * and wakes up the worker thread. This is necessary because:
//     * 
//     * <ol>
//     *   <li>This method is called from a native JS context (event handler, setTimeout, etc.)</li>
//     *   <li>Viz.js async calls require a TeaVM coroutine context</li>
//     *   <li>The worker thread provides that coroutine context</li>
//     * </ol>
//     * 
//     * If a previous request is still pending (worker hasn't picked it up yet), it will
//     * be overwritten. This is the desired behavior for live-typing scenarios.
//     * 
//     * @param lines     the PlantUML source code, split into lines by the JavaScript caller
//     * @param elementId the ID of the HTML element where the SVG should be rendered
//     */
//    private static void requestRender(String[] lines, String elementId) {
//        synchronized (LOCK) {
//            // Store the request (overwrites any previous pending request)
//            pendingLines = lines;
//            pendingElementId = elementId;
//
//            // Wake up the worker thread to process this request
//            LOCK.notify();
//        }
//    }
//
//    // =========================================================================
//    // Worker thread
//    // =========================================================================
//
//    /**
//     * Main loop for the worker thread. Runs forever, processing render requests.
//     * 
//     * This method executes in a TeaVM coroutine context, which means:
//     * <ul>
//     *   <li>{@code LOCK.wait()} is properly transformed to async JS</li>
//     *   <li>Viz.js async calls (via @Async annotation) work correctly</li>
//     * </ul>
//     * 
//     * The loop:
//     * <ol>
//     *   <li>Waits until a render request is available (pendingLines != null)</li>
//     *   <li>Captures and clears the request atomically</li>
//     *   <li>Performs the rendering (may involve async Viz.js calls)</li>
//     *   <li>Repeats forever</li>
//     * </ol>
//     */
//    private static void workerLoop() {
//        while (true) {
//            String[] lines;
//            String elementId;
//
//            // Wait for a render request
//            synchronized (LOCK) {
//                // Spin-wait pattern: keep waiting until we have work to do.
//                // This handles spurious wakeups correctly.
//                while (pendingLines == null) {
//                    try {
//                        LOCK.wait();
//                    } catch (InterruptedException e) {
//                        // Interruption is not expected, but if it happens, just retry
//                    }
//                }
//
//                // Capture the request and clear the pending state.
//                // This must be atomic (inside synchronized) to avoid race conditions.
//                lines = pendingLines;
//                elementId = pendingElementId;
//                pendingLines = null;
//            }
//
//            // Perform rendering OUTSIDE the synchronized block.
//            // This allows new requests to be queued while we're rendering.
//            doRender(lines, elementId);
//        }
//    }
//
//    // =========================================================================
//    // Rendering
//    // =========================================================================
//
//    /**
//     * Performs the actual PlantUML rendering and inserts the SVG into the DOM.
//     * 
//     * This method:
//     * <ol>
//     *   <li>Clears the target element</li>
//     *   <li>Creates a PlantUML diagram from the source lines</li>
//     *   <li>Renders it to SVG using the TeaVM-compatible graphics system</li>
//     *   <li>Appends the SVG element to the target</li>
//     * </ol>
//     * 
//     * For diagrams requiring GraphViz (class diagrams, etc.), this will internally
//     * call Viz.js asynchronously. This works because we're running in the worker
//     * thread's coroutine context.
//     * 
//     * @param lines     the PlantUML source lines
//     * @param elementId the target DOM element ID
//     */
//    private static void doRender(String[] lines, String elementId) {
//        // Find the target element in the DOM
//        HTMLElement out = HTMLDocument.current().getElementById(elementId);
//        if (out == null)
//            return;
//
//        try {
//            // Create SVG graphics context with TeaVM-compatible implementation
//            SvgGraphicsTeaVM svg = new SvgGraphicsTeaVM(900, 900);
//            UGraphicTeaVM ug = UGraphicTeaVM.build(BACK, COLOR_MAPPER, STRING_BOUNDER, svg);
//
//            // Parse and render the diagram.
//            // For class diagrams, this will call GraphVizjsTeaVMEngine internally,
//            // which uses Viz.js for layout. The @Async magic happens here.
//            Diagram diagram = BUILDER.createDiagram(lines);
//            diagram.exportDiagramGraphic(ug, new FileFormatOption(FileFormat.SVG));
//
//            // Clear any previous content (old SVG, error messages, etc.)
//            removeAllChildren(out);
//
//            // Insert the rendered SVG into the DOM
//            appendSvgElement(out, svg.getSvgRoot());
//        } catch (Exception e) {
//            // Display error message in the target element
//            out.setTextContent(String.valueOf(e));
//        }
//    }
//
//    // =========================================================================
//    // JavaScript interop utilities
//    // =========================================================================
//
//    /** Logs a message to the browser's JavaScript console. */
//    @JSBody(params = "msg", script = "console.log(msg);")
//    private static native void consoleLog(String msg);
//
//    /** Appends an SVG element as a child of a DOM element. */
//    @JSBody(params = { "p", "svg" }, script = "p.appendChild(svg);")
//    private static native void appendSvgElement(HTMLElement p, Element svg);
//
//    /** Removes all child nodes from a DOM element. */
//    @JSBody(params = "el", script = "while(el.firstChild)el.removeChild(el.firstChild);")
//    private static native void removeAllChildren(HTMLElement el);
    
    //:: done
}