Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ add_library(${PACKAGE_NAME} SHARED
../cpp/quickjs/bindings/UrlBindings.cpp
../cpp/quickjs/bindings/AbortBindings.cpp
../cpp/quickjs/bindings/TextEncodingBindings.cpp
../cpp/quickjs/bindings/IntlBindings.cpp
../cpp/quickjs/bindings/FormBindings.cpp
../cpp/quickjs/bindings/BlobBindings.cpp
../cpp/quickjs/bindings/CSSOMBindings.cpp
Expand Down
11 changes: 7 additions & 4 deletions cpp/HybridHtmlSandbox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ void HybridHtmlSandbox::initialize(const std::string& html, bool runScripts, con
_runtime->bindDocument(_document.get());

if (runScripts) {
for (const auto& script : _document->getScriptContents()) {
auto* rctx = _runtime->contextState();
for (const auto& [scriptEl, script] : _document->getScriptContents()) {
rctx->current_script = scriptEl;
try {
_runtime->evaluate(script);
} catch (...) { }
rctx->current_script = nullptr;
}
}

Expand Down Expand Up @@ -111,7 +114,7 @@ void HybridHtmlSandbox::setConsoleCallback(

void HybridHtmlSandbox::setDialogCallbacks(
const std::optional<std::variant<nitro::NullType, std::function<void(const std::string&)>>>& onAlert,
const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<bool>>(const std::string&)>>>& onConfirm,
const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string&)>>>& onConfirm,
const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::variant<nitro::NullType, std::string>>>(const std::string&, const std::optional<std::string>&)>>>& onPrompt) {
if (!_runtime) return;

Expand All @@ -129,10 +132,10 @@ void HybridHtmlSandbox::setDialogCallbacks(
if (!onConfirm.has_value() || std::holds_alternative<nitro::NullType>(onConfirm.value())) {
_runtime->setConfirmCallback(nullptr);
} else {
auto fn = std::get<std::function<std::shared_ptr<Promise<bool>>(const std::string&)>>(onConfirm.value());
auto fn = std::get<std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string&)>>(onConfirm.value());
_runtime->setConfirmCallback([fn](const std::string& message) -> bool {
try {
return fn(message)->await().get();
return fn(message)->await().get()->await().get();
} catch (...) {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion cpp/HybridHtmlSandbox.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class HybridHtmlSandbox : public HybridHtmlSandboxSpec {

void setDialogCallbacks(
const std::optional<std::variant<nitro::NullType, std::function<void(const std::string& /* message */)>>>& onAlert,
const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<bool>>(const std::string& /* message */)>>>& onConfirm,
const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string& /* message */)>>>& onConfirm,
const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::variant<nitro::NullType, std::string>>>(const std::string& /* message */, const std::optional<std::string>& /* defaultValue */)>>>& onPrompt
) override;

Expand Down
6 changes: 3 additions & 3 deletions cpp/lexbor/LexborDocument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ void* LexborDocument::documentElement() const {

// ── Script extraction ────────────────────────────────────────────────────────

std::vector<std::string> LexborDocument::getScriptContents() const {
std::vector<std::pair<void*, std::string>> LexborDocument::getScriptContents() const {
auto scriptEls = querySelectorAll_el("script");
std::vector<std::string> contents;
std::vector<std::pair<void*, std::string>> contents;
contents.reserve(scriptEls.size());

for (void* el : scriptEls) {
Expand All @@ -217,7 +217,7 @@ std::vector<std::string> LexborDocument::getScriptContents() const {
size_t len = 0;
lxb_char_t* text = lxb_dom_node_text_content(node, &len);
if (text && len > 0) {
contents.emplace_back(reinterpret_cast<char*>(text), len);
contents.emplace_back(el, std::string(reinterpret_cast<char*>(text), len));
lxb_dom_document_destroy_text(node->owner_document, text);
}
}
Expand Down
5 changes: 3 additions & 2 deletions cpp/lexbor/LexborDocument.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <string>
#include <utility>
#include <vector>

namespace margelo::nitro::nitrojsdom {
Expand Down Expand Up @@ -29,8 +30,8 @@ class LexborDocument {
void* head() const;
void* documentElement() const;

// ── Script extraction ────────────────────────────────────────────────────
std::vector<std::string> getScriptContents() const;
// ── Script extraction — (element, text content) pairs; void* = lxb_dom_element_t* ──
std::vector<std::pair<void*, std::string>> getScriptContents() const;

// ── Node creation ─────────────────────────────────────────────────────────
void* createElement(const std::string& tag);
Expand Down
2 changes: 2 additions & 0 deletions cpp/quickjs/DOMBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "bindings/UrlBindings.hpp"
#include "bindings/AbortBindings.hpp"
#include "bindings/TextEncodingBindings.hpp"
#include "bindings/IntlBindings.hpp"
#include "bindings/FormBindings.hpp"
#include "bindings/BlobBindings.hpp"
#include "bindings/CSSOMBindings.hpp"
Expand Down Expand Up @@ -58,6 +59,7 @@ void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
UrlBindings::install(ctx);
AbortBindings::install(ctx);
TextEncodingBindings::install(ctx);
IntlBindings::install(ctx);
BlobBindings::install(ctx); // uses TextEncoder/TextDecoder + btoa, so must run after both
FetchBindings::install(ctx); // XHR bootstrap uses `new Event(...)`, so must run after EventBindings
FormBindings::install(ctx); // uses globalThis.Element + globalThis.Event, so must run after both
Expand Down
1 change: 1 addition & 0 deletions cpp/quickjs/QuickJSRuntime.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ struct RuntimeContext {
double time_origin_ms { 0 };

void* active_element { nullptr };
void* current_script { nullptr };
std::string ready_state { "loading" };

// node pointer → heap-allocated JSValue* (DupValue'd strong ref)
Expand Down
50 changes: 50 additions & 0 deletions cpp/quickjs/bindings/CSSOMBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,56 @@ const char* kCSSOMBootstrapScript = R"JS(
globalThis.CSSStyleRule = CSSStyleRule;
globalThis.CSSStyleSheet = CSSStyleSheet;

function cssEscape(value) {
var string = String(value);
var length = string.length;
var index = -1;
var result = '';
var firstCodeUnit = string.charCodeAt(0);

if (length === 1 && firstCodeUnit === 0x002D) {
return '\\' + string;
}

while (++index < length) {
var codeUnit = string.charCodeAt(index);
if (codeUnit === 0x0000) {
result += '\uFFFD';
continue;
}
if (
(codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit === 0x007F ||
(index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
(index === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && firstCodeUnit === 0x002D)
) {
result += '\\' + codeUnit.toString(16) + ' ';
continue;
}
if (
codeUnit >= 0x0080 || codeUnit === 0x002D || codeUnit === 0x005F ||
(codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
(codeUnit >= 0x0041 && codeUnit <= 0x005A) ||
(codeUnit >= 0x0061 && codeUnit <= 0x007A)
) {
result += string.charAt(index);
continue;
}
result += '\\' + string.charAt(index);
}
return result;
}

globalThis.CSS = {
escape: cssEscape,
supports: function(propertyOrCondition, value) {
if (arguments.length >= 2) {
return typeof propertyOrCondition === 'string' && propertyOrCondition.length > 0 &&
value !== undefined && String(value).length > 0;
}
return typeof propertyOrCondition === 'string' && propertyOrCondition.trim().length > 0;
},
};

Object.defineProperty(Element.prototype, 'sheet', {
get: function() {
if (this.tagName !== 'STYLE') return null;
Expand Down
2 changes: 1 addition & 1 deletion cpp/quickjs/bindings/CustomElementsBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const char* kCustomElementsBootstrapScript = R"JS(
var n = node;
while (n) {
if (n === docEl) return true;
n = ('host' in n) ? n.host : n.parentNode;
n = (n instanceof ShadowRoot) ? n.host : n.parentNode;
}
return false;
}
Expand Down
8 changes: 8 additions & 0 deletions cpp/quickjs/bindings/DocumentBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ JSValue js_doc_get_activeElement(JSContext* ctx, JSValue) {
return make_element(ctx, get_doc(ctx)->body());
}

JSValue js_doc_get_currentScript(JSContext* ctx, JSValue) {
auto* rctx = get_ctx(ctx);
if (rctx && rctx->current_script) return make_element(ctx, rctx->current_script);
return JS_NULL;
}

JSValue js_doc_get_readyState(JSContext* ctx, JSValue) {
auto* rctx = get_ctx(ctx);
const std::string& state = rctx ? rctx->ready_state : "loading";
Expand Down Expand Up @@ -350,6 +356,7 @@ void DocumentBindings::install(JSContext* ctx) {
define_prop(ctx, doc, "scripts", js_doc_get_scripts, nullptr);
define_prop(ctx, doc, "links", js_doc_get_links, nullptr);
define_prop(ctx, doc, "activeElement", js_doc_get_activeElement, nullptr);
define_prop(ctx, doc, "currentScript", js_doc_get_currentScript, nullptr);
define_prop(ctx, doc, "readyState", js_doc_get_readyState, nullptr);
define_prop(ctx, doc, "compatMode", js_doc_get_compatMode, nullptr);
define_prop(ctx, doc, "baseURI", js_doc_get_baseURI, nullptr);
Expand All @@ -360,6 +367,7 @@ void DocumentBindings::install(JSContext* ctx) {
RuntimeContext* rctx = get_ctx(ctx);
bool hidden = !(rctx && rctx->pretend_to_be_visual);
JS_SetPropertyStr(ctx, doc, "hidden", JS_NewBool(ctx, hidden));
JS_SetPropertyStr(ctx, doc, "visibilityState", JS_NewString(ctx, hidden ? "hidden" : "visible"));
JS_SetPropertyStr(ctx, doc, "nodeType", JS_NewInt32(ctx, 9 /* DOCUMENT_NODE */));
JS_SetPropertyStr(ctx, doc, "nodeName", JS_NewString(ctx, "#document"));
JS_SetPropertyStr(ctx, doc, "ownerDocument", JS_NULL);
Expand Down
113 changes: 112 additions & 1 deletion cpp/quickjs/bindings/ElementBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,107 @@ JSValue js_el_set_checked(JSContext* ctx, JSValue this_val, JSValue val) {
return JS_UNDEFINED;
}

JSValue bool_attr_get(JSContext* ctx, JSValue this_val, const char* attr, size_t attr_len) {
auto* el = unwrap_element(ctx, this_val);
if (!el) return JS_FALSE;
bool has = lxb_dom_element_has_attribute(el, reinterpret_cast<const lxb_char_t*>(attr), attr_len);
return JS_NewBool(ctx, has);
}

JSValue bool_attr_set(JSContext* ctx, JSValue this_val, JSValue val, const char* attr, size_t attr_len) {
auto* el = unwrap_element(ctx, this_val);
if (!el) return JS_UNDEFINED;
bool on = JS_ToBool(ctx, val) > 0;
auto* attr_name = reinterpret_cast<const lxb_char_t*>(attr);
bool has = lxb_dom_element_has_attribute(el, attr_name, attr_len);

auto* rctx = get_ctx(ctx);
bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
std::optional<std::string> old_val;
if (has_obs && has && rctx->mutation_observers->hasAttributeOldValueObserver()) {
size_t len = 0;
const lxb_char_t* v = lxb_dom_element_get_attribute(el, attr_name, attr_len, &len);
if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
}

if (on && !has) {
lxb_dom_element_set_attribute(el, attr_name, attr_len, reinterpret_cast<const lxb_char_t*>(""), 0);
if (has_obs) {
rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), attr, std::nullopt);
}
} else if (!on && has) {
lxb_dom_element_remove_attribute(el, attr_name, attr_len);
if (has_obs) {
rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), attr, old_val);
}
}

return JS_UNDEFINED;
}

JSValue js_el_get_disabled(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "disabled", 8); }
JSValue js_el_set_disabled(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "disabled", 8); }

JSValue js_el_get_required(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "required", 8); }
JSValue js_el_set_required(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "required", 8); }

JSValue js_el_get_readOnly(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "readonly", 8); }
JSValue js_el_set_readOnly(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "readonly", 8); }

JSValue js_el_get_multiple(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "multiple", 8); }
JSValue js_el_set_multiple(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "multiple", 8); }

JSValue js_el_get_autofocus(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "autofocus", 9); }
JSValue js_el_set_autofocus(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "autofocus", 9); }

JSValue js_el_get_selected(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "selected", 8); }
JSValue js_el_set_selected(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "selected", 8); }

JSValue js_el_get_hidden(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "hidden", 6); }
JSValue js_el_set_hidden(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "hidden", 6); }

JSValue string_attr_get(JSContext* ctx, JSValue this_val, const char* attr, size_t attr_len) {
auto* el = unwrap_element(ctx, this_val);
if (!el) return JS_NewString(ctx, "");
size_t len = 0;
const lxb_char_t* val = lxb_dom_element_get_attribute(el, reinterpret_cast<const lxb_char_t*>(attr), attr_len, &len);
return val ? JS_NewStringLen(ctx, reinterpret_cast<const char*>(val), len) : JS_NewString(ctx, "");
}

JSValue string_attr_set(JSContext* ctx, JSValue this_val, JSValue val, const char* attr, size_t attr_len) {
auto* el = unwrap_element(ctx, this_val);
if (!el) return JS_UNDEFINED;
const char* str = JS_ToCString(ctx, val);
if (!str) return JS_UNDEFINED;
auto* attr_name = reinterpret_cast<const lxb_char_t*>(attr);

auto* rctx = get_ctx(ctx);
bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
std::optional<std::string> old_val;
if (has_obs && rctx->mutation_observers->hasAttributeOldValueObserver()) {
size_t len = 0;
const lxb_char_t* v = lxb_dom_element_get_attribute(el, attr_name, attr_len, &len);
if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
}

lxb_dom_element_set_attribute(el, attr_name, attr_len, reinterpret_cast<const lxb_char_t*>(str), strlen(str));
JS_FreeCString(ctx, str);

if (has_obs) {
rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), attr, old_val);
}
return JS_UNDEFINED;
}

JSValue js_el_get_title(JSContext* ctx, JSValue this_val) { return string_attr_get(ctx, this_val, "title", 5); }
JSValue js_el_set_title(JSContext* ctx, JSValue this_val, JSValue val) { return string_attr_set(ctx, this_val, val, "title", 5); }

JSValue js_el_get_lang(JSContext* ctx, JSValue this_val) { return string_attr_get(ctx, this_val, "lang", 4); }
JSValue js_el_set_lang(JSContext* ctx, JSValue this_val, JSValue val) { return string_attr_set(ctx, this_val, val, "lang", 4); }

JSValue js_el_get_dir(JSContext* ctx, JSValue this_val) { return string_attr_get(ctx, this_val, "dir", 3); }
JSValue js_el_set_dir(JSContext* ctx, JSValue this_val, JSValue val) { return string_attr_set(ctx, this_val, val, "dir", 3); }

JSValue js_el_get_textContent(JSContext* ctx, JSValue this_val) {
lxb_dom_node_t* node = unwrap_node(ctx, this_val);
if (!node) return JS_NewString(ctx, "");
Expand Down Expand Up @@ -1513,6 +1614,16 @@ void ElementBindings::install(JSContext* ctx) {
define_prop(ctx, proto, "className", js_el_get_className, js_el_set_className);
define_prop(ctx, proto, "value", js_el_get_value, js_el_set_value);
define_prop(ctx, proto, "checked", js_el_get_checked, js_el_set_checked);
define_prop(ctx, proto, "disabled", js_el_get_disabled, js_el_set_disabled);
define_prop(ctx, proto, "required", js_el_get_required, js_el_set_required);
define_prop(ctx, proto, "readOnly", js_el_get_readOnly, js_el_set_readOnly);
define_prop(ctx, proto, "multiple", js_el_get_multiple, js_el_set_multiple);
define_prop(ctx, proto, "autofocus", js_el_get_autofocus, js_el_set_autofocus);
define_prop(ctx, proto, "selected", js_el_get_selected, js_el_set_selected);
define_prop(ctx, proto, "hidden", js_el_get_hidden, js_el_set_hidden);
define_prop(ctx, proto, "title", js_el_get_title, js_el_set_title);
define_prop(ctx, proto, "lang", js_el_get_lang, js_el_set_lang);
define_prop(ctx, proto, "dir", js_el_get_dir, js_el_set_dir);
define_prop(ctx, proto, "innerHTML", js_el_get_innerHTML, js_el_set_innerHTML);
define_prop(ctx, proto, "outerHTML", js_el_get_outerHTML, nullptr);
define_prop(ctx, proto, "innerText", js_el_get_textContent, js_el_set_textContent);
Expand Down Expand Up @@ -1553,7 +1664,7 @@ void ElementBindings::install(JSContext* ctx) {
while (true) {
var parent = node.parentNode;
if (parent) { node = parent; continue; }
if (composed && node.host) { node = node.host; continue; }
if (composed && node instanceof ShadowRoot) { node = node.host; continue; }
return __nativeCanonicalizeRootNode(node);
}
};
Expand Down
Loading
Loading