From d7f4f94a62297ba059627601ecef5427616e5ebf Mon Sep 17 00:00:00 2001 From: Witold Filipczyk Date: Mon, 7 Nov 2022 20:59:19 +0100 Subject: [PATCH] [click] Added eventListener. It works for these two test cases. --- src/document/document.cpp | 13 ++ src/document/document.h | 7 + src/document/html/parser.h | 2 + src/document/html/parser/forms.c | 8 +- src/document/html/parser/general.c | 1 + src/document/html/parser/link.c | 11 +- src/document/html/renderer.c | 4 + src/document/renderer.cpp | 4 +- src/document/xml/renderer2.cpp | 76 ++++++++- src/ecmascript/spidermonkey/element.cpp | 203 +++++++++++++++++++++++- src/ecmascript/spidermonkey/element.h | 3 +- src/ecmascript/spidermonkey/xhr.cpp | 1 - src/viewer/text/link.cpp | 22 ++- test/ecmascript/clickListener.html | 20 +++ test/ecmascript/clickListener2.html | 21 +++ 15 files changed, 383 insertions(+), 13 deletions(-) create mode 100644 test/ecmascript/clickListener.html create mode 100644 test/ecmascript/clickListener2.html diff --git a/src/document/document.cpp b/src/document/document.cpp index 9ff29d31..aa86b449 100644 --- a/src/document/document.cpp +++ b/src/document/document.cpp @@ -82,6 +82,19 @@ static INIT_LIST_OF(struct document, format_cache); +const char *script_event_hook_name[] = { + "click", + "dblclick", + "mouseover", + "hover", + "focus", + "mouseout", + "blur", + "keydown", + "keyup", + NULL +}; + #ifdef HAVE_INET_NTOP /* DNS callback. */ static void diff --git a/src/document/document.h b/src/document/document.h index 92443c56..c8e72d53 100644 --- a/src/document/document.h +++ b/src/document/document.h @@ -90,6 +90,9 @@ enum script_event_hook_type { SEVHOOK_ONKEYUP }; +/* keep in sync with above */ +extern const char *script_event_hook_name[]; + struct script_event_hook { LIST_HEAD(struct script_event_hook); @@ -102,6 +105,9 @@ struct link { enum link_type type; +#ifdef CONFIG_ECMASCRIPT + int element_offset; +#endif char *where; char *target; char *where_img; @@ -214,6 +220,7 @@ struct document { LIST_OF(struct ecmascript_timeout) timeouts; int ecmascript_counter; void *dom; + void *element_map; char *text; void *forms_nodeset; #endif diff --git a/src/document/html/parser.h b/src/document/html/parser.h index 1f498409..8b76d086 100644 --- a/src/document/html/parser.h +++ b/src/document/html/parser.h @@ -76,6 +76,8 @@ struct text_attrib { char *onblur; char *onkeydown; char *onkeyup; + + char *top_name; }; /* This enum is pretty ugly, yes ;). */ diff --git a/src/document/html/parser/forms.c b/src/document/html/parser/forms.c index 5dfcc6a3..4e258f74 100644 --- a/src/document/html/parser/forms.c +++ b/src/document/html/parser/forms.c @@ -141,13 +141,14 @@ init_form_control(enum form_type type, char *attr, void html_button(struct html_context *html_context, char *a, - char *xxx3, char *xxx4, char **xxx5) + char *html, char *xxx4, char **xxx5) { char *al; struct el_form_control *fc; enum form_type type = FC_SUBMIT; int cp = html_context->doc_cp; + elformat.top_name = html_top->name; html_focusable(html_context, a); al = get_attr_val(a, "type", cp); @@ -192,9 +193,11 @@ html_input_format(struct html_context *html_context, char *a, struct el_form_control *fc) { put_chrs(html_context, " ", 1); + char *top_name = html_top->name; html_stack_dup(html_context, ELEMENT_KILLABLE); html_focusable(html_context, a); elformat.form = fc; + elformat.top_name = top_name; mem_free_if(elformat.title); elformat.title = get_attr_val(a, "title", html_context->doc_cp); switch (fc->type) { @@ -354,6 +357,7 @@ do_html_select(char *attr, char *html, int i, max_width; int closing_tag; + elformat.top_name = html_top->name; html_focusable(html_context, attr); init_menu(&lnk_menu); @@ -529,6 +533,7 @@ do_html_select_multiple(struct html_context *html_context, char *a, char *al = get_attr_val(a, "name", html_context->doc_cp); if (!al) return; + elformat.top_name = html_top->name; html_focusable(html_context, a); html_top->type = ELEMENT_DONT_KILL; mem_free_set(&elformat.select, al); @@ -636,6 +641,7 @@ html_textarea(struct html_context *html_context, char *attr, int cols, rows; int i; + elformat.top_name = html_top->name; html_focusable(html_context, attr); while (html < eof && (*html == '\n' || *html == '\r')) html++; p = html; diff --git a/src/document/html/parser/general.c b/src/document/html/parser/general.c index eb95f68e..b2aaa954 100644 --- a/src/document/html/parser/general.c +++ b/src/document/html/parser/general.c @@ -1047,6 +1047,7 @@ html_frame(struct html_context *html_context, char *a, if (!name) return; if (!html_context->options->frames || !html_top->frameset) { + elformat.top_name = html_top->name; html_focusable(html_context, a); put_link_line("Frame: ", name, url, "", html_context); diff --git a/src/document/html/parser/link.c b/src/document/html/parser/link.c index 5e82043f..d4390cc9 100644 --- a/src/document/html/parser/link.c +++ b/src/document/html/parser/link.c @@ -94,6 +94,7 @@ html_a(struct html_context *html_context, char *a, mem_free_set(&elformat.title, get_attr_val(a, "title", html_context->doc_cp)); + elformat.top_name = html_top->name; html_focusable(html_context, a); } else { @@ -205,6 +206,7 @@ put_image_label(char *a, char *label, /* This is not 100% appropriate for , but well, accepting * accesskey and tabindex near is just our little * extension to the standard. After all, it makes sense. */ + elformat.top_name = html_top->name; html_focusable(html_context, a); saved_foreground = elformat.style.color.foreground; @@ -381,7 +383,7 @@ html_source(struct html_context *html_context, char *a, mem_free_set(&title, get_image_filename_from_src(options->image_link.filename_maxlen, src)); } } - + elformat.top_name = html_top->name; html_focusable(html_context, a); if (title && *title) { @@ -433,7 +435,7 @@ html_applet(struct html_context *html_context, char *a, if (!code) return; alt = get_attr_val(a, "alt", html_context->doc_cp); - + elformat.top_name = html_top->name; html_focusable(html_context, a); if (alt && *alt) { @@ -459,6 +461,7 @@ html_audio(struct html_context *html_context, char *a, url = get_url_val(a, "src", html_context->doc_cp); if (!url) return; + elformat.top_name = html_top->name; html_focusable(html_context, a); put_link_line("Audio: ", basename(url), url, @@ -489,6 +492,7 @@ html_iframe_do(char *a, char *object_src, return; } + elformat.top_name = html_top->name; html_focusable(html_context, a); if (html_context->options->iframes) { @@ -574,6 +578,7 @@ html_object(struct html_context *html_context, char *a, name = get_attr_val(a, "standby", html_context->doc_cp); + elformat.top_name = html_top->name; html_focusable(html_context, a); if (name && *name) { @@ -638,6 +643,7 @@ html_video(struct html_context *html_context, char *a, url = get_url_val(a, "src", html_context->doc_cp); if (!url) return; + elformat.top_name = html_top->name; html_focusable(html_context, a); put_link_line("Video: ", basename(url), url, @@ -944,6 +950,7 @@ html_link(struct html_context *html_context, char *a, if (!name) goto free_and_return; if (!init_string(&text)) goto free_and_return; + elformat.top_name = html_top->name; html_focusable(html_context, a); if (link.title) { diff --git a/src/document/html/renderer.c b/src/document/html/renderer.c index 60e295b8..bbe64c12 100644 --- a/src/document/html/renderer.c +++ b/src/document/html/renderer.c @@ -1551,6 +1551,10 @@ new_link(struct html_context *html_context, const char *name, int namelen) ? elformat.style.color.foreground : elformat.color.clink; +#ifdef CONFIG_ECMASCRIPT + link->element_offset = elformat.top_name ? elformat.top_name - document->text : 0; +#endif + init_link_event_hooks(html_context, link); document->links_sorted = 0; diff --git a/src/document/renderer.cpp b/src/document/renderer.cpp index a4dac320..37ef3726 100644 --- a/src/document/renderer.cpp +++ b/src/document/renderer.cpp @@ -293,8 +293,8 @@ render_encoded_document(struct cache_entry *cached, struct document *document) && (!c_strlcasecmp("text/gemini", 11, cached->content_type, -1))) render_gemini_document(cached, document, &buffer); else -#ifdef CONFIG_XML - if (false) render_xhtml_document(cached, document, &buffer); +#if defined(CONFIG_XML) && defined(CONFIG_ECMASCRIPT) + if (true) render_xhtml_document(cached, document, NULL); else #endif render_html_document(cached, document, &buffer); diff --git a/src/document/xml/renderer2.cpp b/src/document/xml/renderer2.cpp index 82450d53..fa86edb2 100644 --- a/src/document/xml/renderer2.cpp +++ b/src/document/xml/renderer2.cpp @@ -38,6 +38,8 @@ #include "util/string.h" #include +#include + #if 0 @@ -267,6 +269,65 @@ dump_dom_structure(struct source_renderer *renderer, void *nod, int depth) } #endif +static void +dump_element(std::map *mapa, struct string *buf, xmlpp::Element *element) +{ + add_char_to_string(buf, '<'); + (*mapa)[buf->length] = element; + + add_to_string(buf, element->get_name().c_str()); + auto attrs = element->get_attributes(); + auto it = attrs.begin(); + auto end = attrs.end(); + + for (;it != end; ++it) { + add_char_to_string(buf, ' '); + add_to_string(buf, (*it)->get_name().c_str()); + add_char_to_string(buf, '='); + add_char_to_string(buf, '"'); + add_to_string(buf, (*it)->get_value().c_str()); + add_char_to_string(buf, '"'); + } + add_char_to_string(buf, '>'); +} + +static void +walk_tree(std::map *mapa, struct string *buf, void *nod, bool start) +{ + xmlpp::Node *node = static_cast(nod); + + if (!start) { + const auto textNode = dynamic_cast(node); + + if (textNode) { + add_bytes_to_string(buf, textNode->get_content().c_str(), textNode->get_content().length()); + } else { + auto element = dynamic_cast(node); + + if (element) { + dump_element(mapa, buf, element); + } + } + } + + auto childs = node->get_children(); + auto it = childs.begin(); + auto end = childs.end(); + + for (; it != end; ++it) { + walk_tree(mapa, buf, *it, false); + } + + if (!start) { + const auto element = dynamic_cast(node); + if (element) { + add_to_string(buf, "get_name().c_str()); + add_char_to_string(buf, '>'); + } + } +} + void render_xhtml_document(struct cache_entry *cached, struct document *document, struct string *buffer) { @@ -278,7 +339,6 @@ render_xhtml_document(struct cache_entry *cached, struct document *document, str render_html_document(cached, document, buffer); return; } - struct string head; assert(cached && document); @@ -300,19 +360,29 @@ render_xhtml_document(struct cache_entry *cached, struct document *document, str } xmlpp::Document *doc = (xmlpp::Document *)document->dom; + xmlpp::Element* root = (xmlpp::Element *)doc->get_root_node(); if (!buffer) { - xmlpp::ustring text = doc->write_to_string_formatted(); struct string tt; if (!init_string(&tt)) { done_string(&head); return; } - add_bytes_to_string(&tt, text.c_str(), text.size()); + std::map *mapa = (std::map *)document->element_map; + + if (!mapa) { + mapa = new std::map; + document->element_map = (void *)mapa; + } else { + mapa->clear(); + } + + walk_tree(mapa, &tt, root, true); buffer = &tt; document->text = tt.source; } + if (add_to_head) { mem_free_set(&cached->head, head.source); } diff --git a/src/ecmascript/spidermonkey/element.cpp b/src/ecmascript/spidermonkey/element.cpp index f00aa49c..c8492f36 100644 --- a/src/ecmascript/spidermonkey/element.cpp +++ b/src/ecmascript/spidermonkey/element.cpp @@ -28,6 +28,7 @@ #include "ecmascript/spidermonkey/attributes.h" #include "ecmascript/spidermonkey/collection.h" #include "ecmascript/spidermonkey/element.h" +#include "ecmascript/spidermonkey/heartbeat.h" #include "ecmascript/spidermonkey/nodelist.h" #include "ecmascript/spidermonkey/window.h" #include "intl/libintl.h" @@ -97,6 +98,18 @@ static bool element_set_property_textContent(JSContext *ctx, unsigned int argc, static bool element_get_property_title(JSContext *ctx, unsigned int argc, JS::Value *vp); static bool element_set_property_title(JSContext *ctx, unsigned int argc, JS::Value *vp); +struct listener { + LIST_HEAD(struct listener); + char *typ; + JS::RootedValue fun; +}; + +struct element_private { + LIST_OF(struct listener) listeners; + struct ecmascript_interpreter *interpreter; + JS::RootedObject thisval; +}; + static void element_finalize(JS::GCContext *op, JSObject *obj); JSClassOps element_ops = { @@ -114,7 +127,7 @@ JSClassOps element_ops = { JSClass element_class = { "element", - JSCLASS_HAS_RESERVED_SLOTS(1), + JSCLASS_HAS_RESERVED_SLOTS(2), &element_ops }; @@ -2310,6 +2323,7 @@ element_set_property_title(JSContext *ctx, unsigned int argc, JS::Value *vp) return true; } +static bool element_addEventListener(JSContext *ctx, unsigned int argc, JS::Value *rval); static bool element_appendChild(JSContext *ctx, unsigned int argc, JS::Value *rval); static bool element_cloneNode(JSContext *ctx, unsigned int argc, JS::Value *rval); static bool element_closest(JSContext *ctx, unsigned int argc, JS::Value *rval); @@ -2327,9 +2341,11 @@ static bool element_querySelector(JSContext *ctx, unsigned int argc, JS::Value * static bool element_querySelectorAll(JSContext *ctx, unsigned int argc, JS::Value *rval); static bool element_remove(JSContext *ctx, unsigned int argc, JS::Value *rval); static bool element_removeChild(JSContext *ctx, unsigned int argc, JS::Value *rval); +static bool element_removeEventListener(JSContext *ctx, unsigned int argc, JS::Value *rval); static bool element_setAttribute(JSContext *ctx, unsigned int argc, JS::Value *rval); const spidermonkeyFunctionSpec element_funcs[] = { + { "addEventListener", element_addEventListener, 3 }, { "appendChild", element_appendChild, 1 }, { "cloneNode", element_cloneNode, 1 }, { "closest", element_closest, 1 }, @@ -2347,6 +2363,7 @@ const spidermonkeyFunctionSpec element_funcs[] = { { "querySelectorAll", element_querySelectorAll, 1 }, { "remove", element_remove, 0 }, { "removeChild", element_removeChild, 1 }, + { "removeEventListener", element_removeEventListener, 3 }, { "setAttribute", element_setAttribute, 2 }, { NULL } }; @@ -2385,6 +2402,137 @@ check_contains(xmlpp::Node *node, xmlpp::Node *searched, bool *result_set, bool } } +static bool +element_addEventListener(JSContext *ctx, unsigned int argc, JS::Value *rval) +{ +#ifdef ECMASCRIPT_DEBUG + fprintf(stderr, "%s:%s\n", __FILE__, __FUNCTION__); +#endif + JS::Realm *comp = js::GetContextRealm(ctx); + + if (!comp) { +#ifdef ECMASCRIPT_DEBUG + fprintf(stderr, "%s:%s %d\n", __FILE__, __FUNCTION__, __LINE__); +#endif + return false; + } + + JS::CallArgs args = CallArgsFromVp(argc, rval); + JS::RootedObject hobj(ctx, &args.thisv().toObject()); + + struct ecmascript_interpreter *interpreter = (struct ecmascript_interpreter *)JS::GetRealmPrivate(comp); + + if (!JS_InstanceOf(ctx, hobj, &element_class, NULL)) { +#ifdef ECMASCRIPT_DEBUG + fprintf(stderr, "%s:%s %d\n", __FILE__, __FUNCTION__, __LINE__); +#endif + return false; + } + + xmlpp::Element *el = JS::GetMaybePtrFromReservedSlot(hobj, 0); + struct element_private *el_private = JS::GetMaybePtrFromReservedSlot(hobj, 1); + + if (!el || !el_private) { + args.rval().setNull(); + return true; + } + + if (argc < 2) { + args.rval().setUndefined(); + return true; + } + char *method = jsval_to_string(ctx, args[0]); + JS::RootedValue fun(ctx, args[1]); + + struct listener *l; + + foreach(l, el_private->listeners) { + if (strcmp(l->typ, method)) { + continue; + } + if (l->fun == fun) { + args.rval().setUndefined(); + mem_free(method); + return true; + } + } + struct listener *n = (struct listener *)mem_calloc(1, sizeof(*n)); + + if (n) { + n->typ = method; + n->fun = fun; + add_to_list_end(el_private->listeners, n); + } + args.rval().setUndefined(); + return true; +} + +static bool +element_removeEventListener(JSContext *ctx, unsigned int argc, JS::Value *rval) +{ +#ifdef ECMASCRIPT_DEBUG + fprintf(stderr, "%s:%s\n", __FILE__, __FUNCTION__); +#endif + JS::Realm *comp = js::GetContextRealm(ctx); + + if (!comp) { +#ifdef ECMASCRIPT_DEBUG + fprintf(stderr, "%s:%s %d\n", __FILE__, __FUNCTION__, __LINE__); +#endif + return false; + } + + JS::CallArgs args = CallArgsFromVp(argc, rval); + JS::RootedObject hobj(ctx, &args.thisv().toObject()); + + struct ecmascript_interpreter *interpreter = (struct ecmascript_interpreter *)JS::GetRealmPrivate(comp); + + if (!JS_InstanceOf(ctx, hobj, &element_class, NULL)) { +#ifdef ECMASCRIPT_DEBUG + fprintf(stderr, "%s:%s %d\n", __FILE__, __FUNCTION__, __LINE__); +#endif + return false; + } + + xmlpp::Element *el = JS::GetMaybePtrFromReservedSlot(hobj, 0); + struct element_private *el_private = JS::GetMaybePtrFromReservedSlot(hobj, 1); + + if (!el || !el_private) { + args.rval().setNull(); + return true; + } + + if (argc < 2) { + args.rval().setUndefined(); + return true; + } + char *method = jsval_to_string(ctx, args[0]); + + if (!method) { + return false; + } + JS::RootedValue fun(ctx, args[1]); + + struct listener *l; + + foreach(l, el_private->listeners) { + if (strcmp(l->typ, method)) { + continue; + } + if (l->fun == fun) { + del_from_list(l); + mem_free_set(&l->typ, NULL); + mem_free(l); + mem_free(method); + args.rval().setUndefined(); + return true; + } + } + mem_free(method); + args.rval().setUndefined(); + return true; +} + static bool element_appendChild(JSContext *ctx, unsigned int argc, JS::Value *rval) { @@ -3316,14 +3464,34 @@ element_setAttribute(JSContext *ctx, unsigned int argc, JS::Value *rval) return true; } +static std::map map_privates; + JSObject * getElement(JSContext *ctx, void *node) { + auto elem = map_privates.find(node); + struct element_private *el_private = NULL; + + if (elem != map_privates.end()) { + el_private = elem->second; + } else { + el_private = (struct element_private *)mem_calloc(1, sizeof(*el_private)); + + if (!el_private) { + return NULL; + } + init_list(el_private->listeners); + } + JSObject *el = JS_NewObject(ctx, &element_class); if (!el) { + mem_free(el_private); return NULL; } + JS::Realm *comp = js::GetContextRealm(ctx); + struct ecmascript_interpreter *interpreter = (struct ecmascript_interpreter *)JS::GetRealmPrivate(comp); + el_private->interpreter = interpreter; JS::RootedObject r_el(ctx, el); @@ -3331,6 +3499,39 @@ getElement(JSContext *ctx, void *node) spidermonkey_DefineFunctions(ctx, el, element_funcs); JS::SetReservedSlot(el, 0, JS::PrivateValue(node)); + JS::SetReservedSlot(el, 1, JS::PrivateValue(el_private)); + + el_private->thisval = r_el; + map_privates[node] = el_private; return el; } + +void +check_element_event(void *elem, const char *event_name) +{ + auto el = map_privates.find(elem); + + if (el == map_privates.end()) { + return; + } + struct element_private *el_private = el->second; + struct ecmascript_interpreter *interpreter = el_private->interpreter; + JSContext *ctx = (JSContext *)interpreter->backend_data; + JS::Realm *comp = JS::EnterRealm(ctx, (JSObject *)interpreter->ac); + JS::RootedValue r_val(ctx); + interpreter->heartbeat = add_heartbeat(interpreter); + + struct listener *l; + + foreach(l, el_private->listeners) { + if (strcmp(l->typ, event_name)) { + continue; + } + JS_CallFunctionValue(ctx, el_private->thisval, l->fun, JS::HandleValueArray::empty(), &r_val); + } + done_heartbeat(interpreter->heartbeat); + JS::LeaveRealm(ctx, comp); + + check_for_rerender(interpreter, event_name); +} diff --git a/src/ecmascript/spidermonkey/element.h b/src/ecmascript/spidermonkey/element.h index c35716b6..b50fdebc 100644 --- a/src/ecmascript/spidermonkey/element.h +++ b/src/ecmascript/spidermonkey/element.h @@ -1,4 +1,3 @@ - #ifndef EL__ECMASCRIPT_SPIDERMONKEY_ELEMENT_H #define EL__ECMASCRIPT_SPIDERMONKEY_ELEMENT_H @@ -11,4 +10,6 @@ JSObject *getElement(JSContext *ctx, void *node); void walk_tree(struct string *buf, void *nod, bool start = true, bool toSortAttrs = false); +void check_element_event(void *elem, const char *event_name); + #endif diff --git a/src/ecmascript/spidermonkey/xhr.cpp b/src/ecmascript/spidermonkey/xhr.cpp index 80e91155..fa59f2b8 100644 --- a/src/ecmascript/spidermonkey/xhr.cpp +++ b/src/ecmascript/spidermonkey/xhr.cpp @@ -112,7 +112,6 @@ struct listener { JS::RootedValue fun; }; - struct classcomp { bool operator() (const std::string& lhs, const std::string& rhs) const { diff --git a/src/viewer/text/link.cpp b/src/viewer/text/link.cpp index efc727fa..71f35ef4 100644 --- a/src/viewer/text/link.cpp +++ b/src/viewer/text/link.cpp @@ -20,6 +20,12 @@ #include "document/options.h" #include "document/view.h" #include "ecmascript/ecmascript.h" + +#ifdef CONFIG_ECMASCRIPT_SMJS +#include "ecmascript/spidermonkey/element.h" +#include +#endif + #include "intl/libintl.h" #include "main/object.h" #include "protocol/uri.h" @@ -44,6 +50,7 @@ #include "viewer/text/view.h" #include "viewer/text/vs.h" +#include /* Perhaps some of these would be more fun to have in viewer/common/, dunno. * --pasky */ @@ -59,10 +66,21 @@ current_link_evhook(struct document_view *doc_view, enum script_event_hook_type assert(doc_view && doc_view->vs); link = get_current_link(doc_view); if (!link) return -1; - if (!link->event_hooks) return -1; - if (!doc_view->vs->ecmascript) return -1; + std::map *mapa = (std::map *)doc_view->document->element_map; + + if (mapa) { + auto element = (*mapa).find(link->element_offset); + + if (element != (*mapa).end()) { + const char *event_name = script_event_hook_name[(int)type]; + check_element_event(element->second, event_name); + } + } + + if (!link->event_hooks) return -1; + foreach (evhook, *link->event_hooks) { char *ret; diff --git a/test/ecmascript/clickListener.html b/test/ecmascript/clickListener.html new file mode 100644 index 00000000..54bbc3c2 --- /dev/null +++ b/test/ecmascript/clickListener.html @@ -0,0 +1,20 @@ + + + + + +
+ +
+ + diff --git a/test/ecmascript/clickListener2.html b/test/ecmascript/clickListener2.html new file mode 100644 index 00000000..5d55015b --- /dev/null +++ b/test/ecmascript/clickListener2.html @@ -0,0 +1,21 @@ + + + + + +
+ +
+ +