diff options
-rw-r--r-- | Makefile.am | 6 | ||||
-rw-r--r-- | test/smoke-tests/webhelper/webview2.js | 4 | ||||
-rw-r--r-- | test/webhelper/testTranslate2.js | 16 | ||||
-rw-r--r-- | webhelper/webextensions/wh2extension.c | 89 | ||||
-rw-r--r-- | webhelper/webextensions/wh2jscutil.c | 65 | ||||
-rw-r--r-- | webhelper/webextensions/wh2jscutil.h | 25 | ||||
-rw-r--r-- | webhelper/webhelper2.js | 10 |
7 files changed, 212 insertions, 3 deletions
diff --git a/Makefile.am b/Makefile.am index 0e54fe6..0a918ac 100644 --- a/Makefile.am +++ b/Makefile.am @@ -114,7 +114,11 @@ libwebhelper2private_la_LDFLAGS = -avoid-version webhelper2extensionsdir = $(libexecdir)/webhelper2 webhelper2extensions_LTLIBRARIES = wh2extension.la -wh2extension_la_SOURCES = webhelper/webextensions/wh2extension.c +wh2extension_la_SOURCES = \ + webhelper/webextensions/wh2extension.c \ + webhelper/webextensions/wh2jscutil.c \ + webhelper/webextensions/wh2jscutil.h \ + $(NULL) wh2extension_la_CPPFLAGS = @WEBHELPER2_EXTENSION_CFLAGS@ wh2extension_la_LIBADD = @WEBHELPER2_EXTENSION_LIBS@ wh2extension_la_LDFLAGS = -module -avoid-version -no-undefined diff --git a/test/smoke-tests/webhelper/webview2.js b/test/smoke-tests/webhelper/webview2.js index ec029c9..03ed560 100644 --- a/test/smoke-tests/webhelper/webview2.js +++ b/test/smoke-tests/webhelper/webview2.js @@ -43,6 +43,10 @@ message from parameter in this URL</a></p> \ <p>This is text that will be italicized: <span name="translatable">Hello, \ world!</span></p> \ \ +<p><button onclick="alert(gettext(\'I came from gettext\'));"> \ + Click me to use gettext \ +</button></p> \ +\ </body> \ </html>'; diff --git a/test/webhelper/testTranslate2.js b/test/webhelper/testTranslate2.js index 67f7b1f..852c3a1 100644 --- a/test/webhelper/testTranslate2.js +++ b/test/webhelper/testTranslate2.js @@ -116,4 +116,20 @@ describe('WebHelper2 translator', function () { webview.load_html('<html><body></body></html>', null); }); }); + + describe('used from client-side Javascript', function () { + it('translates a string', function (done) { + let webview = new WebKit2.WebView(); + let gettext_spy = jasmine.createSpy('gettext_spy').and.callFake((s) => { + Mainloop.quit('webhelper2'); + return s; + }); + webhelper.set_gettext(gettext_spy); + webview.load_html('<html><body><script type="text/javascript">gettext("Translate Me");</script></body></html>', + null); + Mainloop.run('webhelper2'); + expect(gettext_spy).toHaveBeenCalledWith('Translate Me'); + done(); + }); + }); }); diff --git a/webhelper/webextensions/wh2extension.c b/webhelper/webextensions/wh2extension.c index c51171e..48ea9aa 100644 --- a/webhelper/webextensions/wh2extension.c +++ b/webhelper/webextensions/wh2extension.c @@ -5,9 +5,13 @@ #include <string.h> #include <glib.h> +#include <JavaScriptCore/JavaScript.h> #include <webkit2/webkit-web-extension.h> #include <webkitdom/webkitdom.h> +#include "wh2jscutil.h" + +#define PRIVATE_NAME "_webhelper_private" #define WH2_DBUS_INTERFACE_NAME "com.endlessm.WebHelper2.Translation" #define MAIN_PROGRAM_OBJECT_PATH "/com/endlessm/gettext" #define MAIN_PROGRAM_INTERFACE_NAME "com.endlessm.WebHelper2.Gettext" @@ -74,6 +78,51 @@ translation_function (const gchar *message, return retval; } +static JSValueRef +gettext_shim (JSContextRef js, + JSObjectRef function, + JSObjectRef this_object, + size_t n_args, + const JSValueRef args[], + JSValueRef *exception) +{ + if (n_args != 1) + { + gchar *errmsg = g_strdup_printf ("Expected one argument to gettext()," + "but got %d.", n_args); + *exception = throw_exception (js, errmsg); + g_free (errmsg); + return NULL; + } + if (!JSValueIsString (js, args[0])) + { + *exception = throw_exception (js, + "Expected a string argument to gettext()."); + return NULL; + } + + JSObjectRef window = JSContextGetGlobalObject (js); + JSStringRef private_name = JSStringCreateWithUTF8CString (PRIVATE_NAME); + JSValueRef private_data = JSObjectGetProperty (js, window, private_name, + exception); + if (JSValueIsUndefined (js, private_data)) + return NULL; /* propagate exception */ + Context *ctxt = (Context *) JSObjectGetPrivate ((JSObjectRef) private_data); + + JSStringRef message_ref = JSValueToStringCopy (js, args[0], exception); + if (message_ref == NULL) + return NULL; /* propagate exception */ + gchar *message = string_ref_to_string (message_ref); + JSStringRelease (message_ref); + + gchar *translation = translation_function (message, ctxt); + g_free (message); + + JSValueRef retval = string_to_value_ref (js, translation); + g_free (translation); + return retval; +} + static void translate_html (WebKitDOMDocument *dom, Context *ctxt) @@ -216,6 +265,41 @@ on_page_created (WebKitWebExtension *extension, pctxt, NULL); } +/* window-object-cleared is the best time to define properties on the page's +window object, according to the documentation. */ +static void +on_window_object_cleared (WebKitScriptWorld *script_world, + WebKitWebPage *page, + WebKitFrame *frame, + Context *ctxt) +{ + JSGlobalContextRef js = + webkit_frame_get_javascript_context_for_script_world (frame, script_world); + JSObjectRef window = JSContextGetGlobalObject (js); + + /* First we need to create a custom class for a private data object to store + our context in, because you can't pass callback data to JavaScriptCore + callbacks. You also can't set private data on a Javascript object if it's not + of a custom class, because the built-in classes don't allocate space for a + private pointer. */ + JSClassDefinition class_def = { + .className = "PrivateContextObject" + }; + JSClassRef klass = JSClassCreate (&class_def); + JSObjectRef private_data = JSObjectMake (js, klass, ctxt); + JSClassRelease (klass); + + if (!set_object_property (js, window, PRIVATE_NAME, (JSValueRef) private_data, + kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontEnum | kJSPropertyAttributeDontDelete)) + return; + + JSObjectRef gettext_func = + JSObjectMakeFunctionWithCallback (js, NULL, gettext_shim); + if (!set_object_property (js, window, "gettext", (JSValueRef) gettext_func, + kJSPropertyAttributeNone)) + return; +} + static void on_bus_acquired (GDBusConnection *connection, const gchar *name, @@ -225,6 +309,11 @@ on_bus_acquired (GDBusConnection *connection, ctxt->connection = connection; + /* Get a notification when Javascript is ready */ + WebKitScriptWorld *script_world = webkit_script_world_get_default (); + g_signal_connect (script_world, "window-object-cleared", + G_CALLBACK (on_window_object_cleared), ctxt); + /* Export our interface on the bus */ ctxt->node = g_dbus_node_info_new_for_xml (introspection_xml, &error); if (ctxt->node == NULL) diff --git a/webhelper/webextensions/wh2jscutil.c b/webhelper/webextensions/wh2jscutil.c new file mode 100644 index 0000000..34f325f --- /dev/null +++ b/webhelper/webextensions/wh2jscutil.c @@ -0,0 +1,65 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* Copyright 2015 Endless Mobile, Inc. */ + +#include <glib.h> +#include <JavaScriptCore/JavaScript.h> + +#include "wh2jscutil.h" + +G_GNUC_INTERNAL +gboolean +set_object_property (JSContextRef js, + JSObjectRef object, + const gchar *property_name, + JSValueRef property_value, + JSPropertyAttributes flags) +{ + JSValueRef exception = NULL; + JSStringRef property_name_ref = JSStringCreateWithUTF8CString (property_name); + JSObjectSetProperty (js, object, property_name_ref, property_value, flags, + &exception); + JSStringRelease (property_name_ref); + if (exception != NULL) + { + g_critical ("There was a problem setting the property '%s'.", + property_name); + return FALSE; + } + return TRUE; +} + +/* Returns a newly allocated string. */ +G_GNUC_INTERNAL +gchar * +string_ref_to_string (JSStringRef string_ref) +{ + size_t bufsize = JSStringGetMaximumUTF8CStringSize (string_ref); + gchar *string = g_new0 (gchar, bufsize); + JSStringGetUTF8CString (string_ref, string, bufsize); + return string; +} + +G_GNUC_INTERNAL +JSValueRef +string_to_value_ref (JSContextRef js, + const gchar *string) +{ + JSStringRef string_ref = JSStringCreateWithUTF8CString (string); + JSValueRef value_ref = JSValueMakeString (js, string_ref); + /* value_ref owns string_ref now */ + return value_ref; +} + +G_GNUC_INTERNAL +JSValueRef +throw_exception (JSContextRef js, + const gchar *message) +{ + JSValueRef msgval = string_to_value_ref (js, message); + JSValueRef inner_error = NULL; + JSObjectRef exception = JSObjectMakeError (js, 1, &msgval, &inner_error); + if (inner_error != NULL) + return inner_error; + return (JSValueRef) exception; +} diff --git a/webhelper/webextensions/wh2jscutil.h b/webhelper/webextensions/wh2jscutil.h new file mode 100644 index 0000000..af800e5 --- /dev/null +++ b/webhelper/webextensions/wh2jscutil.h @@ -0,0 +1,25 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* Copyright 2015 Endless Mobile, Inc. */ + +#ifndef WH2_JSC_UTIL_H +#define WH2_JSC_UTIL_H + +#include <glib.h> +#include <JavaScriptCore/JavaScript.h> + +gboolean set_object_property (JSContextRef js, + JSObjectRef object, + const gchar *property_name, + JSValueRef property_value, + JSPropertyAttributes flags); + +gchar *string_ref_to_string (JSStringRef string_ref); + +JSValueRef string_to_value_ref (JSContextRef js, + const gchar *string); + +JSValueRef throw_exception (JSContextRef js, + const gchar *message); + +#endif /* WH2_JSC_UTIL_H */ diff --git a/webhelper/webhelper2.js b/webhelper/webhelper2.js index 2897a61..bba4c63 100644 --- a/webhelper/webhelper2.js +++ b/webhelper/webhelper2.js @@ -55,6 +55,7 @@ const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\ * WebHelper solves this problem by allowing you to mark strings in your HTML * page and translating them through a function of your choice when you run * <WebHelper.translate_html()>. + * It also exposes a *gettext()* function in the client-side Javascript. */ /** @@ -247,12 +248,17 @@ const WebHelper = new Lang.Class({ * Parameters: * gettext_func - a function, or null * - * When you plan to use the <translate_html()> function to translate text in - * your web application, set this property to the translation function. + * When you plan to translate text in your web application, set this + * property to the translation function. * The function must take one parameter, a string, and also return a * string. * The canonical example is gettext(). * + * This function will be called with each string to translate when you call + * <translate_html()>. + * The function is also made available directly to the browser-side + * Javascript as *gettext()*, a property of the global object. + * * Pass null for _gettext_func_ to unset the translation function. * * If this function has not been called, or has most recently been called |