Many new things

This commit is contained in:
UnlegitDqrk
2026-02-28 17:39:42 +01:00
parent a84c626416
commit a9b0ccb8a7
30 changed files with 2490 additions and 150 deletions

49
.idea/misc.xml generated
View File

@@ -8,6 +8,55 @@
</list>
</option>
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" />
<option name="myOrdered" value="false" />
<option name="myNullables">
<value>
<list size="16">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="2" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
<item index="5" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="11" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
<item index="13" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="14" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="15" class="java.lang.String" itemvalue="org.springframework.lang.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="16">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="2" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="5" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="11" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
<item index="13" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="14" class="java.lang.String" itemvalue="lombok.NonNull" />
<item index="15" class="java.lang.String" itemvalue="org.springframework.lang.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>

View File

@@ -6,7 +6,7 @@
<groupId>org.openautonomousconnection</groupId>
<artifactId>LuaScript</artifactId>
<version>0.0.0-STABLE.1.3</version>
<version>0.0.0-STABLE.1.4</version>
<organization>
<name>Open Autonomous Connection</name>

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}));
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}));
}
}

View File

@@ -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);
}));
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,21 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Clipboard access.
*/
public interface ClipboardHost {
/**
* Sets clipboard text.
*
* @param text text
*/
void setText(String text);
/**
* Gets clipboard text.
*
* @return text or empty string if none
*/
String getText();
}

View File

@@ -0,0 +1,63 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.Map;
/**
* CSSOM / computed styles access.
*/
public interface CssHost {
/**
* Gets computed style value for a property.
*
* @param elementId element id
* @param property CSS property (kebab-case or camelCase accepted by browser)
* @return computed value (never null, may be empty)
*/
String getComputedStyle(String elementId, String property);
/**
* Returns a computed style snapshot of selected properties.
*
* @param elementId element id
* @param properties properties to fetch
* @return map property->value
*/
Map<String, String> getComputedStyles(String elementId, String[] properties);
/**
* Sets an inline style property (style attribute).
*
* @param elementId element id
* @param property CSS property
* @param value CSS value (empty => remove)
*/
void setInlineStyle(String elementId, String property, String value);
/**
* Gets an inline style property (from style attribute).
*
* @param elementId element id
* @param property CSS property
* @return inline value (may be empty)
*/
String getInlineStyle(String elementId, String property);
/**
* Sets a CSS variable (custom property) on an element.
*
* @param elementId element id
* @param name variable name (e.g. "--primary")
* @param value value
*/
void setCssVariable(String elementId, String name, String value);
/**
* Gets a CSS variable (computed) from an element.
*
* @param elementId element id
* @param name variable name (e.g. "--primary")
* @return computed var (may be empty)
*/
String getCssVariable(String elementId, String name);
}

View File

@@ -0,0 +1,42 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.Map;
/**
* Layout and scrolling related accessors.
*
* <p>All results are plain maps to keep dependencies minimal.</p>
*/
public interface GeometryHost {
/**
* Returns bounding client rect of an element.
*
* @param elementId element id
* @return map: x,y,width,height,top,left,right,bottom
*/
Map<String, Object> getBoundingClientRect(String elementId);
/**
* Returns viewport metrics.
*
* @return map: width,height,devicePixelRatio,scrollX,scrollY
*/
Map<String, Object> getViewport();
/**
* Scrolls the document to coordinates.
*
* @param x scroll x
* @param y scroll y
*/
void scrollTo(double x, double y);
/**
* Scrolls an element into view.
*
* @param elementId element id
* @param align one of: "start","center","end","nearest" (null => "nearest")
*/
void scrollIntoView(String elementId, String align);
}

View File

@@ -10,62 +10,25 @@ import java.util.Optional;
*/
public interface HostServices {
/**
* Returns an optional UI host.
*
* @return ui host
*/
Optional<UiHost> ui();
/**
* Returns an optional DOM host.
*
* @return dom host
*/
Optional<DomHost> dom();
/**
* Returns an optional event host.
*
* @return event host
*/
Optional<EventHost> events();
/**
* Returns an optional resource host.
*
* @return resource host
*/
Optional<ResourceHost> resources();
/**
* Returns an optional console host.
*
* @return console host
*/
Optional<ConsoleHost> console();
/**
* Returns an optional audio host.
*
* @return audio host
*/
Optional<AudioHost> audio();
/**
* Returns an optional image host.
*
* @return image host
*/
Optional<ImageHost> image();
/**
* Returns an optional video host.
*
* @return video host
*/
Optional<VideoHost> video();
/* NEW */
Optional<SchedulerHost> scheduler();
Optional<SelectorHost> selector();
Optional<GeometryHost> geometry();
Optional<CssHost> css();
Optional<StorageHost> storage();
Optional<UtilHost> util();
Optional<ClipboardHost> clipboard();
Optional<ObserverHost> observers();
/**
* Simple immutable implementation.
*/
@@ -80,18 +43,15 @@ public interface HostServices {
private final ImageHost imageHost;
private final VideoHost videoHost;
/**
* Creates a HostServices container.
*
* @param ui ui host
* @param dom dom host
* @param events event host
* @param resources resource host
* @param console console host
* @param audioHost audio host
* @param imageHost image host
* @param videoHost video host
*/
private final SchedulerHost schedulerHost;
private final SelectorHost selectorHost;
private final GeometryHost geometryHost;
private final CssHost cssHost;
private final StorageHost storageHost;
private final UtilHost utilHost;
private final ClipboardHost clipboardHost;
private final ObserverHost observerHost;
public Default(
UiHost ui,
DomHost dom,
@@ -100,7 +60,15 @@ public interface HostServices {
ConsoleHost console,
AudioHost audioHost,
ImageHost imageHost,
VideoHost videoHost
VideoHost videoHost,
SchedulerHost schedulerHost,
SelectorHost selectorHost,
GeometryHost geometryHost,
CssHost cssHost,
StorageHost storageHost,
UtilHost utilHost,
ClipboardHost clipboardHost,
ObserverHost observerHost
) {
this.ui = ui;
this.dom = dom;
@@ -110,47 +78,33 @@ public interface HostServices {
this.audioHost = audioHost;
this.imageHost = imageHost;
this.videoHost = videoHost;
this.schedulerHost = schedulerHost;
this.selectorHost = selectorHost;
this.geometryHost = geometryHost;
this.cssHost = cssHost;
this.storageHost = storageHost;
this.utilHost = utilHost;
this.clipboardHost = clipboardHost;
this.observerHost = observerHost;
}
@Override
public Optional<UiHost> ui() {
return Optional.ofNullable(ui);
}
@Override public Optional<UiHost> ui() { return Optional.ofNullable(ui); }
@Override public Optional<DomHost> dom() { return Optional.ofNullable(dom); }
@Override public Optional<EventHost> events() { return Optional.ofNullable(events); }
@Override public Optional<ResourceHost> resources() { return Optional.ofNullable(resources); }
@Override public Optional<ConsoleHost> console() { return Optional.ofNullable(console); }
@Override public Optional<AudioHost> audio() { return Optional.ofNullable(audioHost); }
@Override public Optional<ImageHost> image() { return Optional.ofNullable(imageHost); }
@Override public Optional<VideoHost> video() { return Optional.ofNullable(videoHost); }
@Override
public Optional<DomHost> dom() {
return Optional.ofNullable(dom);
}
@Override
public Optional<EventHost> events() {
return Optional.ofNullable(events);
}
@Override
public Optional<ResourceHost> resources() {
return Optional.ofNullable(resources);
}
@Override
public Optional<ConsoleHost> console() {
return Optional.ofNullable(console);
}
@Override
public Optional<AudioHost> audio() {
return Optional.ofNullable(audioHost);
}
@Override
public Optional<ImageHost> image() {
return Optional.ofNullable(imageHost);
}
@Override
public Optional<VideoHost> video() {
return Optional.ofNullable(videoHost);
}
@Override public Optional<SchedulerHost> scheduler() { return Optional.ofNullable(schedulerHost); }
@Override public Optional<SelectorHost> selector() { return Optional.ofNullable(selectorHost); }
@Override public Optional<GeometryHost> geometry() { return Optional.ofNullable(geometryHost); }
@Override public Optional<CssHost> css() { return Optional.ofNullable(cssHost); }
@Override public Optional<StorageHost> storage() { return Optional.ofNullable(storageHost); }
@Override public Optional<UtilHost> util() { return Optional.ofNullable(utilHost); }
@Override public Optional<ClipboardHost> clipboard() { return Optional.ofNullable(clipboardHost); }
@Override public Optional<ObserverHost> observers() { return Optional.ofNullable(observerHost); }
}
/**
@@ -159,11 +113,6 @@ public interface HostServices {
final class StdoutConsole implements ConsoleHost {
private final String prefix;
/**
* Creates a new stdout console with a prefix.
*
* @param prefix prefix (may be empty)
*/
public StdoutConsole(String prefix) {
this.prefix = Objects.requireNonNull(prefix, "prefix");
}
@@ -172,29 +121,10 @@ public interface HostServices {
return s == null ? "" : s;
}
@Override
public void info(String message) {
System.out.println(prefix + "[info] " + safe(message));
}
@Override
public void log(String message) {
System.out.println(prefix + "[log] " + safe(message));
}
@Override
public void warn(String message) {
System.out.println(prefix + "[warn] " + safe(message));
}
@Override
public void error(String message) {
System.err.println(prefix + "[error] " + safe(message));
}
@Override
public void exception(String message) {
System.err.println(prefix + "[exception] " + safe(message));
}
@Override public void info(String message) { System.out.println(prefix + "[info] " + safe(message)); }
@Override public void log(String message) { System.out.println(prefix + "[log] " + safe(message)); }
@Override public void warn(String message) { System.out.println(prefix + "[warn] " + safe(message)); }
@Override public void error(String message) { System.err.println(prefix + "[error] " + safe(message)); }
@Override public void exception(String message) { System.err.println(prefix + "[exception] " + safe(message)); }
}
}

View File

@@ -0,0 +1,80 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.Map;
/**
* Reactive observers: Mutation/Resize/Intersection.
*
* <p>Observers emit events into Lua via a host-provided callback.</p>
*/
public interface ObserverHost {
/**
* Callback used by the host to notify observers.
*/
@FunctionalInterface
interface ObserverCallback {
/**
* Called on observer events.
*
* @param type observer type ("mutation","resize","intersection")
* @param targetId element id
* @param data payload map
*/
void onEvent(String type, String targetId, Map<String, Object> data);
}
/**
* Sets the callback to receive observer events.
*
* @param callback callback (nullable to disable)
*/
void setCallback(ObserverCallback callback);
/**
* Observes DOM mutations on an element (subtree).
*
* @param elementId element id
* @param subtree true to include subtree
* @param attributes true to observe attributes
* @param childList true to observe childList
* @param characterData true to observe text changes
*/
void observeMutations(String elementId, boolean subtree, boolean attributes, boolean childList, boolean characterData);
/**
* Stops mutation observing for an element.
*
* @param elementId element id
*/
void unobserveMutations(String elementId);
/**
* Observes resize changes of an element.
*
* @param elementId element id
*/
void observeResize(String elementId);
/**
* Stops resize observing for an element.
*
* @param elementId element id
*/
void unobserveResize(String elementId);
/**
* Observes intersection changes of an element with viewport.
*
* @param elementId element id
* @param threshold threshold in [0..1]
*/
void observeIntersection(String elementId, double threshold);
/**
* Stops intersection observing for an element.
*
* @param elementId element id
*/
void unobserveIntersection(String elementId);
}

View File

@@ -0,0 +1,53 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Scheduling primitives comparable to JavaScript timers.
*
* <p>No networking. This host only provides time-based callbacks.</p>
*/
public interface SchedulerHost {
/**
* Schedules a one-shot callback after a delay.
*
* @param delayMillis delay in milliseconds (>= 0)
* @param callback callback to run on host-defined thread (typically Lua thread)
* @return handle id
*/
long setTimeout(long delayMillis, Runnable callback);
/**
* Schedules a repeating callback with fixed rate.
*
* @param intervalMillis interval in milliseconds (> 0)
* @param callback callback to run
* @return handle id
*/
long setInterval(long intervalMillis, Runnable callback);
/**
* Cancels a timeout/interval handle.
*
* @param handle handle id
* @return true if canceled
*/
boolean clear(long handle);
/**
* Schedules a callback for the next animation frame.
*
* <p>Comparable to requestAnimationFrame. The callback is invoked once.</p>
*
* @param callback callback to run
* @return handle id
*/
long requestAnimationFrame(Runnable callback);
/**
* Cancels a previously scheduled animation frame.
*
* @param handle handle id
* @return true if canceled
*/
boolean cancelAnimationFrame(long handle);
}

View File

@@ -0,0 +1,45 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.List;
/**
* CSS selector based DOM querying/traversal.
*
* <p>All returned elements are identified by stable element ids.</p>
*/
public interface SelectorHost {
/**
* Returns the first element matching the selector, or null.
*
* @param selector CSS selector
* @return element id or null
*/
String querySelector(String selector);
/**
* Returns all elements matching the selector.
*
* @param selector CSS selector
* @return list of element ids (never null)
*/
List<String> querySelectorAll(String selector);
/**
* Checks if an element matches a selector.
*
* @param elementId element id
* @param selector CSS selector
* @return true if matches
*/
boolean matches(String elementId, String selector);
/**
* Returns the closest ancestor (including itself) matching selector, or null.
*
* @param elementId element id
* @param selector CSS selector
* @return closest element id or null
*/
String closest(String elementId, String selector);
}

View File

@@ -0,0 +1,67 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.List;
/**
* Storage primitives comparable to localStorage/sessionStorage.
*/
public interface StorageHost {
/**
* @return keys in localStorage
*/
List<String> localKeys();
/**
* @param key key
* @return value or null
*/
String localGet(String key);
/**
* @param key key
* @param value value (null removes)
*/
void localSet(String key, String value);
/**
* Removes a key from localStorage.
*
* @param key key
*/
void localRemove(String key);
/**
* Clears localStorage.
*/
void localClear();
/**
* @return keys in sessionStorage
*/
List<String> sessionKeys();
/**
* @param key key
* @return value or null
*/
String sessionGet(String key);
/**
* @param key key
* @param value value (null removes)
*/
void sessionSet(String key, String value);
/**
* Removes a key from sessionStorage.
*
* @param key key
*/
void sessionRemove(String key);
/**
* Clears sessionStorage.
*/
void sessionClear();
}

View File

@@ -0,0 +1,70 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.List;
import java.util.Map;
/**
* Utility helpers commonly available in browsers.
*
* <p>No networking.</p>
*/
public interface UtilHost {
/**
* Encodes bytes (UTF-8) as Base64.
*
* @param text text
* @return base64
*/
String base64Encode(String text);
/**
* Decodes Base64 into UTF-8 text.
*
* @param base64 base64
* @return decoded text
*/
String base64Decode(String base64);
/**
* Generates cryptographically-strong random bytes and returns as hex string.
*
* @param numBytes number of bytes (>0)
* @return hex string
*/
String randomHex(int numBytes);
/**
* Parses a URL string into components.
*
* @param url url string
* @return map containing scheme,host,port,path,query,fragment
*/
Map<String, String> parseUrl(String url);
/**
* Parses a query string into key->list(values).
*
* @param query query string (with or without leading '?')
* @return map key->values
*/
Map<String, List<String>> parseQuery(String query);
/**
* JSON.stringify via browser engine (returns JSON string).
*
* @param elementId element id that provides the JS context (ignored by some engines but kept for safety)
* @param jsExpr JS expression returning a JSON-serializable value
* @return JSON string
*/
String jsonStringifyExpr(String elementId, String jsExpr);
/**
* JSON.parse via browser engine and returns normalized JSON string (stringify(parse(x))).
*
* @param elementId element id providing JS context
* @param json json string
* @return normalized json
*/
String jsonNormalize(String elementId, String json);
}

View File

@@ -80,14 +80,27 @@ public final class FxLuaScriptEngine implements AutoCloseable {
.sandbox(true)
);
HostServices.StdoutConsole console = new HostServices.StdoutConsole("[lua] ");
FxUiHost uiHost = new FxUiHost(engine, dom);
FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine);
FxEventHost eventHost = new FxEventHost(dom);
FxAudioHost audioHost = new FxAudioHost();
FxVideoHost videoHost = new FxVideoHost(engine, dom);
FxImageHost imageHost = new FxImageHost(engine, dom);
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console, audioHost, imageHost, videoHost);
HostServices services = new HostServices.Default(
new FxUiHost(engine, dom),
dom,
eventHost,
new FxWebViewResourceHost(engine),
new HostServices.StdoutConsole("[lua] "),
new FxAudioHost(),
new FxImageHost(engine, dom),
new FxVideoHost(engine, dom),
new FxSchedulerHost(),
new FxSelectorHost(engine, dom),
new FxGeometryHost(engine, dom),
new FxCssHost(engine, dom),
new FxStorageHost(engine, dom),
new FxUtilHost(engine, dom),
new FxClipboardHost(),
new FxObserverHost(engine, dom)
);
LuaRuntime rt = new LuaRuntime(globals, services, policy);
eventHost.setRouter(rt.eventRouter());

View File

@@ -0,0 +1,43 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.*;
import org.openautonomousconnection.luascript.hosts.ClipboardHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
/**
* Lua table: clipboard
*
* <p>Functions:</p>
* <ul>
* <li>clipboard.set(text)</li>
* <li>clipboard.get() -> text</li>
* </ul>
*/
public final class ClipboardTable extends ScriptTable {
public ClipboardTable() {
super("clipboard");
}
@Override
protected void define(HostServices services) {
ClipboardHost host = services.clipboard().orElseThrow(() -> new IllegalStateException("ClipboardHost not provided"));
table().set("set", new OneArgFunction() {
@Override
public LuaValue call(LuaValue text) {
host.setText(text.isnil() ? "" : text.tojstring());
return LuaValue.NIL;
}
});
table().set("get", new ZeroArgFunction() {
@Override
public LuaValue call() {
return LuaValue.valueOf(host.getText());
}
});
}
}

View File

@@ -0,0 +1,86 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.*;
import org.openautonomousconnection.luascript.events.JavaToLua;
import org.openautonomousconnection.luascript.hosts.CssHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import java.util.Map;
/**
* Lua table: css
*
* <p>Functions:</p>
* <ul>
* <li>css.computed(id, prop) -> string</li>
* <li>css.computedMany(id, {props...}) -> map</li>
* <li>css.inlineGet(id, prop) -> string</li>
* <li>css.inlineSet(id, prop, value)</li>
* <li>css.varGet(id, name) -> string</li>
* <li>css.varSet(id, name, value)</li>
* </ul>
*/
public final class CssTable extends ScriptTable {
public CssTable() {
super("css");
}
@Override
protected void define(HostServices services) {
CssHost host = services.css().orElseThrow(() -> new IllegalStateException("CssHost not provided"));
table().set("computed", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue prop) {
return LuaValue.valueOf(host.getComputedStyle(id.checkjstring(), prop.checkjstring()));
}
});
table().set("computedMany", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue propsTable) {
if (!propsTable.istable()) throw new IllegalArgumentException("props must be a table");
int n = propsTable.length();
String[] props = new String[n];
for (int i = 1; i <= n; i++) {
props[i - 1] = propsTable.get(i).checkjstring();
}
Map<String, String> m = host.getComputedStyles(id.checkjstring(), props);
return JavaToLua.coerce(m);
}
});
table().set("inlineGet", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue prop) {
return LuaValue.valueOf(host.getInlineStyle(id.checkjstring(), prop.checkjstring()));
}
});
table().set("inlineSet", new ThreeArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue prop, LuaValue value) {
host.setInlineStyle(id.checkjstring(), prop.checkjstring(), value.isnil() ? "" : value.tojstring());
return LuaValue.NIL;
}
});
table().set("varGet", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue name) {
return LuaValue.valueOf(host.getCssVariable(id.checkjstring(), name.checkjstring()));
}
});
table().set("varSet", new ThreeArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue name, LuaValue value) {
host.setCssVariable(id.checkjstring(), name.checkjstring(), value.isnil() ? "" : value.tojstring());
return LuaValue.NIL;
}
});
}
}

View File

@@ -0,0 +1,70 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.lib.TwoArgFunction;
import org.luaj.vm2.lib.VarArgFunction;
import org.luaj.vm2.Varargs;
import org.openautonomousconnection.luascript.events.JavaToLua;
import org.openautonomousconnection.luascript.hosts.GeometryHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import java.util.Map;
/**
* Lua table: geometry
*
* <p>Functions:</p>
* <ul>
* <li>geometry.rect(id) -> map</li>
* <li>geometry.viewport() -> map</li>
* <li>geometry.scrollTo(x,y)</li>
* <li>geometry.scrollIntoView(id, align?)</li>
* </ul>
*/
public final class GeometryTable extends ScriptTable {
public GeometryTable() {
super("geometry");
}
@Override
protected void define(HostServices services) {
GeometryHost host = services.geometry().orElseThrow(() -> new IllegalStateException("GeometryHost not provided"));
table().set("rect", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
Map<String, Object> m = host.getBoundingClientRect(id.checkjstring());
return JavaToLua.coerce(m);
}
});
table().set("viewport", new org.luaj.vm2.lib.ZeroArgFunction() {
@Override
public LuaValue call() {
return JavaToLua.coerce(host.getViewport());
}
});
table().set("scrollTo", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
host.scrollTo(args.arg(1).checkdouble(), args.arg(2).checkdouble());
return LuaValue.NIL;
}
});
table().set("scrollIntoView", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
String id = args.arg(1).checkjstring();
String align = args.narg() >= 2 && !args.arg(2).isnil() ? args.arg(2).tojstring() : null;
host.scrollIntoView(id, align);
return LuaValue.NIL;
}
});
}
}

View File

@@ -0,0 +1,103 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaFunction;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.lib.*;
import org.openautonomousconnection.luascript.events.JavaToLua;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.ObserverHost;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import java.util.Map;
/**
* Lua table: observers
*
* <p>Functions:</p>
* <ul>
* <li>observers.on(fn(type, targetId, dataTable))</li>
* <li>observers.mutationObserve(id, subtree, attributes, childList, characterData)</li>
* <li>observers.mutationUnobserve(id)</li>
* <li>observers.resizeObserve(id)</li>
* <li>observers.resizeUnobserve(id)</li>
* <li>observers.intersectionObserve(id, threshold)</li>
* <li>observers.intersectionUnobserve(id)</li>
* </ul>
*/
public final class ObserversTable extends ScriptTable {
public ObserversTable() {
super("observers");
}
@Override
protected void define(HostServices services) {
ObserverHost host = services.observers().orElseThrow(() -> new IllegalStateException("ObserverHost not provided"));
table().set("on", new OneArgFunction() {
@Override
public LuaValue call(LuaValue fn) {
LuaFunction cb = fn.checkfunction();
host.setCallback((type, targetId, data) -> {
LuaValue luaData = JavaToLua.coerce(data);
cb.call(LuaValue.valueOf(type), LuaValue.valueOf(targetId), luaData);
});
return LuaValue.NIL;
}
});
table().set("mutationObserve", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
String id = args.arg(1).checkjstring();
boolean subtree = args.arg(2).optboolean(false);
boolean attributes = args.arg(3).optboolean(true);
boolean childList = args.arg(4).optboolean(true);
boolean characterData = args.arg(5).optboolean(false);
host.observeMutations(id, subtree, attributes, childList, characterData);
return LuaValue.NIL;
}
});
table().set("mutationUnobserve", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
host.unobserveMutations(id.checkjstring());
return LuaValue.NIL;
}
});
table().set("resizeObserve", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
host.observeResize(id.checkjstring());
return LuaValue.NIL;
}
});
table().set("resizeUnobserve", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
host.unobserveResize(id.checkjstring());
return LuaValue.NIL;
}
});
table().set("intersectionObserve", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue threshold) {
host.observeIntersection(id.checkjstring(), threshold.checkdouble());
return LuaValue.NIL;
}
});
table().set("intersectionUnobserve", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
host.unobserveIntersection(id.checkjstring());
return LuaValue.NIL;
}
});
}
}

View File

@@ -0,0 +1,78 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaFunction;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.lib.TwoArgFunction;
import org.luaj.vm2.lib.VarArgFunction;
import org.luaj.vm2.Varargs;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.SchedulerHost;
import org.openautonomousconnection.luascript.utils.ScriptTable;
/**
* Lua table: scheduler
*
* <p>Functions:</p>
* <ul>
* <li>scheduler.timeout(ms, fn) -> id</li>
* <li>scheduler.interval(ms, fn) -> id</li>
* <li>scheduler.clear(id) -> boolean</li>
* <li>scheduler.raf(fn) -> id</li>
* <li>scheduler.cancelRaf(id) -> boolean</li>
* </ul>
*/
public final class SchedulerTable extends ScriptTable {
public SchedulerTable() {
super("scheduler");
}
@Override
protected void define(HostServices services) {
SchedulerHost host = services.scheduler().orElseThrow(() -> new IllegalStateException("SchedulerHost not provided"));
table().set("timeout", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue ms, LuaValue fn) {
long delay = ms.checklong();
LuaFunction cb = fn.checkfunction();
long id = host.setTimeout(delay, () -> cb.call());
return LuaValue.valueOf(id);
}
});
table().set("interval", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue ms, LuaValue fn) {
long interval = ms.checklong();
LuaFunction cb = fn.checkfunction();
long id = host.setInterval(interval, () -> cb.call());
return LuaValue.valueOf(id);
}
});
table().set("clear", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
return LuaValue.valueOf(host.clear(id.checklong()));
}
});
table().set("raf", new OneArgFunction() {
@Override
public LuaValue call(LuaValue fn) {
LuaFunction cb = fn.checkfunction();
long id = host.requestAnimationFrame(() -> cb.call());
return LuaValue.valueOf(id);
}
});
table().set("cancelRaf", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
return LuaValue.valueOf(host.cancelAnimationFrame(id.checklong()));
}
});
}
}

View File

@@ -0,0 +1,74 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.TwoArgFunction;
import org.luaj.vm2.lib.OneArgFunction;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.SelectorHost;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import java.util.List;
/**
* Lua table: selector
*
* <p>Functions:</p>
* <ul>
* <li>selector.one(css) -> id|nil</li>
* <li>selector.all(css) -> { ids... }</li>
* <li>selector.matches(id, css) -> boolean</li>
* <li>selector.closest(id, css) -> id|nil</li>
* </ul>
*/
public final class SelectorTable extends ScriptTable {
public SelectorTable() {
super("selector");
}
private static LuaValue toLuaArray(List<String> values) {
LuaTable t = new LuaTable();
if (values == null || values.isEmpty()) return t;
int i = 1;
for (String v : values) {
t.set(i++, v == null ? LuaValue.NIL : LuaValue.valueOf(v));
}
return t;
}
@Override
protected void define(HostServices services) {
SelectorHost host = services.selector().orElseThrow(() -> new IllegalStateException("SelectorHost not provided"));
table().set("one", new OneArgFunction() {
@Override
public LuaValue call(LuaValue css) {
String id = host.querySelector(css.checkjstring());
return id == null ? LuaValue.NIL : LuaValue.valueOf(id);
}
});
table().set("all", new OneArgFunction() {
@Override
public LuaValue call(LuaValue css) {
return toLuaArray(host.querySelectorAll(css.checkjstring()));
}
});
table().set("matches", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue css) {
return LuaValue.valueOf(host.matches(id.checkjstring(), css.checkjstring()));
}
});
table().set("closest", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue css) {
String out = host.closest(id.checkjstring(), css.checkjstring());
return out == null ? LuaValue.NIL : LuaValue.valueOf(out);
}
});
}
}

View File

@@ -0,0 +1,121 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.*;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.StorageHost;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import java.util.List;
/**
* Lua table: storage
*
* <p>Functions:</p>
* <ul>
* <li>storage.localGet(key) -> string|nil</li>
* <li>storage.localSet(key, value|nil)</li>
* <li>storage.localKeys() -> {keys...}</li>
* <li>storage.localRemove(key)</li>
* <li>storage.localClear()</li>
* <li>same for session*</li>
* </ul>
*/
public final class StorageTable extends ScriptTable {
public StorageTable() {
super("storage");
}
private static LuaValue toLuaArray(List<String> values) {
LuaTable t = new LuaTable();
if (values == null || values.isEmpty()) return t;
int i = 1;
for (String v : values) t.set(i++, v == null ? LuaValue.NIL : LuaValue.valueOf(v));
return t;
}
@Override
protected void define(HostServices services) {
StorageHost host = services.storage().orElseThrow(() -> new IllegalStateException("StorageHost not provided"));
table().set("localKeys", new ZeroArgFunction() {
@Override
public LuaValue call() {
return toLuaArray(host.localKeys());
}
});
table().set("localGet", new OneArgFunction() {
@Override
public LuaValue call(LuaValue key) {
String v = host.localGet(key.checkjstring());
return v == null ? LuaValue.NIL : LuaValue.valueOf(v);
}
});
table().set("localSet", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue key, LuaValue value) {
host.localSet(key.checkjstring(), value.isnil() ? null : value.tojstring());
return LuaValue.NIL;
}
});
table().set("localRemove", new OneArgFunction() {
@Override
public LuaValue call(LuaValue key) {
host.localRemove(key.checkjstring());
return LuaValue.NIL;
}
});
table().set("localClear", new ZeroArgFunction() {
@Override
public LuaValue call() {
host.localClear();
return LuaValue.NIL;
}
});
table().set("sessionKeys", new ZeroArgFunction() {
@Override
public LuaValue call() {
return toLuaArray(host.sessionKeys());
}
});
table().set("sessionGet", new OneArgFunction() {
@Override
public LuaValue call(LuaValue key) {
String v = host.sessionGet(key.checkjstring());
return v == null ? LuaValue.NIL : LuaValue.valueOf(v);
}
});
table().set("sessionSet", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue key, LuaValue value) {
host.sessionSet(key.checkjstring(), value.isnil() ? null : value.tojstring());
return LuaValue.NIL;
}
});
table().set("sessionRemove", new OneArgFunction() {
@Override
public LuaValue call(LuaValue key) {
host.sessionRemove(key.checkjstring());
return LuaValue.NIL;
}
});
table().set("sessionClear", new ZeroArgFunction() {
@Override
public LuaValue call() {
host.sessionClear();
return LuaValue.NIL;
}
});
}
}

View File

@@ -0,0 +1,83 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.*;
import org.openautonomousconnection.luascript.events.JavaToLua;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.UtilHost;
import org.openautonomousconnection.luascript.utils.ScriptTable;
/**
* Lua table: util
*
* <p>Functions:</p>
* <ul>
* <li>util.base64Encode(text)</li>
* <li>util.base64Decode(b64)</li>
* <li>util.randomHex(bytes)</li>
* <li>util.parseUrl(url) -> map</li>
* <li>util.parseQuery(query) -> map(key->array)</li>
* <li>util.jsonStringifyExpr(elementId, jsExpr) -> jsonString</li>
* <li>util.jsonNormalize(elementId, json) -> jsonString</li>
* </ul>
*/
public final class UtilTable extends ScriptTable {
public UtilTable() {
super("util");
}
@Override
protected void define(HostServices services) {
UtilHost host = services.util().orElseThrow(() -> new IllegalStateException("UtilHost not provided"));
table().set("base64Encode", new OneArgFunction() {
@Override
public LuaValue call(LuaValue text) {
return LuaValue.valueOf(host.base64Encode(text.isnil() ? "" : text.tojstring()));
}
});
table().set("base64Decode", new OneArgFunction() {
@Override
public LuaValue call(LuaValue b64) {
return LuaValue.valueOf(host.base64Decode(b64.isnil() ? "" : b64.tojstring()));
}
});
table().set("randomHex", new OneArgFunction() {
@Override
public LuaValue call(LuaValue bytes) {
return LuaValue.valueOf(host.randomHex(bytes.checkint()));
}
});
table().set("parseUrl", new OneArgFunction() {
@Override
public LuaValue call(LuaValue url) {
return JavaToLua.coerce(host.parseUrl(url.checkjstring()));
}
});
table().set("parseQuery", new OneArgFunction() {
@Override
public LuaValue call(LuaValue q) {
return JavaToLua.coerce(host.parseQuery(q.isnil() ? "" : q.tojstring()));
}
});
table().set("jsonStringifyExpr", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue elementId, LuaValue jsExpr) {
return LuaValue.valueOf(host.jsonStringifyExpr(elementId.checkjstring(), jsExpr.checkjstring()));
}
});
table().set("jsonNormalize", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue elementId, LuaValue json) {
return LuaValue.valueOf(host.jsonNormalize(elementId.checkjstring(), json.checkjstring()));
}
});
}
}

View File

@@ -1,15 +0,0 @@
package org.openautonomousconnection.luascript.values;
import org.luaj.vm2.LuaValue;
public class LuaObject extends LuaValue {
@Override
public int type() {
return 0;
}
@Override
public String typename() {
return "";
}
}