first commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package org.openautonomousconnection.luascript.dom.jsoup;
|
||||
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.openautonomousconnection.luascript.hosts.DomHost;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* DomHost implementation backed by a jsoup Document.
|
||||
*
|
||||
* <p>Element identity is the HTML attribute {@code id}. This implementation auto-assigns ids to all
|
||||
* elements that do not have one, making them addressable.</p>
|
||||
*/
|
||||
public final class JsoupDomHost implements DomHost {
|
||||
|
||||
private final Document document;
|
||||
private final AtomicLong autoIdSeq = new AtomicLong(1);
|
||||
|
||||
public JsoupDomHost(Document document) {
|
||||
this.document = Objects.requireNonNull(document, "document");
|
||||
ensureAllElementsHaveId();
|
||||
}
|
||||
|
||||
public Document document() {
|
||||
return document;
|
||||
}
|
||||
|
||||
public void ensureAllElementsHaveId() {
|
||||
Elements all = document.getAllElements();
|
||||
for (Element el : all) {
|
||||
if (el == document) continue;
|
||||
if (!hasUsableId(el)) {
|
||||
el.attr("id", generateUniqueId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAllElementIds() {
|
||||
Elements all = document.getAllElements();
|
||||
List<String> ids = new ArrayList<>(all.size());
|
||||
for (Element el : all) {
|
||||
if (el == document) continue;
|
||||
String id = el.id();
|
||||
if (id != null && !id.isBlank()) ids.add(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getAttributes(String elementId) {
|
||||
Element el = byId(elementId);
|
||||
Map<String, String> out = new LinkedHashMap<>();
|
||||
el.attributes().forEach(a -> out.put(a.getKey(), a.getValue()));
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTagName(String elementId) {
|
||||
return byId(elementId).tagName().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTextContent(String elementId) {
|
||||
return byId(elementId).text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTextContent(String elementId, String text) {
|
||||
byId(elementId).text(text == null ? "" : text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttribute(String elementId, String name) {
|
||||
Element el = byId(elementId);
|
||||
String n = normalizeAttr(name);
|
||||
if (!el.hasAttr(n)) return null;
|
||||
return el.attr(n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String elementId, String name, String value) {
|
||||
Element el = byId(elementId);
|
||||
el.attr(normalizeAttr(name), value == null ? "" : value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String elementId, String name) {
|
||||
byId(elementId).removeAttr(normalizeAttr(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParentId(String elementId) {
|
||||
Element el = byId(elementId);
|
||||
Element p = el.parent();
|
||||
if (p == null || p == document) return null;
|
||||
if (!hasUsableId(p)) p.attr("id", generateUniqueId());
|
||||
return p.id();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getChildrenIds(String elementId) {
|
||||
Element el = byId(elementId);
|
||||
List<String> out = new ArrayList<>();
|
||||
for (Element child : el.children()) {
|
||||
if (!hasUsableId(child)) child.attr("id", generateUniqueId());
|
||||
out.add(child.id());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createElement(String tagName, String requestedId) {
|
||||
String tag = normalizeTag(tagName);
|
||||
String id = normalizeOrGenerateId(requestedId);
|
||||
if (exists(id)) throw new IllegalArgumentException("Element id already exists: " + id);
|
||||
|
||||
Element el = document.createElement(tag);
|
||||
el.attr("id", id);
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeElement(String elementId) {
|
||||
byId(elementId).remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendChild(String parentId, String childId) {
|
||||
Element parent = byId(parentId);
|
||||
Element child = byId(childId);
|
||||
child.remove();
|
||||
parent.appendChild(child);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertBefore(String parentId, String childId, String beforeChildId) {
|
||||
Element parent = byId(parentId);
|
||||
Element child = byId(childId);
|
||||
Element before = byId(beforeChildId);
|
||||
|
||||
if (before.parent() == null || !Objects.equals(before.parent(), parent)) {
|
||||
throw new IllegalArgumentException("beforeChildId is not a direct child of parentId: " + beforeChildId);
|
||||
}
|
||||
|
||||
child.remove();
|
||||
before.before(child);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String id) {
|
||||
if (id == null || id.isBlank()) return false;
|
||||
return document.getElementById(id) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> queryByTag(String tagName) {
|
||||
String tag = normalizeTag(tagName);
|
||||
Elements els = document.getElementsByTag(tag);
|
||||
|
||||
List<String> out = new ArrayList<>(els.size());
|
||||
for (Element el : els) {
|
||||
if (!hasUsableId(el)) el.attr("id", generateUniqueId());
|
||||
out.add(el.id());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> queryByClass(String className) {
|
||||
String cls = normalizeCssIdent(className);
|
||||
Elements els = document.getElementsByClass(cls);
|
||||
|
||||
List<String> out = new ArrayList<>(els.size());
|
||||
for (Element el : els) {
|
||||
if (!hasUsableId(el)) el.attr("id", generateUniqueId());
|
||||
out.add(el.id());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Element byId(String id) {
|
||||
if (id == null || id.isBlank()) throw new IllegalArgumentException("elementId is blank");
|
||||
Element el = document.getElementById(id);
|
||||
if (el == null) throw new IllegalArgumentException("Unknown element id: " + id);
|
||||
return el;
|
||||
}
|
||||
|
||||
private boolean hasUsableId(Element el) {
|
||||
String id = el.id();
|
||||
return id != null && !id.isBlank();
|
||||
}
|
||||
|
||||
private String generateUniqueId() {
|
||||
while (true) {
|
||||
String id = "__auto_" + autoIdSeq.getAndIncrement();
|
||||
if (!exists(id)) return id;
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeTag(String tagName) {
|
||||
if (tagName == null) throw new IllegalArgumentException("tagName is null");
|
||||
String t = tagName.trim().toLowerCase(Locale.ROOT);
|
||||
if (t.isEmpty()) throw new IllegalArgumentException("tagName is empty");
|
||||
return t;
|
||||
}
|
||||
|
||||
private String normalizeAttr(String name) {
|
||||
if (name == null) throw new IllegalArgumentException("attribute name is null");
|
||||
String n = name.trim();
|
||||
if (n.isEmpty()) throw new IllegalArgumentException("attribute name is empty");
|
||||
return n;
|
||||
}
|
||||
|
||||
private String normalizeCssIdent(String s) {
|
||||
if (s == null) throw new IllegalArgumentException("identifier is null");
|
||||
String v = s.trim();
|
||||
if (v.isEmpty()) throw new IllegalArgumentException("identifier is empty");
|
||||
return v;
|
||||
}
|
||||
|
||||
private String normalizeOrGenerateId(String requestedId) {
|
||||
if (requestedId != null) {
|
||||
String id = requestedId.trim();
|
||||
if (!id.isEmpty()) return id;
|
||||
}
|
||||
return generateUniqueId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.openautonomousconnection.luascript.events;
|
||||
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Coerces common Java values into Lua values.
|
||||
*/
|
||||
public final class JavaToLua {
|
||||
|
||||
private JavaToLua() { }
|
||||
|
||||
public static LuaValue coerce(Object v) {
|
||||
if (v == null) return LuaValue.NIL;
|
||||
if (v instanceof LuaValue lv) return lv;
|
||||
if (v instanceof String s) return LuaValue.valueOf(s);
|
||||
if (v instanceof Boolean b) return LuaValue.valueOf(b);
|
||||
if (v instanceof Integer i) return LuaValue.valueOf(i);
|
||||
if (v instanceof Long l) return LuaValue.valueOf(l);
|
||||
if (v instanceof Float f) return LuaValue.valueOf(f);
|
||||
if (v instanceof Double d) return LuaValue.valueOf(d);
|
||||
|
||||
if (v instanceof Map<?, ?> m) {
|
||||
LuaTable t = new LuaTable();
|
||||
for (Map.Entry<?, ?> e : m.entrySet()) {
|
||||
Object k = e.getKey();
|
||||
if (k == null) continue;
|
||||
t.set(String.valueOf(k), coerce(e.getValue()));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
if (v instanceof List<?> list) {
|
||||
LuaTable t = new LuaTable();
|
||||
int i = 1;
|
||||
for (Object o : list) {
|
||||
t.set(i++, coerce(o));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
return LuaValue.valueOf(String.valueOf(v));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.openautonomousconnection.luascript.events;
|
||||
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.luaj.vm2.LuaError;
|
||||
import org.luaj.vm2.LuaFunction;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.Varargs;
|
||||
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
||||
import org.openautonomousconnection.luascript.security.LuaSecurityManager;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Stores bindings (elementId,eventName -> handlerPath) and dispatches events into Lua.
|
||||
*/
|
||||
public final class LuaEventDispatcher {
|
||||
|
||||
private final Globals globals;
|
||||
private final LuaSecurityManager securityManager;
|
||||
private final LuaExecutionPolicy policy;
|
||||
private final ConcurrentHashMap<String, String> bindings = new ConcurrentHashMap<>();
|
||||
|
||||
public LuaEventDispatcher(Globals globals, LuaSecurityManager securityManager, LuaExecutionPolicy policy) {
|
||||
this.globals = Objects.requireNonNull(globals, "globals");
|
||||
this.securityManager = Objects.requireNonNull(securityManager, "securityManager");
|
||||
this.policy = Objects.requireNonNull(policy, "policy");
|
||||
}
|
||||
|
||||
public void bind(String elementId, String eventName, String handlerPath) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(eventName, "eventName");
|
||||
Objects.requireNonNull(handlerPath, "handlerPath");
|
||||
|
||||
String k = key(elementId, UiEventRegistry.normalize(eventName));
|
||||
bindings.put(k, handlerPath);
|
||||
}
|
||||
|
||||
public void unbind(String elementId, String eventName) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(eventName, "eventName");
|
||||
bindings.remove(key(elementId, UiEventRegistry.normalize(eventName)));
|
||||
}
|
||||
|
||||
public boolean hasBinding(String elementId, String eventName) {
|
||||
return bindings.containsKey(key(elementId, UiEventRegistry.normalize(eventName)));
|
||||
}
|
||||
|
||||
public boolean dispatch(UiEvent event) {
|
||||
Objects.requireNonNull(event, "event");
|
||||
|
||||
String k = key(event.targetId(), UiEventRegistry.normalize(event.type()));
|
||||
String handlerPath = bindings.get(k);
|
||||
if (handlerPath == null) return false;
|
||||
|
||||
LuaValue fn = resolvePath(handlerPath);
|
||||
if (fn.isnil() || !fn.isfunction()) {
|
||||
throw new LuaError("Handler is not a function: " + handlerPath);
|
||||
}
|
||||
|
||||
LuaValue eventTable = toLuaEvent(event);
|
||||
Varargs args = LuaValue.varargsOf(new LuaValue[]{eventTable});
|
||||
|
||||
securityManager.callGuarded(globals, (LuaFunction) fn, args, policy);
|
||||
return true;
|
||||
}
|
||||
|
||||
private LuaValue resolvePath(String path) {
|
||||
String[] parts = path.split("\\.");
|
||||
LuaValue cur = globals;
|
||||
for (String p : parts) {
|
||||
String key = p.trim();
|
||||
if (key.isEmpty()) throw new LuaError("Invalid handler path: " + path);
|
||||
cur = cur.get(key);
|
||||
if (cur.isnil()) return LuaValue.NIL;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
private LuaValue toLuaEvent(UiEvent event) {
|
||||
LuaTable t = new LuaTable();
|
||||
t.set("target", LuaValue.valueOf(event.targetId()));
|
||||
t.set("type", LuaValue.valueOf(event.type()));
|
||||
|
||||
LuaTable d = new LuaTable();
|
||||
for (Map.Entry<String, Object> e : event.data().entrySet()) {
|
||||
String k = e.getKey();
|
||||
if (k == null) continue;
|
||||
d.set(k, JavaToLua.coerce(e.getValue()));
|
||||
}
|
||||
t.set("data", d);
|
||||
return t;
|
||||
}
|
||||
|
||||
private static String key(String elementId, String event) {
|
||||
return elementId + "\n" + event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.openautonomousconnection.luascript.events;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Event passed from host into Lua.
|
||||
*/
|
||||
public final class UiEvent {
|
||||
|
||||
private final String targetId;
|
||||
private final String type;
|
||||
private final Map<String, Object> data;
|
||||
|
||||
public UiEvent(String targetId, String type, Map<String, Object> data) {
|
||||
this.targetId = Objects.requireNonNull(targetId, "targetId");
|
||||
this.type = Objects.requireNonNull(type, "type");
|
||||
this.data = (data == null) ? Collections.emptyMap() : Collections.unmodifiableMap(data);
|
||||
}
|
||||
|
||||
public String targetId() {
|
||||
return targetId;
|
||||
}
|
||||
|
||||
public String type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Map<String, Object> data() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.openautonomousconnection.luascript.events;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Normalizes event names.
|
||||
*/
|
||||
public final class UiEventRegistry {
|
||||
|
||||
private UiEventRegistry() { }
|
||||
|
||||
public static String normalize(String eventName) {
|
||||
if (eventName == null) throw new IllegalArgumentException("eventName is null");
|
||||
String e = eventName.trim().toLowerCase(Locale.ROOT);
|
||||
if (e.isEmpty()) throw new IllegalArgumentException("eventName is empty");
|
||||
return e;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
/**
|
||||
* Host capability for console logging.
|
||||
*/
|
||||
public interface ConsoleHost {
|
||||
/** @param message message */
|
||||
void info(String message);
|
||||
|
||||
/** @param message message */
|
||||
void log(String message);
|
||||
|
||||
/** @param message message */
|
||||
void warn(String message);
|
||||
|
||||
/** @param message message */
|
||||
void error(String message);
|
||||
|
||||
/** @param message message */
|
||||
void exception(String message);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Host capability that exposes a DOM-like API.
|
||||
*
|
||||
* <p>Element identity is the (stable) element id.</p>
|
||||
*/
|
||||
public interface DomHost {
|
||||
|
||||
/**
|
||||
* Returns all element ids known to the renderer (must be stable).
|
||||
*
|
||||
* @return list of element ids
|
||||
*/
|
||||
List<String> getAllElementIds();
|
||||
|
||||
/**
|
||||
* Returns all attributes for the given element id.
|
||||
*
|
||||
* @param elementId element id
|
||||
* @return attributes map (attributeName -> attributeValue)
|
||||
*/
|
||||
Map<String, String> getAttributes(String elementId);
|
||||
|
||||
/**
|
||||
* Returns the tag name of the element (lowercase recommended), e.g. "script", "button".
|
||||
*
|
||||
* @param elementId element id
|
||||
* @return tag name
|
||||
*/
|
||||
String getTagName(String elementId);
|
||||
|
||||
/**
|
||||
* Returns the text content of an element (used for inline <script>...).
|
||||
*
|
||||
* @param elementId element id
|
||||
* @return text content (never null)
|
||||
*/
|
||||
String getTextContent(String elementId);
|
||||
|
||||
/**
|
||||
* Sets the text content of an element.
|
||||
*
|
||||
* @param elementId element id
|
||||
* @param text text
|
||||
*/
|
||||
void setTextContent(String elementId, String text);
|
||||
|
||||
/**
|
||||
* Gets a single attribute or null if missing.
|
||||
*
|
||||
* @param elementId element id
|
||||
* @param name attribute name
|
||||
* @return value or null
|
||||
*/
|
||||
String getAttribute(String elementId, String name);
|
||||
|
||||
/**
|
||||
* Sets an attribute (creates it if missing).
|
||||
*
|
||||
* @param elementId element id
|
||||
* @param name attribute name
|
||||
* @param value attribute value
|
||||
*/
|
||||
void setAttribute(String elementId, String name, String value);
|
||||
|
||||
/**
|
||||
* Removes an attribute.
|
||||
*
|
||||
* @param elementId element id
|
||||
* @param name attribute name
|
||||
*/
|
||||
void removeAttribute(String elementId, String name);
|
||||
|
||||
/**
|
||||
* Returns parent id or null.
|
||||
*
|
||||
* @param elementId element id
|
||||
* @return parent id or null
|
||||
*/
|
||||
String getParentId(String elementId);
|
||||
|
||||
/**
|
||||
* Returns direct children ids.
|
||||
*
|
||||
* @param elementId element id
|
||||
* @return children ids
|
||||
*/
|
||||
List<String> getChildrenIds(String elementId);
|
||||
|
||||
/**
|
||||
* Creates a new element (detached) and returns its id.
|
||||
*
|
||||
* @param tagName tag name
|
||||
* @param requestedId optional requested id, may be null/blank for auto id
|
||||
* @return created element id
|
||||
*/
|
||||
String createElement(String tagName, String requestedId);
|
||||
|
||||
/**
|
||||
* Removes an element from the DOM.
|
||||
*
|
||||
* @param elementId element id
|
||||
*/
|
||||
void removeElement(String elementId);
|
||||
|
||||
/**
|
||||
* Moves/appends child under parent.
|
||||
*
|
||||
* @param parentId parent id
|
||||
* @param childId child id
|
||||
*/
|
||||
void appendChild(String parentId, String childId);
|
||||
|
||||
/**
|
||||
* Inserts child before an existing direct child.
|
||||
*
|
||||
* @param parentId parent id
|
||||
* @param childId child id
|
||||
* @param beforeChildId existing child id
|
||||
*/
|
||||
void insertBefore(String parentId, String childId, String beforeChildId);
|
||||
|
||||
/**
|
||||
* Checks if an element id exists.
|
||||
*
|
||||
* @param id element id
|
||||
* @return true if exists
|
||||
*/
|
||||
boolean exists(String id);
|
||||
|
||||
/**
|
||||
* Returns element ids by tag.
|
||||
*
|
||||
* @param tagName tag
|
||||
* @return ids
|
||||
*/
|
||||
List<String> queryByTag(String tagName);
|
||||
|
||||
/**
|
||||
* Returns element ids by class.
|
||||
*
|
||||
* @param className class
|
||||
* @return ids
|
||||
*/
|
||||
List<String> queryByClass(String className);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
/**
|
||||
* Event subscription abstraction (implemented by the client UI layer).
|
||||
*/
|
||||
public interface EventHost {
|
||||
|
||||
/**
|
||||
* Subscribes to an element event.
|
||||
*
|
||||
* @param elementId element id
|
||||
* @param eventName event name (e.g. click)
|
||||
*/
|
||||
void addListener(String elementId, String eventName);
|
||||
|
||||
/**
|
||||
* Unsubscribes from an element event.
|
||||
*
|
||||
* @param elementId element id
|
||||
* @param eventName event name
|
||||
*/
|
||||
void removeListener(String elementId, String eventName);
|
||||
|
||||
/**
|
||||
* Subscribes to a global event (app/window scope).
|
||||
*
|
||||
* @param eventName event name
|
||||
*/
|
||||
void addGlobalListener(String eventName);
|
||||
|
||||
/**
|
||||
* Unsubscribes from a global event.
|
||||
*
|
||||
* @param eventName event name
|
||||
*/
|
||||
void removeGlobalListener(String eventName);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Service container holding optional host capabilities.
|
||||
*
|
||||
* <p>This avoids one huge "bridge" interface.</p>
|
||||
*/
|
||||
public final class HostServices {
|
||||
|
||||
private final ConsoleHost console;
|
||||
private final DomHost dom;
|
||||
private final EventHost events;
|
||||
private final ResourceHost resources;
|
||||
private final UiHost ui;
|
||||
|
||||
private HostServices(Builder b) {
|
||||
this.console = b.console;
|
||||
this.ui = b.ui;
|
||||
this.dom = b.dom;
|
||||
this.events = b.events;
|
||||
this.resources = b.resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return optional DomHost capability
|
||||
*/
|
||||
public Optional<DomHost> dom() {
|
||||
return Optional.ofNullable(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return optional EventHost capability
|
||||
*/
|
||||
public Optional<EventHost> events() {
|
||||
return Optional.ofNullable(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return optional ResourceHost capability
|
||||
*/
|
||||
public Optional<ResourceHost> resources() {
|
||||
return Optional.ofNullable(resources);
|
||||
}
|
||||
|
||||
/** @return optional console host */
|
||||
public Optional<ConsoleHost> console() {
|
||||
return Optional.ofNullable(console);
|
||||
}
|
||||
|
||||
/** @return optional ui host */
|
||||
public Optional<UiHost> ui() {
|
||||
return Optional.ofNullable(ui);
|
||||
}
|
||||
|
||||
/** @return builder */
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for HostServices.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private ConsoleHost console;
|
||||
private DomHost dom;
|
||||
private EventHost events;
|
||||
private ResourceHost resources;
|
||||
private UiHost ui;
|
||||
|
||||
public Builder console(ConsoleHost console) {
|
||||
this.console = Objects.requireNonNull(console, "console");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder ui(UiHost ui) {
|
||||
this.ui = Objects.requireNonNull(ui, "ui");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides dom capability.
|
||||
*
|
||||
* @param dom dom host
|
||||
* @return this
|
||||
*/
|
||||
public Builder dom(DomHost dom) {
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides event subscription capability.
|
||||
*
|
||||
* @param events event host
|
||||
* @return this
|
||||
*/
|
||||
public Builder events(EventHost events) {
|
||||
this.events = Objects.requireNonNull(events, "events");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides resource loading capability.
|
||||
*
|
||||
* @param resources resource host
|
||||
* @return this
|
||||
*/
|
||||
public Builder resources(ResourceHost resources) {
|
||||
this.resources = Objects.requireNonNull(resources, "resources");
|
||||
return this;
|
||||
}
|
||||
|
||||
public HostServices build() {
|
||||
return new HostServices(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
/**
|
||||
* Resource loading abstraction for LuaScript (e.g. script src).
|
||||
*/
|
||||
public interface ResourceHost {
|
||||
|
||||
/**
|
||||
* Reads text from a script source (file/url/virtual path).
|
||||
*
|
||||
* @param src source identifier
|
||||
* @return text content
|
||||
* @throws Exception on load failures
|
||||
*/
|
||||
String readText(String src) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.openautonomousconnection.luascript.hosts;
|
||||
|
||||
/**
|
||||
* Host capability for UI operations.
|
||||
*/
|
||||
public interface UiHost {
|
||||
|
||||
void alert(String message);
|
||||
|
||||
boolean confirm(String message);
|
||||
|
||||
String prompt(String message, String defaultValue);
|
||||
|
||||
void setText(String elementId, String text);
|
||||
|
||||
String getText(String elementId);
|
||||
|
||||
void setHtml(String elementId, String html);
|
||||
|
||||
String getHtml(String elementId);
|
||||
|
||||
void setValue(String elementId, String value);
|
||||
|
||||
String getValue(String elementId);
|
||||
|
||||
void setEnabled(String elementId, boolean enabled);
|
||||
|
||||
void setVisible(String elementId, boolean visible);
|
||||
|
||||
void addClass(String elementId, String className);
|
||||
|
||||
void removeClass(String elementId, String className);
|
||||
|
||||
boolean toggleClass(String elementId, String className);
|
||||
|
||||
boolean hasClass(String elementId, String className);
|
||||
|
||||
void setStyle(String elementId, String property, String value);
|
||||
|
||||
String getStyle(String elementId, String property);
|
||||
|
||||
void setAttribute(String elementId, String name, String value);
|
||||
|
||||
String getAttribute(String elementId, String name);
|
||||
|
||||
void removeAttribute(String elementId, String name);
|
||||
|
||||
void focus(String elementId);
|
||||
|
||||
void blur(String elementId);
|
||||
|
||||
void scrollIntoView(String elementId);
|
||||
|
||||
int viewportWidth();
|
||||
|
||||
int viewportHeight();
|
||||
|
||||
long nowMillis();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.openautonomousconnection.luascript.runtime;
|
||||
|
||||
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
|
||||
import org.openautonomousconnection.luascript.events.UiEventRegistry;
|
||||
import org.openautonomousconnection.luascript.hosts.DomHost;
|
||||
import org.openautonomousconnection.luascript.hosts.EventHost;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Scans the DOM for attributes in variant B form: on:EVENT="path.to.handler"
|
||||
* and binds them automatically via LuaEventDispatcher + EventHost subscriptions.
|
||||
*/
|
||||
public final class LuaDomBinder {
|
||||
|
||||
private static final String ATTR_PREFIX = "on:";
|
||||
|
||||
private final DomHost dom;
|
||||
private final EventHost eventHost;
|
||||
private final LuaEventDispatcher dispatcher;
|
||||
|
||||
public LuaDomBinder(DomHost dom, EventHost eventHost, LuaEventDispatcher dispatcher) {
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
this.eventHost = Objects.requireNonNull(eventHost, "eventHost");
|
||||
this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher");
|
||||
}
|
||||
|
||||
public void bindAll() {
|
||||
for (String elementId : dom.getAllElementIds()) {
|
||||
Map<String, String> attrs = dom.getAttributes(elementId);
|
||||
if (attrs == null || attrs.isEmpty()) continue;
|
||||
|
||||
for (Map.Entry<String, String> e : attrs.entrySet()) {
|
||||
String attr = e.getKey();
|
||||
if (attr == null) continue;
|
||||
|
||||
String a = attr.trim().toLowerCase();
|
||||
if (!a.startsWith(ATTR_PREFIX)) continue;
|
||||
|
||||
String eventName = UiEventRegistry.normalize(a.substring(ATTR_PREFIX.length()));
|
||||
String handlerPath = (e.getValue() == null) ? "" : e.getValue().trim();
|
||||
if (handlerPath.isEmpty()) {
|
||||
throw new IllegalStateException("Empty handler for attribute '" + attr + "' on element '" + elementId + "'");
|
||||
}
|
||||
|
||||
dispatcher.bind(elementId, eventName, handlerPath);
|
||||
eventHost.addListener(elementId, eventName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.openautonomousconnection.luascript.runtime;
|
||||
|
||||
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
|
||||
import org.openautonomousconnection.luascript.events.UiEvent;
|
||||
import org.openautonomousconnection.luascript.events.UiEventRegistry;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Single entry point for the host to forward events into Lua.
|
||||
*/
|
||||
public final class LuaEventRouter {
|
||||
|
||||
private final LuaEventDispatcher dispatcher;
|
||||
|
||||
public LuaEventRouter(LuaEventDispatcher dispatcher) {
|
||||
this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher");
|
||||
}
|
||||
|
||||
public boolean emit(String elementId, String eventName, Map<String, Object> data) {
|
||||
Objects.requireNonNull(elementId, "elementId");
|
||||
Objects.requireNonNull(eventName, "eventName");
|
||||
|
||||
String evt = UiEventRegistry.normalize(eventName);
|
||||
Map<String, Object> payload = (data == null) ? Collections.emptyMap() : data;
|
||||
return dispatcher.dispatch(new UiEvent(elementId, evt, payload));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.openautonomousconnection.luascript.runtime;
|
||||
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
||||
import org.openautonomousconnection.luascript.security.LuaSecurityManager;
|
||||
import org.openautonomousconnection.luascript.tables.console.ConsoleTable;
|
||||
import org.openautonomousconnection.luascript.tables.DomTable;
|
||||
import org.openautonomousconnection.luascript.tables.EventsTable;
|
||||
import org.openautonomousconnection.luascript.tables.UiTable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* High-level entry point for wiring Lua tables + bootstrap + event routing.
|
||||
*/
|
||||
public final class LuaRuntime implements AutoCloseable {
|
||||
|
||||
private final Globals globals;
|
||||
private final HostServices services;
|
||||
|
||||
private final LuaSecurityManager securityManager;
|
||||
private final LuaEventDispatcher dispatcher;
|
||||
private final LuaEventRouter eventRouter;
|
||||
|
||||
public LuaRuntime(Globals globals, HostServices services) {
|
||||
this(globals, services, LuaExecutionPolicy.uiDefault());
|
||||
}
|
||||
|
||||
public LuaRuntime(Globals globals, HostServices services, LuaExecutionPolicy policy) {
|
||||
this.globals = Objects.requireNonNull(globals, "globals");
|
||||
this.services = Objects.requireNonNull(services, "services");
|
||||
Objects.requireNonNull(policy, "policy");
|
||||
|
||||
this.securityManager = new LuaSecurityManager();
|
||||
this.dispatcher = new LuaEventDispatcher(globals, securityManager, policy);
|
||||
this.eventRouter = new LuaEventRouter(dispatcher);
|
||||
}
|
||||
|
||||
public Globals globals() {
|
||||
return globals;
|
||||
}
|
||||
|
||||
public LuaEventRouter eventRouter() {
|
||||
return eventRouter;
|
||||
}
|
||||
|
||||
public void installStdTables(boolean overwrite) {
|
||||
new UiTable().inject(globals, services, overwrite);
|
||||
new ConsoleTable().inject(globals, services, overwrite);
|
||||
new EventsTable(dispatcher).inject(globals, services, overwrite);
|
||||
new DomTable().inject(globals, services, overwrite);
|
||||
}
|
||||
|
||||
public void bootstrapFromDom() {
|
||||
LuaScriptBootstrap.bootstrap(globals, services, dispatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
securityManager.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.openautonomousconnection.luascript.runtime;
|
||||
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
|
||||
import org.openautonomousconnection.luascript.hosts.DomHost;
|
||||
import org.openautonomousconnection.luascript.hosts.EventHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.hosts.ResourceHost;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Bootstrap that:
|
||||
* 1) Executes all Lua scripts found in DOM (<script type="lua"> and <script type="lua" src="...">)
|
||||
* 2) Scans DOM for on:* handlers and binds them automatically.
|
||||
*/
|
||||
public final class LuaScriptBootstrap {
|
||||
|
||||
private LuaScriptBootstrap() { }
|
||||
|
||||
public static void bootstrap(Globals globals, HostServices services, LuaEventDispatcher dispatcher) {
|
||||
Objects.requireNonNull(globals, "globals");
|
||||
Objects.requireNonNull(services, "services");
|
||||
Objects.requireNonNull(dispatcher, "dispatcher");
|
||||
|
||||
DomHost dom = services.dom().orElseThrow(() -> new IllegalStateException("DomHost not provided"));
|
||||
EventHost eventHost = services.events().orElseThrow(() -> new IllegalStateException("EventHost not provided"));
|
||||
ResourceHost resources = services.resources().orElseThrow(() -> new IllegalStateException("ResourceHost not provided"));
|
||||
|
||||
executeAllScripts(globals, dom, resources);
|
||||
new LuaDomBinder(dom, eventHost, dispatcher).bindAll();
|
||||
}
|
||||
|
||||
private static void executeAllScripts(Globals globals, DomHost dom, ResourceHost resources) {
|
||||
for (String elementId : dom.getAllElementIds()) {
|
||||
String tag = safeLower(dom.getTagName(elementId));
|
||||
if (!"script".equals(tag)) continue;
|
||||
|
||||
Map<String, String> attrs = dom.getAttributes(elementId);
|
||||
if (attrs == null) continue;
|
||||
|
||||
String type = safeLower(attrs.getOrDefault("type", ""));
|
||||
String src = attrs.get("src");
|
||||
|
||||
boolean isLuaByType = "lua".equals(type) || "text/lua".equals(type) || "application/lua".equals(type);
|
||||
if (!isLuaByType) continue; // IMPORTANT: only run scripts explicitly marked as Lua
|
||||
|
||||
boolean hasSrc = src != null && !src.trim().isEmpty();
|
||||
if (hasSrc) {
|
||||
String path = src.trim();
|
||||
try {
|
||||
String code = resources.readText(path);
|
||||
LuaScriptExecutor.execute(globals, code, path);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to load lua script src='" + path + "': " + ex.getMessage(), ex);
|
||||
}
|
||||
} else {
|
||||
String inline = dom.getTextContent(elementId);
|
||||
if (inline == null) inline = "";
|
||||
LuaScriptExecutor.execute(globals, inline, "inline:" + elementId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeLower(String s) {
|
||||
return s == null ? "" : s.trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.openautonomousconnection.luascript.runtime;
|
||||
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.luaj.vm2.LuaError;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Loads and executes Lua source code in a given Globals environment.
|
||||
*/
|
||||
public final class LuaScriptExecutor {
|
||||
|
||||
private LuaScriptExecutor() { }
|
||||
|
||||
public static void execute(Globals globals, String source, String chunkName) {
|
||||
Objects.requireNonNull(globals, "globals");
|
||||
Objects.requireNonNull(source, "source");
|
||||
Objects.requireNonNull(chunkName, "chunkName");
|
||||
|
||||
try {
|
||||
LuaValue chunk = globals.load(source, chunkName);
|
||||
chunk.call();
|
||||
} catch (LuaError e) {
|
||||
throw new LuaError("Lua error in '" + chunkName + "': " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.openautonomousconnection.luascript.security;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Execution policy for guarded Lua execution.
|
||||
*
|
||||
* @param timeout max wall-clock time
|
||||
* @param instructionLimit max VM instruction budget (approximation via debug hook)
|
||||
* @param hookStep number of VM instructions between hook ticks
|
||||
*/
|
||||
public record LuaExecutionPolicy(Duration timeout, long instructionLimit, int hookStep) {
|
||||
|
||||
public LuaExecutionPolicy {
|
||||
if (timeout == null) throw new IllegalArgumentException("timeout must not be null");
|
||||
if (timeout.isZero() || timeout.isNegative()) throw new IllegalArgumentException("timeout must be > 0");
|
||||
if (instructionLimit <= 0) throw new IllegalArgumentException("instructionLimit must be > 0");
|
||||
if (hookStep <= 0) throw new IllegalArgumentException("hookStep must be > 0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Default policy for UI event handlers.
|
||||
*
|
||||
* @return policy
|
||||
*/
|
||||
public static LuaExecutionPolicy uiDefault() {
|
||||
return new LuaExecutionPolicy(Duration.ofMillis(50), 200_000, 5_000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package org.openautonomousconnection.luascript.security;
|
||||
|
||||
import org.luaj.vm2.*;
|
||||
import org.luaj.vm2.lib.DebugLib;
|
||||
import org.luaj.vm2.lib.VarArgFunction;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Executes Lua functions on a dedicated thread under a debug hook (LuaJ).
|
||||
*
|
||||
* <p>LuaJ cannot be hard-killed reliably; the debug hook is the enforcement mechanism.</p>
|
||||
*/
|
||||
public final class LuaSecurityManager implements AutoCloseable {
|
||||
|
||||
private final ExecutorService executor;
|
||||
|
||||
/**
|
||||
* Creates a new LuaSecurityManager that runs all Lua code on a single dedicated daemon thread.
|
||||
*/
|
||||
public LuaSecurityManager() {
|
||||
this.executor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "oac-lua-thread");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a Lua function guarded by the provided policy.
|
||||
*
|
||||
* @param globals globals/environment
|
||||
* @param function function to execute
|
||||
* @param args arguments
|
||||
* @param policy execution policy
|
||||
* @return results (Lua varargs)
|
||||
*/
|
||||
public Varargs callGuarded(Globals globals, LuaFunction function, Varargs args, LuaExecutionPolicy policy) {
|
||||
Objects.requireNonNull(globals, "globals");
|
||||
Objects.requireNonNull(function, "function");
|
||||
Objects.requireNonNull(args, "args");
|
||||
Objects.requireNonNull(policy, "policy");
|
||||
|
||||
Future<Varargs> f = executor.submit(() -> callWithHook(globals, function, args, policy));
|
||||
|
||||
try {
|
||||
return f.get(policy.timeout().toMillis(), TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
f.cancel(true);
|
||||
throw new LuaError("Lua execution timed out after " + policy.timeout().toMillis() + "ms");
|
||||
} catch (ExecutionException e) {
|
||||
Throwable c = e.getCause();
|
||||
if (c instanceof LuaError le) throw le;
|
||||
if (c instanceof RuntimeException re) throw re;
|
||||
throw new RuntimeException(c);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Interrupted while waiting for Lua execution", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Varargs callWithHook(Globals globals, LuaFunction function, Varargs args, LuaExecutionPolicy policy) {
|
||||
final LuaValue sethook = resolveAndHideDebugSetHook(globals);
|
||||
|
||||
final long deadlineMillis = System.currentTimeMillis() + policy.timeout().toMillis();
|
||||
final AtomicLong ticks = new AtomicLong(0);
|
||||
|
||||
final LuaValue hookFn = new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs ignored) {
|
||||
long used = ticks.addAndGet(policy.hookStep());
|
||||
if (used > policy.instructionLimit()) {
|
||||
throw new LuaError("Lua instruction limit exceeded: " + policy.instructionLimit());
|
||||
}
|
||||
if (System.currentTimeMillis() > deadlineMillis) {
|
||||
throw new LuaError("Lua execution deadline exceeded");
|
||||
}
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
};
|
||||
|
||||
final LuaThread thread = new LuaThread(globals, function);
|
||||
|
||||
try {
|
||||
// debug.sethook(thread, hookFn, mask, count)
|
||||
sethook.invoke(LuaValue.varargsOf(new LuaValue[]{
|
||||
thread,
|
||||
hookFn,
|
||||
LuaValue.valueOf(""),
|
||||
LuaValue.valueOf(policy.hookStep())
|
||||
}));
|
||||
|
||||
Varargs resumed = thread.resume(args);
|
||||
return unwrapCoroutineResume(resumed);
|
||||
|
||||
} finally {
|
||||
try {
|
||||
sethook.invoke(LuaValue.varargsOf(new LuaValue[]{thread}));
|
||||
} catch (Exception ignored) {
|
||||
// Best-effort cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Varargs unwrapCoroutineResume(Varargs resumed) {
|
||||
if (resumed == null || resumed.narg() == 0) return LuaValue.NIL;
|
||||
|
||||
LuaValue ok = resumed.arg1();
|
||||
if (ok.isboolean() && !ok.toboolean()) {
|
||||
LuaValue err = resumed.narg() >= 2 ? resumed.arg(2) : LuaValue.valueOf("Unknown Lua error");
|
||||
throw new LuaError(err.tojstring());
|
||||
}
|
||||
|
||||
if (ok.isboolean()) return resumed.subargs(2);
|
||||
return resumed;
|
||||
}
|
||||
|
||||
private static LuaValue resolveAndHideDebugSetHook(Globals globals) {
|
||||
// Ensure DebugLib exists for hooks.
|
||||
globals.load(new DebugLib());
|
||||
|
||||
LuaValue debugTable = globals.get("debug");
|
||||
if (debugTable.isnil() || !debugTable.istable()) {
|
||||
throw new IllegalStateException("Debug library not available (debug table missing)");
|
||||
}
|
||||
|
||||
LuaValue sethook = debugTable.get("sethook");
|
||||
if (sethook.isnil() || !sethook.isfunction()) {
|
||||
throw new IllegalStateException("debug.sethook not available");
|
||||
}
|
||||
|
||||
// Hide debug from scripts.
|
||||
globals.set("debug", LuaValue.NIL);
|
||||
return sethook;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package org.openautonomousconnection.luascript.tables;
|
||||
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.*;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
import org.openautonomousconnection.luascript.hosts.DomHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Lua namespace "dom" for DOM querying and mutation.
|
||||
*
|
||||
* <p>All element references are by element id (string).</p>
|
||||
*/
|
||||
public final class DomTable extends ScriptTable {
|
||||
|
||||
public DomTable() {
|
||||
super("dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
DomHost dom = services.dom().orElseThrow(() -> new IllegalStateException("DomHost not provided"));
|
||||
|
||||
table().set("allIds", new ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
return toLuaArray(dom.getAllElementIds());
|
||||
}
|
||||
});
|
||||
|
||||
table().set("exists", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
return LuaValue.valueOf(dom.exists(id.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("tag", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
return LuaValue.valueOf(dom.getTagName(id.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("text", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
return LuaValue.valueOf(dom.getTextContent(id.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setText", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue text) {
|
||||
dom.setTextContent(id.checkjstring(), text.isnil() ? "" : text.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("attrs", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
Map<String, String> attrs = dom.getAttributes(id.checkjstring());
|
||||
LuaTable t = new LuaTable();
|
||||
if (attrs != null) {
|
||||
for (Map.Entry<String, String> e : attrs.entrySet()) {
|
||||
if (e.getKey() == null) continue;
|
||||
t.set(e.getKey(), e.getValue() == null ? LuaValue.NIL : LuaValue.valueOf(e.getValue()));
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("getAttr", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue name) {
|
||||
String v = dom.getAttribute(id.checkjstring(), name.checkjstring());
|
||||
return v == null ? LuaValue.NIL : LuaValue.valueOf(v);
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setAttr", new ThreeArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue name, LuaValue value) {
|
||||
dom.setAttribute(id.checkjstring(), name.checkjstring(), value.isnil() ? "" : value.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("removeAttr", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue name) {
|
||||
dom.removeAttribute(id.checkjstring(), name.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("parent", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
String p = dom.getParentId(id.checkjstring());
|
||||
return p == null ? LuaValue.NIL : LuaValue.valueOf(p);
|
||||
}
|
||||
});
|
||||
|
||||
table().set("children", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
List<String> children = dom.getChildrenIds(id.checkjstring());
|
||||
return toLuaArray(children);
|
||||
}
|
||||
});
|
||||
|
||||
table().set("byTag", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue tag) {
|
||||
return toLuaArray(dom.queryByTag(tag.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("byClass", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue cls) {
|
||||
return toLuaArray(dom.queryByClass(cls.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("create", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue tagName, LuaValue requestedId) {
|
||||
String id = dom.createElement(
|
||||
tagName.checkjstring(),
|
||||
requestedId.isnil() ? null : requestedId.tojstring()
|
||||
);
|
||||
return LuaValue.valueOf(id);
|
||||
}
|
||||
});
|
||||
|
||||
table().set("remove", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
dom.removeElement(id.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("appendChild", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue parentId, LuaValue childId) {
|
||||
dom.appendChild(parentId.checkjstring(), childId.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("insertBefore", new ThreeArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue parentId, LuaValue childId, LuaValue beforeChildId) {
|
||||
dom.insertBefore(parentId.checkjstring(), childId.checkjstring(), beforeChildId.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.openautonomousconnection.luascript.tables;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.luaj.vm2.lib.TwoArgFunction;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
|
||||
import org.openautonomousconnection.luascript.events.UiEventRegistry;
|
||||
import org.openautonomousconnection.luascript.hosts.EventHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
|
||||
/**
|
||||
* events namespace for manual binding/unbinding.
|
||||
*
|
||||
* <p>Auto-binding via on:* is handled by LuaDomBinder. This table is for explicit scripting.</p>
|
||||
*/
|
||||
public final class EventsTable extends ScriptTable {
|
||||
|
||||
private final LuaEventDispatcher dispatcher;
|
||||
|
||||
public EventsTable(LuaEventDispatcher dispatcher) {
|
||||
super("events");
|
||||
this.dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
EventHost host = services.events().orElseThrow(() -> new IllegalStateException("EventHost not provided"));
|
||||
|
||||
table().set("on", new org.luaj.vm2.lib.ThreeArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue elementId, LuaValue eventName, LuaValue handlerPath) {
|
||||
String id = elementId.checkjstring();
|
||||
String evt = UiEventRegistry.normalize(eventName.checkjstring());
|
||||
String hp = handlerPath.checkjstring();
|
||||
|
||||
dispatcher.bind(id, evt, hp);
|
||||
host.addListener(id, evt);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("off", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue elementId, LuaValue eventName) {
|
||||
String id = elementId.checkjstring();
|
||||
String evt = UiEventRegistry.normalize(eventName.checkjstring());
|
||||
|
||||
dispatcher.unbind(id, evt);
|
||||
host.removeListener(id, evt);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("has", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue elementId, LuaValue eventName) {
|
||||
String id = elementId.checkjstring();
|
||||
String evt = UiEventRegistry.normalize(eventName.checkjstring());
|
||||
return LuaValue.valueOf(dispatcher.hasBinding(id, evt));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("onGlobal", new TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue eventName, LuaValue handlerPath) {
|
||||
String evt = UiEventRegistry.normalize(eventName.checkjstring());
|
||||
String hp = handlerPath.checkjstring();
|
||||
|
||||
dispatcher.bind("__global__", evt, hp);
|
||||
host.addGlobalListener(evt);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("offGlobal", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue eventName) {
|
||||
String evt = UiEventRegistry.normalize(eventName.checkjstring());
|
||||
|
||||
dispatcher.unbind("__global__", evt);
|
||||
host.removeGlobalListener(evt);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package org.openautonomousconnection.luascript.tables;
|
||||
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.hosts.UiHost;
|
||||
|
||||
/**
|
||||
* ui namespace for common UI operations.
|
||||
*/
|
||||
public final class UiTable extends ScriptTable {
|
||||
|
||||
public UiTable() {
|
||||
super("ui");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
UiHost ui = services.ui().orElseThrow(() -> new IllegalStateException("UiHost not provided"));
|
||||
|
||||
table().set("alert", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue msg) {
|
||||
ui.alert(msg.isnil() ? "" : msg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("confirm", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue msg) {
|
||||
return LuaValue.valueOf(ui.confirm(msg.isnil() ? "" : msg.tojstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("prompt", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue msg, LuaValue def) {
|
||||
String out = ui.prompt(
|
||||
msg.isnil() ? "" : msg.tojstring(),
|
||||
def.isnil() ? null : def.tojstring()
|
||||
);
|
||||
return out == null ? LuaValue.NIL : LuaValue.valueOf(out);
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setText", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue text) {
|
||||
ui.setText(id.checkjstring(), text.isnil() ? "" : text.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("getText", new OneArgFunction() {
|
||||
@Override public LuaValue call(LuaValue id) {
|
||||
String v = ui.getText(id.checkjstring());
|
||||
return v == null ? LuaValue.NIL : LuaValue.valueOf(v);
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setHtml", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue html) {
|
||||
ui.setHtml(id.checkjstring(), html.isnil() ? "" : html.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("getHtml", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
return LuaValue.valueOf(ui.getHtml(id.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setValue", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue value) {
|
||||
ui.setValue(id.checkjstring(), value.isnil() ? "" : value.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("getValue", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
return LuaValue.valueOf(ui.getValue(id.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setEnabled", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue enabled) {
|
||||
ui.setEnabled(id.checkjstring(), enabled.toboolean());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setVisible", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue visible) {
|
||||
ui.setVisible(id.checkjstring(), visible.toboolean());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("addClass", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue cls) {
|
||||
ui.addClass(id.checkjstring(), cls.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("removeClass", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue cls) {
|
||||
ui.removeClass(id.checkjstring(), cls.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("toggleClass", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue cls) {
|
||||
return LuaValue.valueOf(ui.toggleClass(id.checkjstring(), cls.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("hasClass", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue cls) {
|
||||
return LuaValue.valueOf(ui.hasClass(id.checkjstring(), cls.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setStyle", new org.luaj.vm2.lib.ThreeArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue prop, LuaValue value) {
|
||||
ui.setStyle(id.checkjstring(), prop.checkjstring(), value.isnil() ? "" : value.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("getStyle", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue prop) {
|
||||
return LuaValue.valueOf(ui.getStyle(id.checkjstring(), prop.checkjstring()));
|
||||
}
|
||||
});
|
||||
|
||||
table().set("setAttr", new org.luaj.vm2.lib.ThreeArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue name, LuaValue value) {
|
||||
ui.setAttribute(id.checkjstring(), name.checkjstring(), value.isnil() ? "" : value.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("getAttr", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue name) {
|
||||
String v = ui.getAttribute(id.checkjstring(), name.checkjstring());
|
||||
return v == null ? LuaValue.NIL : LuaValue.valueOf(v);
|
||||
}
|
||||
});
|
||||
|
||||
table().set("removeAttr", new org.luaj.vm2.lib.TwoArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id, LuaValue name) {
|
||||
ui.removeAttribute(id.checkjstring(), name.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("focus", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
ui.focus(id.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("blur", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
ui.blur(id.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("scrollIntoView", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue id) {
|
||||
ui.scrollIntoView(id.checkjstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
LuaTable viewport = new LuaTable();
|
||||
viewport.set("width", new org.luaj.vm2.lib.ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
return LuaValue.valueOf(ui.viewportWidth());
|
||||
}
|
||||
});
|
||||
viewport.set("height", new org.luaj.vm2.lib.ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
return LuaValue.valueOf(ui.viewportHeight());
|
||||
}
|
||||
});
|
||||
table().set("viewport", viewport);
|
||||
|
||||
table().set("now", new org.luaj.vm2.lib.ZeroArgFunction() {
|
||||
@Override
|
||||
public LuaValue call() {
|
||||
return LuaValue.valueOf(ui.nowMillis());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.openautonomousconnection.luascript.tables.console;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
|
||||
public class ConsoleLogTable extends ScriptTable {
|
||||
/**
|
||||
* Creates a new script table with the given global name.
|
||||
*/
|
||||
public ConsoleLogTable() {
|
||||
super("log");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
table().set("info", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().info(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("log", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().log(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("warn", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().warn(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.openautonomousconnection.luascript.tables.console;
|
||||
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.OneArgFunction;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
|
||||
public class ConsoleStacktraceTable extends ScriptTable {
|
||||
/**
|
||||
* Creates a new script table with the given global name.
|
||||
*/
|
||||
public ConsoleStacktraceTable() {
|
||||
super("stacktrace");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
table().set("print", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().error(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
|
||||
table().set("exception", new OneArgFunction() {
|
||||
@Override
|
||||
public LuaValue call(LuaValue arg) {
|
||||
services.console().get().exception(arg.isnil() ? "nil" : arg.tojstring());
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.openautonomousconnection.luascript.tables.console;
|
||||
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.utils.ScriptTable;
|
||||
|
||||
public class ConsoleTable extends ScriptTable {
|
||||
/**
|
||||
* Creates a new script table with the given global name.
|
||||
*/
|
||||
public ConsoleTable() {
|
||||
super("console");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void define(HostServices services) {
|
||||
injectChild(new ConsoleLogTable(), services, true);
|
||||
injectChild(new ConsoleStacktraceTable(), services, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.openautonomousconnection.luascript.utils;
|
||||
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.DebugLib;
|
||||
import org.luaj.vm2.lib.jse.JsePlatform;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Factory for creating configured LuaJ {@link Globals} instances.
|
||||
*
|
||||
* <p>Design goals:</p>
|
||||
* <ul>
|
||||
* <li>No global singletons (safe for multi-view/multi-session usage)</li>
|
||||
* <li>Optional debug library</li>
|
||||
* <li>Optional sandbox hardening</li>
|
||||
* <li>Extension point for host APIs</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class LuaGlobalsFactory {
|
||||
|
||||
private LuaGlobalsFactory() { }
|
||||
|
||||
/**
|
||||
* Configuration options for creating a {@link Globals} instance.
|
||||
*/
|
||||
public static final class Options {
|
||||
private boolean enableDebug;
|
||||
private boolean sandbox;
|
||||
private Consumer<Globals> hostApiConfigurer;
|
||||
|
||||
/**
|
||||
* Enables or disables Lua debug library exposure.
|
||||
*
|
||||
* @param enableDebug true to enable debug lib, false otherwise
|
||||
* @return this options instance
|
||||
*/
|
||||
public Options enableDebug(boolean enableDebug) {
|
||||
this.enableDebug = enableDebug;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables sandbox hardening (disables os/io/debug/luajava/package/require/loadfile/dofile).
|
||||
*
|
||||
* @param sandbox true to harden, false otherwise
|
||||
* @return this options instance
|
||||
*/
|
||||
public Options sandbox(boolean sandbox) {
|
||||
this.sandbox = sandbox;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a hook to register host APIs (e.g. {@code host.ui.alert}).
|
||||
*
|
||||
* @param hostApiConfigurer configurer callback
|
||||
* @return this options instance
|
||||
*/
|
||||
public Options hostApiConfigurer(Consumer<Globals> hostApiConfigurer) {
|
||||
this.hostApiConfigurer = hostApiConfigurer;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Globals} based on {@link JsePlatform#standardGlobals()} and applies options.
|
||||
*
|
||||
* @param options options for creation
|
||||
* @return configured globals instance
|
||||
*/
|
||||
public static Globals create(Options options) {
|
||||
Objects.requireNonNull(options, "options");
|
||||
|
||||
Globals g = JsePlatform.standardGlobals();
|
||||
|
||||
if (options.enableDebug) {
|
||||
// Ensure debug functions are available (depending on defaults).
|
||||
g.load(new DebugLib());
|
||||
} else {
|
||||
// If sandbox is not enabled but you still want no debug, explicitly remove it.
|
||||
g.set("debug", LuaValue.NIL);
|
||||
}
|
||||
|
||||
if (options.sandbox) {
|
||||
hardenSandbox(g);
|
||||
}
|
||||
|
||||
if (options.hostApiConfigurer != null) {
|
||||
options.hostApiConfigurer.accept(g);
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies basic sandbox hardening to the given globals.
|
||||
*
|
||||
* @param g globals
|
||||
*/
|
||||
public static void hardenSandbox(Globals g) {
|
||||
Objects.requireNonNull(g, "g");
|
||||
|
||||
g.set("os", LuaValue.NIL);
|
||||
g.set("io", LuaValue.NIL);
|
||||
g.set("debug", LuaValue.NIL);
|
||||
g.set("luajava", LuaValue.NIL);
|
||||
|
||||
// Prevent module loading / file loading
|
||||
g.set("package", LuaValue.NIL);
|
||||
g.set("require", LuaValue.NIL);
|
||||
g.set("loadfile", LuaValue.NIL);
|
||||
g.set("dofile", LuaValue.NIL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to register a root 'host' table if missing and return it.
|
||||
*
|
||||
* @param g globals
|
||||
* @return host table
|
||||
*/
|
||||
public static LuaTable ensureHostTable(Globals g) {
|
||||
Objects.requireNonNull(g, "g");
|
||||
|
||||
LuaValue existing = g.get("host");
|
||||
if (existing.istable()) {
|
||||
return (LuaTable) existing;
|
||||
}
|
||||
|
||||
LuaTable host = new LuaTable();
|
||||
g.set("host", host);
|
||||
return host;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.openautonomousconnection.luascript.utils;
|
||||
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base class for exposing a Lua table (namespace) into globals.
|
||||
*/
|
||||
public abstract class ScriptTable {
|
||||
|
||||
private final LuaTable table;
|
||||
private final String tableName;
|
||||
|
||||
protected ScriptTable(String tableName) {
|
||||
this.tableName = Objects.requireNonNull(tableName, "tableName");
|
||||
this.table = new LuaTable();
|
||||
}
|
||||
|
||||
public final LuaTable table() {
|
||||
return table;
|
||||
}
|
||||
|
||||
public final String tableName() {
|
||||
return tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines functions/fields for this table. Called exactly once before injection.
|
||||
*
|
||||
* @param services host services
|
||||
*/
|
||||
protected abstract void define(HostServices services);
|
||||
|
||||
/**
|
||||
* Injects this table into globals as a global namespace (e.g. "ui", "console", "events").
|
||||
*
|
||||
* @param globals globals
|
||||
* @param services services
|
||||
* @param overwriteExisting overwrite existing global
|
||||
*/
|
||||
public final void inject(Globals globals, HostServices services, boolean overwriteExisting) {
|
||||
Objects.requireNonNull(globals, "globals");
|
||||
Objects.requireNonNull(services, "services");
|
||||
|
||||
LuaValue existing = globals.get(tableName);
|
||||
if (!overwriteExisting && !existing.isnil()) {
|
||||
throw new IllegalStateException("Lua global already exists: " + tableName);
|
||||
}
|
||||
|
||||
define(services);
|
||||
globals.set(tableName, table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects a child table as a nested namespace (e.g. ui.modal.*).
|
||||
*
|
||||
* @param child child table
|
||||
* @param services host services
|
||||
* @param overwriteExisting overwrite if key exists
|
||||
*/
|
||||
public final void injectChild(ScriptTable child, HostServices services, boolean overwriteExisting) {
|
||||
Objects.requireNonNull(child, "child");
|
||||
Objects.requireNonNull(services, "services");
|
||||
|
||||
LuaValue existing = table.get(child.tableName);
|
||||
if (!overwriteExisting && !existing.isnil()) {
|
||||
throw new IllegalStateException("Lua key already exists: " + tableName + "." + child.tableName);
|
||||
}
|
||||
|
||||
child.define(services);
|
||||
table.set(child.tableName, child.table);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user