Many new things
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import org.openautonomousconnection.luascript.hosts.ClipboardHost;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* JavaFX clipboard host (text only).
|
||||
*/
|
||||
public final class FxClipboardHost implements ClipboardHost {
|
||||
|
||||
@Override
|
||||
public void setText(String text) {
|
||||
String s = text == null ? "" : text;
|
||||
Platform.runLater(() -> {
|
||||
ClipboardContent c = new ClipboardContent();
|
||||
c.putString(s);
|
||||
Clipboard.getSystemClipboard().setContent(c);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
AtomicReference<String> out = new AtomicReference<>("");
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
String s = Clipboard.getSystemClipboard().getString();
|
||||
out.set(s == null ? "" : s);
|
||||
});
|
||||
return out.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.openautonomousconnection.luascript.hosts.CssHost;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* CSS host via internal JS bridge.
|
||||
*/
|
||||
public final class FxCssHost implements CssHost {
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
|
||||
public FxCssHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getComputedStyle(String elementId, String property) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(property, "property");
|
||||
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var prop=" + FxWebBridge.toJsLiteral(property) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " var cs=getComputedStyle(el);"
|
||||
+ " var v=cs.getPropertyValue(prop) || cs[prop] || '';"
|
||||
+ " return String(v);"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return ret == null ? "" : String.valueOf(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getComputedStyles(String elementId, String[] properties) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(properties, "properties");
|
||||
|
||||
Map<String, String> out = new LinkedHashMap<>();
|
||||
for (String p : properties) {
|
||||
if (p == null || p.isBlank()) continue;
|
||||
out.put(p, getComputedStyle(elementId, p));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInlineStyle(String elementId, String property, String value) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(property, "property");
|
||||
|
||||
FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String v = value == null ? "" : value;
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var prop=" + FxWebBridge.toJsLiteral(property) + ";"
|
||||
+ " var val=" + FxWebBridge.toJsLiteral(v) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " if(!val){ el.style.removeProperty(prop); return null; }"
|
||||
+ " el.style.setProperty(prop, val);"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
|
||||
engine.executeScript(script);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInlineStyle(String elementId, String property) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(property, "property");
|
||||
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var prop=" + FxWebBridge.toJsLiteral(property) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " var v=el.style.getPropertyValue(prop) || '';"
|
||||
+ " return String(v);"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return ret == null ? "" : String.valueOf(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCssVariable(String elementId, String name, String value) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
if (!name.trim().startsWith("--")) throw new IllegalArgumentException("CSS variable must start with '--': " + name);
|
||||
setInlineStyle(elementId, name.trim(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCssVariable(String elementId, String name) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
if (!name.trim().startsWith("--")) throw new IllegalArgumentException("CSS variable must start with '--': " + name);
|
||||
return getComputedStyle(elementId, name.trim());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.openautonomousconnection.luascript.hosts.GeometryHost;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Geometry/scroll host via internal JS bridge.
|
||||
*/
|
||||
public final class FxGeometryHost implements GeometryHost {
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
|
||||
public FxGeometryHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getBoundingClientRect(String elementId) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " var r=el.getBoundingClientRect();"
|
||||
+ " return {"
|
||||
+ " x:r.x, y:r.y, width:r.width, height:r.height,"
|
||||
+ " top:r.top, left:r.left, right:r.right, bottom:r.bottom"
|
||||
+ " };"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return FxWebBridge.toStringObjectMap(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getViewport() {
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " return {"
|
||||
+ " width: window.innerWidth || 0,"
|
||||
+ " height: window.innerHeight || 0,"
|
||||
+ " devicePixelRatio: window.devicePixelRatio || 1,"
|
||||
+ " scrollX: window.scrollX || 0,"
|
||||
+ " scrollY: window.scrollY || 0"
|
||||
+ " };"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return FxWebBridge.toStringObjectMap(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollTo(double x, double y) {
|
||||
FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " window.scrollTo(" + FxWebBridge.toJsLiteral(x) + "," + FxWebBridge.toJsLiteral(y) + ");"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollIntoView(String elementId, String align) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
String a = FxWebBridge.normalizeAlign(align);
|
||||
|
||||
FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " el.scrollIntoView({block:" + FxWebBridge.toJsLiteral(a) + ", inline:" + FxWebBridge.toJsLiteral(a) + "});"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import netscape.javascript.JSObject;
|
||||
import org.openautonomousconnection.luascript.hosts.ObserverHost;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Observer host implemented via JS observers calling back into Java.
|
||||
*
|
||||
* <p>Requires JavaScript enabled (bridge only).</p>
|
||||
*/
|
||||
public final class FxObserverHost implements ObserverHost {
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
|
||||
private volatile ObserverCallback callback;
|
||||
|
||||
private final ConcurrentHashMap<String, Boolean> mutationObserved = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Boolean> resizeObserved = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Boolean> intersectionObserved = new ConcurrentHashMap<>();
|
||||
|
||||
public FxObserverHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCallback(ObserverCallback callback) {
|
||||
this.callback = callback;
|
||||
FxThreadBridge.runAndWait(this::installBridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observeMutations(String elementId, boolean subtree, boolean attributes, boolean childList, boolean characterData) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
dom.requireDocument();
|
||||
FxWebBridge.ensureJsEnabled(engine);
|
||||
installBridge();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " window.__oac_obs = window.__oac_obs || {};"
|
||||
+ " window.__oac_obs.muts = window.__oac_obs.muts || new Map();"
|
||||
+ " if(window.__oac_obs.muts.has(id)) return null;"
|
||||
+ " var cfg={subtree:" + (subtree ? "true" : "false")
|
||||
+ " ,attributes:" + (attributes ? "true" : "false")
|
||||
+ " ,childList:" + (childList ? "true" : "false")
|
||||
+ " ,characterData:" + (characterData ? "true" : "false")
|
||||
+ " };"
|
||||
+ " var mo=new MutationObserver(function(muts){"
|
||||
+ " try{"
|
||||
+ " var payload={count:muts.length};"
|
||||
+ " window.__oac_bridge.emit('mutation', id, JSON.stringify(payload));"
|
||||
+ " }catch(e){}"
|
||||
+ " });"
|
||||
+ " mo.observe(el,cfg);"
|
||||
+ " window.__oac_obs.muts.set(id, mo);"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
mutationObserved.put(elementId, true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unobserveMutations(String elementId) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
dom.requireDocument();
|
||||
FxWebBridge.ensureJsEnabled(engine);
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " if(!window.__oac_obs || !window.__oac_obs.muts) return null;"
|
||||
+ " var mo=window.__oac_obs.muts.get(id);"
|
||||
+ " if(mo){ mo.disconnect(); window.__oac_obs.muts.delete(id); }"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
mutationObserved.remove(elementId);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observeResize(String elementId) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
dom.requireDocument();
|
||||
FxWebBridge.ensureJsEnabled(engine);
|
||||
installBridge();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " if(!('ResizeObserver' in window)) return null;"
|
||||
+ " window.__oac_obs = window.__oac_obs || {};"
|
||||
+ " window.__oac_obs.res = window.__oac_obs.res || new Map();"
|
||||
+ " if(window.__oac_obs.res.has(id)) return null;"
|
||||
+ " var ro=new ResizeObserver(function(entries){"
|
||||
+ " try{"
|
||||
+ " var r=entries && entries[0] && entries[0].contentRect;"
|
||||
+ " var payload=r?{x:r.x,y:r.y,width:r.width,height:r.height}:{count:(entries?entries.length:0)};"
|
||||
+ " window.__oac_bridge.emit('resize', id, JSON.stringify(payload));"
|
||||
+ " }catch(e){}"
|
||||
+ " });"
|
||||
+ " ro.observe(el);"
|
||||
+ " window.__oac_obs.res.set(id, ro);"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
resizeObserved.put(elementId, true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unobserveResize(String elementId) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
dom.requireDocument();
|
||||
FxWebBridge.ensureJsEnabled(engine);
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " if(!window.__oac_obs || !window.__oac_obs.res) return null;"
|
||||
+ " var ro=window.__oac_obs.res.get(id);"
|
||||
+ " if(ro){ ro.disconnect(); window.__oac_obs.res.delete(id); }"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
resizeObserved.remove(elementId);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observeIntersection(String elementId, double threshold) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
double t = threshold;
|
||||
if (t < 0.0) t = 0.0;
|
||||
if (t > 1.0) t = 1.0;
|
||||
|
||||
double finalT = t;
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
dom.requireDocument();
|
||||
FxWebBridge.ensureJsEnabled(engine);
|
||||
installBridge();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " if(!('IntersectionObserver' in window)) return null;"
|
||||
+ " window.__oac_obs = window.__oac_obs || {};"
|
||||
+ " window.__oac_obs.int = window.__oac_obs.int || new Map();"
|
||||
+ " if(window.__oac_obs.int.has(id)) return null;"
|
||||
+ " var io=new IntersectionObserver(function(entries){"
|
||||
+ " try{"
|
||||
+ " var e=entries && entries[0];"
|
||||
+ " var payload=e?{"
|
||||
+ " isIntersecting:!!e.isIntersecting,"
|
||||
+ " ratio:(e.intersectionRatio||0)"
|
||||
+ " }:{count:(entries?entries.length:0)};"
|
||||
+ " window.__oac_bridge.emit('intersection', id, JSON.stringify(payload));"
|
||||
+ " }catch(ex){}"
|
||||
+ " }, {threshold:" + FxWebBridge.toJsLiteral(finalT) + "});"
|
||||
+ " io.observe(el);"
|
||||
+ " window.__oac_obs.int.set(id, io);"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
intersectionObserved.put(elementId, true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unobserveIntersection(String elementId) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
dom.requireDocument();
|
||||
FxWebBridge.ensureJsEnabled(engine);
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " if(!window.__oac_obs || !window.__oac_obs.int) return null;"
|
||||
+ " var io=window.__oac_obs.int.get(id);"
|
||||
+ " if(io){ io.disconnect(); window.__oac_obs.int.delete(id); }"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
intersectionObserved.remove(elementId);
|
||||
});
|
||||
}
|
||||
|
||||
private void installBridge() {
|
||||
dom.requireDocument();
|
||||
FxWebBridge.ensureJsEnabled(engine);
|
||||
|
||||
JSObject win = (JSObject) engine.executeScript("window");
|
||||
win.setMember("__oac_bridge", new Bridge());
|
||||
}
|
||||
|
||||
/**
|
||||
* Object exposed to JS.
|
||||
*/
|
||||
public final class Bridge {
|
||||
/**
|
||||
* Emits observer events from JS to Java.
|
||||
*
|
||||
* @param type observer type
|
||||
* @param targetId element id
|
||||
* @param json payload JSON string
|
||||
*/
|
||||
public void emit(String type, String targetId, String json) {
|
||||
ObserverCallback cb = callback;
|
||||
if (cb == null) return;
|
||||
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("json", json == null ? "" : json);
|
||||
|
||||
try {
|
||||
cb.onEvent(type == null ? "" : type, targetId == null ? "" : targetId, payload);
|
||||
} catch (RuntimeException ex) {
|
||||
System.err.println("[observer] callback failed: " + ex.getMessage());
|
||||
ex.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
import org.openautonomousconnection.luascript.hosts.SchedulerHost;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* JavaFX-based scheduler providing setTimeout/setInterval/requestAnimationFrame.
|
||||
*/
|
||||
public final class FxSchedulerHost implements SchedulerHost, AutoCloseable {
|
||||
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final AtomicLong seq = new AtomicLong(1);
|
||||
|
||||
private final ConcurrentHashMap<Long, ScheduledFuture<?>> scheduled = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Runnable> rafCallbacks = new ConcurrentHashMap<>();
|
||||
private final Set<Long> canceledRaf = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final AnimationTimer rafTimer;
|
||||
|
||||
public FxSchedulerHost() {
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "oac-fx-scheduler");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
this.rafTimer = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
// Drain RAF callbacks once per frame.
|
||||
if (rafCallbacks.isEmpty()) return;
|
||||
|
||||
Map<Long, Runnable> snap = new ConcurrentHashMap<>(rafCallbacks);
|
||||
rafCallbacks.clear();
|
||||
|
||||
for (Map.Entry<Long, Runnable> e : snap.entrySet()) {
|
||||
long id = e.getKey();
|
||||
Runnable cb = e.getValue();
|
||||
if (canceledRaf.remove(id)) continue;
|
||||
|
||||
try {
|
||||
cb.run();
|
||||
} catch (RuntimeException ex) {
|
||||
System.err.println("[scheduler.raf] callback failed: " + ex.getMessage());
|
||||
ex.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Platform.runLater(rafTimer::start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long setTimeout(long delayMillis, Runnable callback) {
|
||||
if (delayMillis < 0) delayMillis = 0;
|
||||
Runnable cb = Objects.requireNonNull(callback, "callback");
|
||||
|
||||
long id = seq.getAndIncrement();
|
||||
ScheduledFuture<?> f = scheduler.schedule(() -> safeRun(cb, "timeout"), delayMillis, TimeUnit.MILLISECONDS);
|
||||
scheduled.put(id, f);
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long setInterval(long intervalMillis, Runnable callback) {
|
||||
if (intervalMillis <= 0) throw new IllegalArgumentException("intervalMillis must be > 0");
|
||||
Runnable cb = Objects.requireNonNull(callback, "callback");
|
||||
|
||||
long id = seq.getAndIncrement();
|
||||
ScheduledFuture<?> f = scheduler.scheduleAtFixedRate(() -> safeRun(cb, "interval"), intervalMillis, intervalMillis, TimeUnit.MILLISECONDS);
|
||||
scheduled.put(id, f);
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clear(long handle) {
|
||||
ScheduledFuture<?> f = scheduled.remove(handle);
|
||||
if (f == null) return false;
|
||||
return f.cancel(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long requestAnimationFrame(Runnable callback) {
|
||||
Runnable cb = Objects.requireNonNull(callback, "callback");
|
||||
long id = seq.getAndIncrement();
|
||||
rafCallbacks.put(id, cb);
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancelAnimationFrame(long handle) {
|
||||
// If already queued, mark as canceled.
|
||||
boolean existed = rafCallbacks.remove(handle) != null;
|
||||
canceledRaf.add(handle);
|
||||
return existed;
|
||||
}
|
||||
|
||||
private static void safeRun(Runnable cb, String kind) {
|
||||
try {
|
||||
cb.run();
|
||||
} catch (RuntimeException ex) {
|
||||
System.err.println("[scheduler." + kind + "] callback failed: " + ex.getMessage());
|
||||
ex.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
Platform.runLater(rafTimer::stop);
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
scheduler.shutdownNow();
|
||||
scheduled.clear();
|
||||
rafCallbacks.clear();
|
||||
canceledRaf.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.openautonomousconnection.luascript.hosts.SelectorHost;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* CSS selector host implemented via internal JS bridge.
|
||||
*/
|
||||
public final class FxSelectorHost implements SelectorHost {
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
|
||||
public FxSelectorHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String querySelector(String selector) {
|
||||
Objects.requireNonNull(selector, "selector");
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
dom.ensureAllElementsHaveId();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var sel = " + FxWebBridge.toJsLiteral(selector) + ";"
|
||||
+ " var el = document.querySelector(sel);"
|
||||
+ " if(!el) return null;"
|
||||
+ " if(!el.id){"
|
||||
+ " el.id='__auto_' + Math.floor(Math.random()*1e18).toString(36);"
|
||||
+ " }"
|
||||
+ " return el.id;"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return ret == null ? null : String.valueOf(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> querySelectorAll(String selector) {
|
||||
Objects.requireNonNull(selector, "selector");
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
dom.ensureAllElementsHaveId();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var sel=" + FxWebBridge.toJsLiteral(selector) + ";"
|
||||
+ " var els=document.querySelectorAll(sel);"
|
||||
+ " var out=[];"
|
||||
+ " for(var i=0;i<els.length;i++){"
|
||||
+ " var el=els[i];"
|
||||
+ " if(!el.id){ el.id='__auto_' + Math.floor(Math.random()*1e18).toString(36); }"
|
||||
+ " out.push(el.id);"
|
||||
+ " }"
|
||||
+ " return out;"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return FxWebBridge.toStringList(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String elementId, String selector) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(selector, "selector");
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var sel=" + FxWebBridge.toJsLiteral(selector) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " return !!el.matches(sel);"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return ret instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(ret));
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String closest(String elementId, String selector) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(selector, "selector");
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var id=" + FxWebBridge.toJsLiteral(elementId) + ";"
|
||||
+ " var sel=" + FxWebBridge.toJsLiteral(selector) + ";"
|
||||
+ " var el=document.getElementById(id);"
|
||||
+ " if(!el) throw new Error('Unknown element id: '+id);"
|
||||
+ " var c=el.closest(sel);"
|
||||
+ " if(!c) return null;"
|
||||
+ " if(!c.id){ c.id='__auto_' + Math.floor(Math.random()*1e18).toString(36); }"
|
||||
+ " return c.id;"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return ret == null ? null : String.valueOf(ret);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.openautonomousconnection.luascript.hosts.StorageHost;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Storage host backed by localStorage/sessionStorage via JS bridge.
|
||||
*/
|
||||
public final class FxStorageHost implements StorageHost {
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
|
||||
public FxStorageHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> localKeys() {
|
||||
return keys("localStorage");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String localGet(String key) {
|
||||
return get("localStorage", key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void localSet(String key, String value) {
|
||||
set("localStorage", key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void localRemove(String key) {
|
||||
remove("localStorage", key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void localClear() {
|
||||
clear("localStorage");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> sessionKeys() {
|
||||
return keys("sessionStorage");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sessionGet(String key) {
|
||||
return get("sessionStorage", key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionSet(String key, String value) {
|
||||
set("sessionStorage", key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionRemove(String key) {
|
||||
remove("sessionStorage", key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionClear() {
|
||||
clear("sessionStorage");
|
||||
}
|
||||
|
||||
private List<String> keys(String storageName) {
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];"
|
||||
+ " if(!s) return [];"
|
||||
+ " var out=[];"
|
||||
+ " for(var i=0;i<s.length;i++){ out.push(String(s.key(i))); }"
|
||||
+ " return out;"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return FxWebBridge.toStringList(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
private String get(String storageName, String key) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];"
|
||||
+ " if(!s) return null;"
|
||||
+ " var v=s.getItem(" + FxWebBridge.toJsLiteral(key) + ");"
|
||||
+ " return v===null?null:String(v);"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return ret == null ? null : String.valueOf(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
private void set(String storageName, String key, String value) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
|
||||
FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
if (value == null) {
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];"
|
||||
+ " if(!s) return null;"
|
||||
+ " s.removeItem(" + FxWebBridge.toJsLiteral(key) + ");"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
return;
|
||||
}
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];"
|
||||
+ " if(!s) return null;"
|
||||
+ " s.setItem(" + FxWebBridge.toJsLiteral(key) + "," + FxWebBridge.toJsLiteral(value) + ");"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
}));
|
||||
}
|
||||
|
||||
private void remove(String storageName, String key) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
set(storageName, key, null);
|
||||
}
|
||||
|
||||
private void clear(String storageName) {
|
||||
FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];"
|
||||
+ " if(!s) return null;"
|
||||
+ " s.clear();"
|
||||
+ " return null;"
|
||||
+ "})();";
|
||||
engine.executeScript(script);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.openautonomousconnection.luascript.hosts.UtilHost;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Utility host: Base64, random, URL parsing, query parsing, JSON stringify/normalize via JS.
|
||||
*/
|
||||
public final class FxUtilHost implements UtilHost {
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
private final SecureRandom rng = new SecureRandom();
|
||||
|
||||
public FxUtilHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String base64Encode(String text) {
|
||||
String s = text == null ? "" : text;
|
||||
return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String base64Decode(String base64) {
|
||||
if (base64 == null) return "";
|
||||
byte[] b = Base64.getDecoder().decode(base64);
|
||||
return new String(b, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String randomHex(int numBytes) {
|
||||
if (numBytes <= 0) throw new IllegalArgumentException("numBytes must be > 0");
|
||||
byte[] b = new byte[numBytes];
|
||||
rng.nextBytes(b);
|
||||
StringBuilder sb = new StringBuilder(numBytes * 2);
|
||||
for (byte x : b) sb.append(String.format("%02x", x));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseUrl(String url) {
|
||||
Objects.requireNonNull(url, "url");
|
||||
URI u = URI.create(url.trim());
|
||||
|
||||
Map<String, String> out = new LinkedHashMap<>();
|
||||
out.put("scheme", safe(u.getScheme()));
|
||||
out.put("host", safe(u.getHost()));
|
||||
out.put("port", u.getPort() < 0 ? "" : String.valueOf(u.getPort()));
|
||||
out.put("path", safe(u.getPath()));
|
||||
out.put("query", safe(u.getQuery()));
|
||||
out.put("fragment", safe(u.getFragment()));
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> parseQuery(String query) {
|
||||
String q = query == null ? "" : query.trim();
|
||||
if (q.startsWith("?")) q = q.substring(1);
|
||||
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
if (q.isEmpty()) return out;
|
||||
|
||||
for (String part : q.split("&")) {
|
||||
if (part.isEmpty()) continue;
|
||||
String k;
|
||||
String v;
|
||||
int idx = part.indexOf('=');
|
||||
if (idx < 0) {
|
||||
k = decode(part);
|
||||
v = "";
|
||||
} else {
|
||||
k = decode(part.substring(0, idx));
|
||||
v = decode(part.substring(idx + 1));
|
||||
}
|
||||
out.computeIfAbsent(k, __ -> new ArrayList<>()).add(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String jsonStringifyExpr(String elementId, String jsExpr) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(jsExpr, "jsExpr");
|
||||
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
// NOTE: jsExpr is intentionally raw JS expression; this is a trusted host API.
|
||||
// If you need untrusted usage, wrap/validate upstream.
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var v=(" + jsExpr + ");"
|
||||
+ " return JSON.stringify(v);"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return ret == null ? "null" : String.valueOf(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String jsonNormalize(String elementId, String json) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(json, "json");
|
||||
|
||||
return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> {
|
||||
dom.requireDocument();
|
||||
|
||||
String script = ""
|
||||
+ "(function(){"
|
||||
+ " var s=" + FxWebBridge.toJsLiteral(json) + ";"
|
||||
+ " var obj=JSON.parse(s);"
|
||||
+ " return JSON.stringify(obj);"
|
||||
+ "})();";
|
||||
|
||||
Object ret = engine.executeScript(script);
|
||||
return ret == null ? "null" : String.valueOf(ret);
|
||||
}));
|
||||
}
|
||||
|
||||
private static String safe(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
private static String decode(String s) {
|
||||
return URLDecoder.decode(s, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Shared utilities for WebEngine JS bridging.
|
||||
*/
|
||||
public final class FxWebBridge {
|
||||
|
||||
private FxWebBridge() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures JavaScript is enabled for the engine.
|
||||
*
|
||||
* @param engine engine
|
||||
*/
|
||||
public static void ensureJsEnabled(WebEngine engine) {
|
||||
Objects.requireNonNull(engine, "engine");
|
||||
if (!engine.isJavaScriptEnabled()) {
|
||||
engine.setJavaScriptEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes bridge code with JavaScript temporarily enabled.
|
||||
*
|
||||
* @param engine engine
|
||||
* @param action action
|
||||
*/
|
||||
public static void runWithJs(WebEngine engine, Runnable action) {
|
||||
Objects.requireNonNull(action, "action");
|
||||
callWithJs(engine, () -> {
|
||||
action.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes bridge code with JavaScript temporarily enabled and restores the prior state.
|
||||
*
|
||||
* @param engine engine
|
||||
* @param action action
|
||||
* @param <T> result type
|
||||
* @return action result
|
||||
*/
|
||||
public static <T> T callWithJs(WebEngine engine, Supplier<T> action) {
|
||||
Objects.requireNonNull(engine, "engine");
|
||||
Objects.requireNonNull(action, "action");
|
||||
|
||||
boolean enabledBefore = engine.isJavaScriptEnabled();
|
||||
if (!enabledBefore) {
|
||||
engine.setJavaScriptEnabled(true);
|
||||
}
|
||||
try {
|
||||
return action.get();
|
||||
} finally {
|
||||
if (!enabledBefore) {
|
||||
engine.setJavaScriptEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Java value into a safe JavaScript literal.
|
||||
*
|
||||
* <p>Supported: null, String, Boolean, Number (finite).</p>
|
||||
*
|
||||
* @param v value
|
||||
* @return JS literal
|
||||
*/
|
||||
public static String toJsLiteral(Object v) {
|
||||
if (v == null) return "null";
|
||||
|
||||
if (v instanceof String s) {
|
||||
return "'" + escapeJsSingleQuotedString(s) + "'";
|
||||
}
|
||||
|
||||
if (v instanceof Boolean b) {
|
||||
return b ? "true" : "false";
|
||||
}
|
||||
|
||||
if (v instanceof Number n) {
|
||||
double d = n.doubleValue();
|
||||
if (!Double.isFinite(d)) {
|
||||
throw new IllegalArgumentException("Non-finite number is not supported for JS literal: " + d);
|
||||
}
|
||||
return Double.toString(d);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported value type for JS literal: " + v.getClass().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a JavaScript array literal from Java values.
|
||||
*
|
||||
* @param values values
|
||||
* @return array literal
|
||||
*/
|
||||
public static String toJsArrayLiteral(Object[] values) {
|
||||
if (values == null || values.length == 0) return "[]";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('[');
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (i > 0) sb.append(',');
|
||||
sb.append(toJsLiteral(values[i]));
|
||||
}
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restricts input to a plain JavaScript identifier (prevents injection).
|
||||
*
|
||||
* @param s value
|
||||
* @param label label
|
||||
* @return identifier
|
||||
*/
|
||||
public static String requireJsIdentifier(String s, String label) {
|
||||
if (s == null) throw new IllegalArgumentException(label + " is null");
|
||||
String v = s.trim();
|
||||
if (v.isEmpty()) throw new IllegalArgumentException(label + " is blank");
|
||||
if (!v.matches("^[A-Za-z_$][A-Za-z0-9_$]*$")) {
|
||||
throw new IllegalArgumentException(label + " must be a JS identifier: " + v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes alignment options for scrollIntoView.
|
||||
*
|
||||
* @param align input
|
||||
* @return normalized
|
||||
*/
|
||||
public static String normalizeAlign(String align) {
|
||||
if (align == null) return "nearest";
|
||||
String a = align.trim().toLowerCase(Locale.ROOT);
|
||||
return switch (a) {
|
||||
case "start", "center", "end", "nearest" -> a;
|
||||
default -> "nearest";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JS array-like object into a Java string list.
|
||||
*
|
||||
* @param jsValue raw JS value
|
||||
* @return list of string values
|
||||
*/
|
||||
public static List<String> toStringList(Object jsValue) {
|
||||
if (jsValue == null) return List.of();
|
||||
if (jsValue instanceof List<?> list) {
|
||||
List<String> out = new ArrayList<>(list.size());
|
||||
for (Object value : list) out.add(value == null ? null : String.valueOf(value));
|
||||
return out;
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> jsObj = Class.forName("netscape.javascript.JSObject");
|
||||
if (jsObj.isInstance(jsValue)) {
|
||||
Object lenObj = jsObj.getMethod("getMember", String.class).invoke(jsValue, "length");
|
||||
int len = Integer.parseInt(String.valueOf(lenObj));
|
||||
List<String> out = new ArrayList<>(len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
Object value = jsObj.getMethod("getSlot", int.class).invoke(jsValue, i);
|
||||
out.add(value == null ? null : String.valueOf(value));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// best-effort fallback below
|
||||
}
|
||||
|
||||
return List.of(String.valueOf(jsValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JS object into a Java string-object map when possible.
|
||||
*
|
||||
* @param jsValue raw JS value
|
||||
* @return mapped values
|
||||
*/
|
||||
public static Map<String, Object> toStringObjectMap(Object jsValue) {
|
||||
if (jsValue == null) return Map.of();
|
||||
if (jsValue instanceof Map<?, ?> map) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (entry.getKey() == null) continue;
|
||||
out.put(String.valueOf(entry.getKey()), entry.getValue());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return Map.of("value", jsValue);
|
||||
}
|
||||
|
||||
private static String escapeJsSingleQuotedString(String s) {
|
||||
if (s == null || s.isEmpty()) return "";
|
||||
StringBuilder out = new StringBuilder(s.length() + 16);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '\'' -> out.append("\\'");
|
||||
case '\\' -> out.append("\\\\");
|
||||
case '\n' -> out.append("\\n");
|
||||
case '\r' -> out.append("\\r");
|
||||
case '\t' -> out.append("\\t");
|
||||
case '\u0000' -> out.append("\\0");
|
||||
case '\u2028' -> out.append("\\u2028");
|
||||
case '\u2029' -> out.append("\\u2029");
|
||||
default -> {
|
||||
if (c < 0x20) out.append(String.format("\\u%04x", (int) c));
|
||||
else out.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user