summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am6
-rw-r--r--test/smoke-tests/webhelper/webview2.js4
-rw-r--r--test/webhelper/testTranslate2.js16
-rw-r--r--webhelper/webextensions/wh2extension.c89
-rw-r--r--webhelper/webextensions/wh2jscutil.c65
-rw-r--r--webhelper/webextensions/wh2jscutil.h25
-rw-r--r--webhelper/webhelper2.js10
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