Switched to JavaFX and added builtin Support

This commit is contained in:
Finn
2026-01-19 22:23:49 +01:00
parent 627e0c86e4
commit 8a20970e12
13 changed files with 1019 additions and 263 deletions

View File

@@ -1,232 +0,0 @@
package org.openautonomousconnection.luascript.dom.jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openautonomousconnection.luascript.hosts.DomHost;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* DomHost implementation backed by a jsoup Document.
*
* <p>Element identity is the HTML attribute {@code id}. This implementation auto-assigns ids to all
* elements that do not have one, making them addressable.</p>
*/
public final class JsoupDomHost implements DomHost {
private final Document document;
private final AtomicLong autoIdSeq = new AtomicLong(1);
public JsoupDomHost(Document document) {
this.document = Objects.requireNonNull(document, "document");
ensureAllElementsHaveId();
}
public Document document() {
return document;
}
public void ensureAllElementsHaveId() {
Elements all = document.getAllElements();
for (Element el : all) {
if (el == document) continue;
if (!hasUsableId(el)) {
el.attr("id", generateUniqueId());
}
}
}
@Override
public List<String> getAllElementIds() {
Elements all = document.getAllElements();
List<String> ids = new ArrayList<>(all.size());
for (Element el : all) {
if (el == document) continue;
String id = el.id();
if (id != null && !id.isBlank()) ids.add(id);
}
return ids;
}
@Override
public Map<String, String> getAttributes(String elementId) {
Element el = byId(elementId);
Map<String, String> out = new LinkedHashMap<>();
el.attributes().forEach(a -> out.put(a.getKey(), a.getValue()));
return out;
}
@Override
public String getTagName(String elementId) {
return byId(elementId).tagName().toLowerCase(Locale.ROOT);
}
@Override
public String getTextContent(String elementId) {
return byId(elementId).text();
}
@Override
public void setTextContent(String elementId, String text) {
byId(elementId).text(text == null ? "" : text);
}
@Override
public String getAttribute(String elementId, String name) {
Element el = byId(elementId);
String n = normalizeAttr(name);
if (!el.hasAttr(n)) return null;
return el.attr(n);
}
@Override
public void setAttribute(String elementId, String name, String value) {
Element el = byId(elementId);
el.attr(normalizeAttr(name), value == null ? "" : value);
}
@Override
public void removeAttribute(String elementId, String name) {
byId(elementId).removeAttr(normalizeAttr(name));
}
@Override
public String getParentId(String elementId) {
Element el = byId(elementId);
Element p = el.parent();
if (p == null || p == document) return null;
if (!hasUsableId(p)) p.attr("id", generateUniqueId());
return p.id();
}
@Override
public List<String> getChildrenIds(String elementId) {
Element el = byId(elementId);
List<String> out = new ArrayList<>();
for (Element child : el.children()) {
if (!hasUsableId(child)) child.attr("id", generateUniqueId());
out.add(child.id());
}
return out;
}
@Override
public String createElement(String tagName, String requestedId) {
String tag = normalizeTag(tagName);
String id = normalizeOrGenerateId(requestedId);
if (exists(id)) throw new IllegalArgumentException("Element id already exists: " + id);
Element el = document.createElement(tag);
el.attr("id", id);
return id;
}
@Override
public void removeElement(String elementId) {
byId(elementId).remove();
}
@Override
public void appendChild(String parentId, String childId) {
Element parent = byId(parentId);
Element child = byId(childId);
child.remove();
parent.appendChild(child);
}
@Override
public void insertBefore(String parentId, String childId, String beforeChildId) {
Element parent = byId(parentId);
Element child = byId(childId);
Element before = byId(beforeChildId);
if (before.parent() == null || !Objects.equals(before.parent(), parent)) {
throw new IllegalArgumentException("beforeChildId is not a direct child of parentId: " + beforeChildId);
}
child.remove();
before.before(child);
}
@Override
public boolean exists(String id) {
if (id == null || id.isBlank()) return false;
return document.getElementById(id) != null;
}
@Override
public List<String> queryByTag(String tagName) {
String tag = normalizeTag(tagName);
Elements els = document.getElementsByTag(tag);
List<String> out = new ArrayList<>(els.size());
for (Element el : els) {
if (!hasUsableId(el)) el.attr("id", generateUniqueId());
out.add(el.id());
}
return out;
}
@Override
public List<String> queryByClass(String className) {
String cls = normalizeCssIdent(className);
Elements els = document.getElementsByClass(cls);
List<String> out = new ArrayList<>(els.size());
for (Element el : els) {
if (!hasUsableId(el)) el.attr("id", generateUniqueId());
out.add(el.id());
}
return out;
}
private Element byId(String id) {
if (id == null || id.isBlank()) throw new IllegalArgumentException("elementId is blank");
Element el = document.getElementById(id);
if (el == null) throw new IllegalArgumentException("Unknown element id: " + id);
return el;
}
private boolean hasUsableId(Element el) {
String id = el.id();
return id != null && !id.isBlank();
}
private String generateUniqueId() {
while (true) {
String id = "__auto_" + autoIdSeq.getAndIncrement();
if (!exists(id)) return id;
}
}
private String normalizeTag(String tagName) {
if (tagName == null) throw new IllegalArgumentException("tagName is null");
String t = tagName.trim().toLowerCase(Locale.ROOT);
if (t.isEmpty()) throw new IllegalArgumentException("tagName is empty");
return t;
}
private String normalizeAttr(String name) {
if (name == null) throw new IllegalArgumentException("attribute name is null");
String n = name.trim();
if (n.isEmpty()) throw new IllegalArgumentException("attribute name is empty");
return n;
}
private String normalizeCssIdent(String s) {
if (s == null) throw new IllegalArgumentException("identifier is null");
String v = s.trim();
if (v.isEmpty()) throw new IllegalArgumentException("identifier is empty");
return v;
}
private String normalizeOrGenerateId(String requestedId) {
if (requestedId != null) {
String id = requestedId.trim();
if (!id.isEmpty()) return id;
}
return generateUniqueId();
}
}

View File

@@ -0,0 +1,357 @@
package org.openautonomousconnection.luascript.fx;
import javafx.scene.web.WebEngine;
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
import org.openautonomousconnection.luascript.hosts.DomHost;
import org.w3c.dom.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* DomHost implementation backed by JavaFX WebView's W3C DOM (WebEngine#getDocument()).
*
* <p>No jsoup and no JavaScript. All operations are performed via W3C DOM APIs.</p>
*
* <p>Element identity is the {@code id} attribute. This host auto-assigns stable ids to elements that
* do not have one, ensuring addressability for Lua bindings and event routing.</p>
*/
public final class FxDomHost implements DomHost {
private final WebEngine engine;
private final AtomicLong autoIdSeq = new AtomicLong(1);
/**
* Creates a new FxDomHost.
*
* @param engine JavaFX WebEngine
*/
public FxDomHost(WebEngine engine) {
this.engine = Objects.requireNonNull(engine, "engine");
}
/**
* Ensures every element has a stable id.
*/
public void ensureAllElementsHaveId() {
FxThreadBridge.runAndWait(() -> {
Document doc = requireDocument();
NodeList all = doc.getElementsByTagName("*");
for (int i = 0; i < all.getLength(); i++) {
Node n = all.item(i);
if (n instanceof Element el) {
if (!hasUsableId(el)) {
el.setAttribute("id", generateUniqueId(doc));
}
}
}
});
}
@Override
public List<String> getAllElementIds() {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
NodeList all = doc.getElementsByTagName("*");
List<String> ids = new ArrayList<>(all.getLength());
for (int i = 0; i < all.getLength(); i++) {
Node n = all.item(i);
if (n instanceof Element el) {
String id = el.getAttribute("id");
if (id != null && !id.isBlank()) ids.add(id);
}
}
return ids;
});
}
@Override
public Map<String, String> getAttributes(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Element el = byId(elementId);
NamedNodeMap nnm = el.getAttributes();
Map<String, String> out = new LinkedHashMap<>();
for (int i = 0; i < nnm.getLength(); i++) {
Node a = nnm.item(i);
if (a == null) continue;
out.put(a.getNodeName(), a.getNodeValue());
}
return out;
});
}
@Override
public String getTagName(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Element el = byId(elementId);
String tag = el.getTagName();
return tag == null ? "" : tag.trim().toLowerCase(Locale.ROOT);
});
}
@Override
public String getTextContent(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Element el = byId(elementId);
String t = el.getTextContent();
return t == null ? "" : t;
});
}
@Override
public void setTextContent(String elementId, String text) {
FxThreadBridge.runAndWait(() -> byId(elementId).setTextContent(text == null ? "" : text));
}
@Override
public String getAttribute(String elementId, String name) {
return FxThreadBridge.callAndWait(() -> {
Element el = byId(elementId);
String n = normalizeAttr(name);
if (!el.hasAttribute(n)) return null;
String v = el.getAttribute(n);
return v == null ? "" : v;
});
}
@Override
public void setAttribute(String elementId, String name, String value) {
FxThreadBridge.runAndWait(() -> byId(elementId).setAttribute(normalizeAttr(name), value == null ? "" : value));
}
@Override
public void removeAttribute(String elementId, String name) {
FxThreadBridge.runAndWait(() -> byId(elementId).removeAttribute(normalizeAttr(name)));
}
@Override
public String getParentId(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
Element el = byId(elementId);
Node p = el.getParentNode();
if (!(p instanceof Element pe)) return null;
if (!hasUsableId(pe)) pe.setAttribute("id", generateUniqueId(doc));
String id = pe.getAttribute("id");
return (id == null || id.isBlank()) ? null : id;
});
}
@Override
public List<String> getChildrenIds(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
Element el = byId(elementId);
NodeList children = el.getChildNodes();
List<String> out = new ArrayList<>();
for (int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if (n instanceof Element ce) {
if (!hasUsableId(ce)) ce.setAttribute("id", generateUniqueId(doc));
out.add(ce.getAttribute("id"));
}
}
return out;
});
}
@Override
public String createElement(String tagName, String requestedId) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
String tag = normalizeTag(tagName);
String id = normalizeOrGenerateId(requestedId, doc);
if (doc.getElementById(id) != null) throw new IllegalArgumentException("Element id already exists: " + id);
Element el = doc.createElement(tag);
el.setAttribute("id", id);
// Make it immediately addressable by putting it into a hidden staging container.
ensureStagingContainer(doc).appendChild(el);
return id;
});
}
@Override
public void removeElement(String elementId) {
FxThreadBridge.runAndWait(() -> {
Element el = byId(elementId);
Node p = el.getParentNode();
if (p != null) p.removeChild(el);
});
}
@Override
public void appendChild(String parentId, String childId) {
FxThreadBridge.runAndWait(() -> {
Element parent = byId(parentId);
Element child = byId(childId);
Node old = child.getParentNode();
if (old != null) old.removeChild(child);
parent.appendChild(child);
});
}
@Override
public void insertBefore(String parentId, String childId, String beforeChildId) {
FxThreadBridge.runAndWait(() -> {
Element parent = byId(parentId);
Element child = byId(childId);
Element before = byId(beforeChildId);
Node beforeParent = before.getParentNode();
if (beforeParent == null || !beforeParent.isSameNode(parent)) {
throw new IllegalArgumentException("beforeChildId is not a direct child of parentId: " + beforeChildId);
}
Node old = child.getParentNode();
if (old != null) old.removeChild(child);
parent.insertBefore(child, before);
});
}
@Override
public boolean exists(String id) {
if (id == null || id.isBlank()) return false;
return FxThreadBridge.callAndWait(() -> {
Document doc = engine.getDocument();
return doc != null && doc.getElementById(id) != null;
});
}
@Override
public List<String> queryByTag(String tagName) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
String tag = normalizeTag(tagName);
NodeList els = doc.getElementsByTagName(tag);
List<String> out = new ArrayList<>(els.getLength());
for (int i = 0; i < els.getLength(); i++) {
Node n = els.item(i);
if (n instanceof Element el) {
if (!hasUsableId(el)) el.setAttribute("id", generateUniqueId(doc));
out.add(el.getAttribute("id"));
}
}
return out;
});
}
@Override
public List<String> queryByClass(String className) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
String cls = normalizeCssIdent(className);
NodeList all = doc.getElementsByTagName("*");
List<String> out = new ArrayList<>();
for (int i = 0; i < all.getLength(); i++) {
Node n = all.item(i);
if (n instanceof Element el) {
String c = el.getAttribute("class");
if (c == null || c.isBlank()) continue;
if (hasClassToken(c, cls)) {
if (!hasUsableId(el)) el.setAttribute("id", generateUniqueId(doc));
out.add(el.getAttribute("id"));
}
}
}
return out;
});
}
/**
* Exposes the current document (FX thread access required by callers).
*
* @return document
*/
public Document requireDocument() {
Document doc = engine.getDocument();
if (doc == null) throw new IllegalStateException("WebEngine document is not available yet (page not loaded?)");
return doc;
}
/**
* Exposes element lookup for FX event host / ui host (FX thread access required by callers).
*
* @param id element id
* @return element
*/
public Element byId(String id) {
if (id == null || id.isBlank()) throw new IllegalArgumentException("elementId is blank");
Document doc = requireDocument();
Element el = doc.getElementById(id);
if (el == null) throw new IllegalArgumentException("Unknown element id: " + id);
return el;
}
private static boolean hasUsableId(Element el) {
String id = el.getAttribute("id");
return id != null && !id.isBlank();
}
private String generateUniqueId(Document doc) {
while (true) {
String id = "__auto_" + autoIdSeq.getAndIncrement();
if (doc.getElementById(id) == null) return id;
}
}
private static Element ensureStagingContainer(Document doc) {
Element body = (Element) doc.getElementsByTagName("body").item(0);
if (body == null) throw new IllegalStateException("No <body> element available");
Element staging = doc.getElementById("__oac_staging");
if (staging != null) return staging;
staging = doc.createElement("div");
staging.setAttribute("id", "__oac_staging");
staging.setAttribute("style", "display:none !important;");
body.appendChild(staging);
return staging;
}
private static boolean hasClassToken(String classAttr, String cls) {
String[] parts = classAttr.trim().split("\\s+");
for (String p : parts) {
if (p.equals(cls)) return true;
}
return false;
}
private static String normalizeTag(String tagName) {
if (tagName == null) throw new IllegalArgumentException("tagName is null");
String t = tagName.trim().toLowerCase(Locale.ROOT);
if (t.isEmpty()) throw new IllegalArgumentException("tagName is empty");
return t;
}
private static String normalizeAttr(String name) {
if (name == null) throw new IllegalArgumentException("attribute name is null");
String n = name.trim();
if (n.isEmpty()) throw new IllegalArgumentException("attribute name is empty");
return n;
}
private static String normalizeCssIdent(String s) {
if (s == null) throw new IllegalArgumentException("identifier is null");
String v = s.trim();
if (v.isEmpty()) throw new IllegalArgumentException("identifier is empty");
return v;
}
private String normalizeOrGenerateId(String requestedId, Document doc) {
if (requestedId != null) {
String id = requestedId.trim();
if (!id.isEmpty()) return id;
}
return generateUniqueId(doc);
}
}

View File

@@ -0,0 +1,118 @@
package org.openautonomousconnection.luascript.fx;
import org.openautonomousconnection.luascript.fx.FxDomHost;
import org.openautonomousconnection.luascript.events.UiEventRegistry;
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
import org.openautonomousconnection.luascript.hosts.EventHost;
import org.openautonomousconnection.luascript.runtime.LuaEventRouter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* EventHost implementation for JavaFX WebView using W3C DOM EventTarget listeners.
*
* <p>No JavaScript required. Events are forwarded to Lua via {@link LuaEventRouter}.</p>
*/
public final class FxEventHost implements EventHost {
private static final String GLOBAL_TARGET_ID = "__global__";
private final FxDomHost dom;
private final LuaEventRouter router;
private final ConcurrentHashMap<String, EventListener> elementListeners = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, EventListener> globalListeners = new ConcurrentHashMap<>();
/**
* Creates a new FxEventHost.
*
* @param dom fx dom host
* @param router lua router
*/
public FxEventHost(FxDomHost dom, LuaEventRouter router) {
this.dom = Objects.requireNonNull(dom, "dom");
this.router = Objects.requireNonNull(router, "router");
}
@Override
public void addListener(String elementId, String eventName) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
String key = elementId + "\n" + evt;
elementListeners.computeIfAbsent(key, k -> {
EventListener listener = ev -> {
Map<String, Object> payload = FxEventPayloadExtractor.extract(ev);
router.emit(elementId, evt, payload);
};
FxThreadBridge.runAndWait(() -> {
Element el = dom.byId(elementId);
((EventTarget) el).addEventListener(evt, listener, false);
});
return listener;
});
}
@Override
public void removeListener(String elementId, String eventName) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
String key = elementId + "\n" + evt;
EventListener listener = elementListeners.remove(key);
if (listener == null) return;
FxThreadBridge.runAndWait(() -> {
Element el = dom.byId(elementId);
((EventTarget) el).removeEventListener(evt, listener, false);
});
}
@Override
public void addGlobalListener(String eventName) {
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
globalListeners.computeIfAbsent(evt, k -> {
EventListener listener = ev -> {
Map<String, Object> payload = FxEventPayloadExtractor.extract(ev);
router.emit(GLOBAL_TARGET_ID, evt, payload);
};
FxThreadBridge.runAndWait(() -> {
Document doc = dom.requireDocument();
((EventTarget) doc).addEventListener(evt, listener, false);
});
return listener;
});
}
@Override
public void removeGlobalListener(String eventName) {
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
EventListener listener = globalListeners.remove(evt);
if (listener == null) return;
FxThreadBridge.runAndWait(() -> {
Document doc = dom.requireDocument();
((EventTarget) doc).removeEventListener(evt, listener, false);
});
}
}

View File

@@ -0,0 +1,56 @@
package org.openautonomousconnection.luascript.fx;
import com.sun.webkit.dom.KeyboardEventImpl;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.MouseEvent;
import org.w3c.dom.events.UIEvent;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Extracts common DOM event fields into a plain map for Lua.
*/
public final class FxEventPayloadExtractor {
private FxEventPayloadExtractor() {
}
/**
* Extracts an event payload.
*
* @param ev W3C DOM event
* @return map payload (never null)
*/
public static Map<String, Object> extract(Event ev) {
Map<String, Object> data = new LinkedHashMap<>();
if (ev == null) return data;
data.put("type", ev.getType());
if (ev instanceof UIEvent uiev) {
data.put("detail", uiev.getDetail());
}
if (ev instanceof MouseEvent mev) {
data.put("clientX", (int) mev.getClientX());
data.put("clientY", (int) mev.getClientY());
data.put("button", (int) mev.getButton());
data.put("altKey", mev.getAltKey());
data.put("ctrlKey", mev.getCtrlKey());
data.put("shiftKey", mev.getShiftKey());
data.put("metaKey", mev.getMetaKey());
}
if (ev instanceof KeyboardEventImpl kev) {
data.put("key", kev.getKeyIdentifier());
data.put("keyCode", (int) kev.getKeyCode());
data.put("altKey", kev.getAltKey());
data.put("ctrlKey", kev.getCtrlKey());
data.put("shiftKey", kev.getShiftKey());
data.put("metaKey", kev.getMetaKey());
}
return data;
}
}

View File

@@ -0,0 +1,66 @@
package org.openautonomousconnection.luascript.fx;
import javafx.application.Platform;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* Executes work on the JavaFX Application Thread and waits synchronously for completion.
*/
public final class FxThreadBridge {
private FxThreadBridge() {
}
/**
* Runs the given task on the FX thread and blocks until completion.
*
* @param task task to execute
* @param <T> result type
* @return task result
*/
public static <T> T callAndWait(Callable<T> task) {
Objects.requireNonNull(task, "task");
if (Platform.isFxApplicationThread()) {
try {
return task.call();
} catch (Exception e) {
throw rethrow(e);
}
}
FutureTask<T> ft = new FutureTask<>(task);
Platform.runLater(ft);
try {
return ft.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for FX thread", e);
} catch (ExecutionException e) {
throw rethrow(e.getCause());
}
}
/**
* Runs the given runnable on the FX thread and blocks until completion.
*
* @param runnable runnable
*/
public static void runAndWait(Runnable runnable) {
callAndWait(() -> {
runnable.run();
return null;
});
}
private static RuntimeException rethrow(Throwable t) {
if (t instanceof RuntimeException re) return re;
if (t instanceof Error err) throw err;
return new RuntimeException(t);
}
}

View File

@@ -0,0 +1,293 @@
package org.openautonomousconnection.luascript.fx;
import javafx.scene.web.WebEngine;
import org.openautonomousconnection.luascript.fx.FxDomHost;
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
import org.openautonomousconnection.luascript.hosts.UiHost;
import org.w3c.dom.Element;
import java.util.Objects;
/**
* UiHost implementation for JavaFX WebView (no JavaScript).
*
* <p>Operations are implemented via W3C DOM attributes/text, and best-effort behavior for
* value/style/class using standard attributes.</p>
*/
public final class FxUiHost implements UiHost {
private final WebEngine engine;
private final FxDomHost dom;
/**
* Creates a new UI host.
*
* @param engine web engine
* @param dom dom host
*/
public FxUiHost(WebEngine engine, FxDomHost dom) {
this.engine = Objects.requireNonNull(engine, "engine");
this.dom = Objects.requireNonNull(dom, "dom");
}
@Override
public void alert(String message) {
// No JS: use simple JavaFX dialog-less fallback (log-style). You can replace with real Dialogs later.
// Keeping it deterministic and non-blocking for now.
System.out.println("[ui.alert] " + (message == null ? "" : message));
}
@Override
public boolean confirm(String message) {
// No JS: deterministic default (false). Replace with JavaFX dialogs if you want UI interaction.
System.out.println("[ui.confirm] " + (message == null ? "" : message));
return false;
}
@Override
public String prompt(String message, String defaultValue) {
// No JS: deterministic default.
System.out.println("[ui.prompt] " + (message == null ? "" : message));
return defaultValue;
}
@Override
public void setText(String elementId, String text) {
dom.setTextContent(elementId, text);
}
@Override
public String getText(String elementId) {
return dom.getTextContent(elementId);
}
@Override
public void setHtml(String elementId, String html) {
// Without JS, safest is to set textContent (prevents HTML parsing).
// If you need real HTML injection, we must extend DomHost with fragment parsing (not in current API).
dom.setTextContent(elementId, html);
}
@Override
public String getHtml(String elementId) {
// Without JS, best-effort: return textContent.
return dom.getTextContent(elementId);
}
@Override
public void setValue(String elementId, String value) {
// Input/textarea value is commonly reflected as attribute "value".
dom.setAttribute(elementId, "value", value == null ? "" : value);
}
@Override
public String getValue(String elementId) {
String v = dom.getAttribute(elementId, "value");
return v == null ? "" : v;
}
@Override
public void setEnabled(String elementId, boolean enabled) {
if (enabled) dom.removeAttribute(elementId, "disabled");
else dom.setAttribute(elementId, "disabled", "disabled");
}
@Override
public void setVisible(String elementId, boolean visible) {
// Best-effort via style attribute
String style = dom.getAttribute(elementId, "style");
style = style == null ? "" : style;
style = removeCssProp(style, "display");
if (!visible) {
style = style.trim();
if (!style.isEmpty() && !style.endsWith(";")) style += ";";
style += "display:none;";
}
dom.setAttribute(elementId, "style", style);
}
@Override
public void addClass(String elementId, String className) {
String cls = Objects.requireNonNull(className, "className").trim();
if (cls.isEmpty()) return;
FxThreadBridge.runAndWait(() -> {
Element el = dom.byId(elementId);
String c = el.getAttribute("class");
c = (c == null) ? "" : c.trim();
if (c.isEmpty()) {
el.setAttribute("class", cls);
return;
}
if (!hasClassToken(c, cls)) {
el.setAttribute("class", c + " " + cls);
}
});
}
@Override
public void removeClass(String elementId, String className) {
String cls = Objects.requireNonNull(className, "className").trim();
if (cls.isEmpty()) return;
FxThreadBridge.runAndWait(() -> {
Element el = dom.byId(elementId);
String c = el.getAttribute("class");
c = (c == null) ? "" : c.trim();
if (c.isEmpty()) return;
String[] parts = c.split("\\s+");
StringBuilder sb = new StringBuilder();
for (String p : parts) {
if (p.equals(cls)) continue;
if (!sb.isEmpty()) sb.append(' ');
sb.append(p);
}
el.setAttribute("class", sb.toString());
});
}
@Override
public boolean toggleClass(String elementId, String className) {
if (hasClass(elementId, className)) {
removeClass(elementId, className);
return false;
}
addClass(elementId, className);
return true;
}
@Override
public boolean hasClass(String elementId, String className) {
String cls = Objects.requireNonNull(className, "className").trim();
if (cls.isEmpty()) return false;
return FxThreadBridge.callAndWait(() -> {
Element el = dom.byId(elementId);
String c = el.getAttribute("class");
c = (c == null) ? "" : c.trim();
return !c.isEmpty() && hasClassToken(c, cls);
});
}
@Override
public void setStyle(String elementId, String property, String value) {
String prop = Objects.requireNonNull(property, "property").trim().toLowerCase();
if (prop.isEmpty()) return;
String style = dom.getAttribute(elementId, "style");
style = style == null ? "" : style;
style = removeCssProp(style, prop);
String v = value == null ? "" : value.trim();
if (!v.isEmpty()) {
style = style.trim();
if (!style.isEmpty() && !style.endsWith(";")) style += ";";
style += prop + ":" + v + ";";
}
dom.setAttribute(elementId, "style", style);
}
@Override
public String getStyle(String elementId, String property) {
// Best-effort parsing from style attribute.
String prop = Objects.requireNonNull(property, "property").trim().toLowerCase();
if (prop.isEmpty()) return "";
String style = dom.getAttribute(elementId, "style");
style = style == null ? "" : style;
for (String part : style.split(";")) {
String p = part.trim();
if (p.isEmpty()) continue;
int idx = p.indexOf(':');
if (idx <= 0) continue;
String k = p.substring(0, idx).trim().toLowerCase();
if (k.equals(prop)) return p.substring(idx + 1).trim();
}
return "";
}
@Override
public void setAttribute(String elementId, String name, String value) {
dom.setAttribute(elementId, name, value);
}
@Override
public String getAttribute(String elementId, String name) {
return dom.getAttribute(elementId, name);
}
@Override
public void removeAttribute(String elementId, String name) {
dom.removeAttribute(elementId, name);
}
@Override
public void focus(String elementId) {
// Without JS, focus control is limited. Best-effort via attribute.
dom.setAttribute(elementId, "autofocus", "autofocus");
}
@Override
public void blur(String elementId) {
// No-op without JS.
}
@Override
public void scrollIntoView(String elementId) {
// No JS => no reliable scrollIntoView. No-op.
}
@Override
public int viewportWidth() {
// Without JS, no real viewport query. Return -1 (unknown).
return -1;
}
@Override
public int viewportHeight() {
return -1;
}
@Override
public long nowMillis() {
return System.currentTimeMillis();
}
private static boolean hasClassToken(String classAttr, String cls) {
String[] parts = classAttr.trim().split("\\s+");
for (String p : parts) {
if (p.equals(cls)) return true;
}
return false;
}
private static String removeCssProp(String style, String propLower) {
if (style == null || style.isBlank()) return "";
StringBuilder sb = new StringBuilder();
for (String part : style.split(";")) {
String p = part.trim();
if (p.isEmpty()) continue;
int idx = p.indexOf(':');
if (idx <= 0) continue;
String k = p.substring(0, idx).trim().toLowerCase();
if (k.equals(propLower)) continue;
if (!sb.isEmpty()) sb.append(';');
sb.append(p);
}
String out = sb.toString().trim();
if (!out.isEmpty() && !out.endsWith(";")) out += ";";
return out;
}
}

View File

@@ -0,0 +1,56 @@
package org.openautonomousconnection.luascript.fx;
import javafx.scene.web.WebEngine;
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
import org.openautonomousconnection.luascript.hosts.ResourceHost;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* ResourceHost that loads script src via URLConnection.
*
* <p>Relative URLs are resolved against the current WebEngine location.</p>
*/
public final class FxWebViewResourceHost implements ResourceHost {
private final WebEngine engine;
/**
* Creates a new resource host.
*
* @param engine web engine
*/
public FxWebViewResourceHost(WebEngine engine) {
this.engine = Objects.requireNonNull(engine, "engine");
}
@Override
public String readText(String src) throws Exception {
Objects.requireNonNull(src, "src");
String trimmed = src.trim();
if (trimmed.isEmpty()) throw new IllegalArgumentException("src is empty");
String base = FxThreadBridge.callAndWait(engine::getLocation);
URL url = (base == null || base.isBlank())
? new URL(trimmed)
: new URL(new URL(base), trimmed);
URLConnection con = url.openConnection();
con.setUseCaches(false);
try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder(4096);
char[] buf = new char[8192];
int r;
while ((r = br.read(buf)) >= 0) {
sb.append(buf, 0, r);
}
return sb.toString();
}
}
}

View File

@@ -1,5 +1,12 @@
package org.openautonomousconnection.luascript.hosts;
import javafx.scene.web.WebEngine;
import org.openautonomousconnection.luascript.fx.FxDomHost;
import org.openautonomousconnection.luascript.fx.FxEventHost;
import org.openautonomousconnection.luascript.fx.FxUiHost;
import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost;
import org.openautonomousconnection.luascript.runtime.LuaRuntime;
import java.util.Objects;
import java.util.Optional;
@@ -24,6 +31,38 @@ public final class HostServices {
this.resources = b.resources;
}
/**
* Builds a JavaFX WebView preset of HostServices (DOM + events + resources + UI).
*
* <p>Important: This method creates the {@link EventHost} using the provided {@link LuaRuntime}'s
* {@link org.openautonomousconnection.luascript.runtime.LuaEventRouter}. Therefore, the runtime must already
* be constructed before calling this method.</p>
*
* <p>Note: You should call {@link FxDomHost#ensureAllElementsHaveId()} after the WebEngine finished loading.</p>
*
* @param engine JavaFX WebEngine
* @param runtime Lua runtime used for event routing
* @param console optional console host (may be null)
* @return HostServices preset for JavaFX WebView
*/
public static HostServices fxPreset(WebEngine engine, LuaRuntime runtime, ConsoleHost console) {
Objects.requireNonNull(engine, "engine");
Objects.requireNonNull(runtime, "runtime");
FxDomHost dom = new FxDomHost(engine);
FxWebViewResourceHost resources = new FxWebViewResourceHost(engine);
FxUiHost ui = new FxUiHost(engine, dom);
FxEventHost events = new FxEventHost(dom, runtime.eventRouter());
Builder b = builder().dom(dom).events(events).resources(resources).ui(ui);
if (console != null) {
b.console(console);
}
return b.build();
}
/**
* @return builder
*/
@@ -124,4 +163,3 @@ public final class HostServices {
}
}
}

View File

@@ -12,14 +12,21 @@ import java.util.Objects;
/**
* Bootstrap that:
* 1) Executes all Lua scripts found in DOM
* * 2) Scans DOM for on:* handlers and binds them automatically.
* 1) Executes ALL scripts found in DOM as Lua: &lt;script&gt; ... or &lt;script src="..."&gt;
* 2) Scans DOM for on:* handlers and binds them automatically.
*/
public final class LuaScriptBootstrap {
private LuaScriptBootstrap() {
}
/**
* Bootstraps Lua from DOM.
*
* @param globals globals
* @param services host services
* @param dispatcher event dispatcher
*/
public static void bootstrap(Globals globals, HostServices services, LuaEventDispatcher dispatcher) {
Objects.requireNonNull(globals, "globals");
Objects.requireNonNull(services, "services");
@@ -41,20 +48,16 @@ public final class LuaScriptBootstrap {
Map<String, String> attrs = dom.getAttributes(elementId);
if (attrs == null) continue;
String type = safeLower(attrs.getOrDefault("type", ""));
String src = attrs.get("src");
boolean isLuaByType = "lua".equals(type) || "text/lua".equals(type) || "application/lua".equals(type);
if (!isLuaByType) continue; // IMPORTANT: only run scripts explicitly marked as Lua
boolean hasSrc = src != null && !src.trim().isEmpty();
if (hasSrc) {
String path = src.trim();
try {
String code = resources.readText(path);
LuaScriptExecutor.execute(globals, code, path);
} catch (Exception ex) {
throw new IllegalStateException("Failed to load lua script src='" + path + "': " + ex.getMessage(), ex);
throw new IllegalStateException("Failed to load script src='" + path + "': " + ex.getMessage(), ex);
}
} else {
String inline = dom.getTextContent(elementId);