summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Chimento <philip@endlessm.com>2015-05-15 23:04:42 -0700
committerPhilip Chimento <philip@endlessm.com>2015-05-21 13:22:19 -0700
commit6ff70f1a9ae70e3481411d27de7594477fa4d0d2 (patch)
tree66fc9e468023797401193d699e9f513683c596e3
parent820994f6f86bbc35c53cddaa7d976d81d4509ff6 (diff)
WebHelper2 for translating WebKit2 views
This adds a new Javascript module, WebHelper2. It's the WebKit2 analogue to WebHelper. It offers a facility for calling gettext() on the contents of DOM elements in your web page. It accomplishes this using an extension module that's loaded into WebKit's web process. [endlessm/eos-sdk#291]
-rw-r--r--.gitignore1
-rw-r--r--Makefile.am35
-rw-r--r--configure.ac6
-rw-r--r--jasmine.json5
-rw-r--r--test/Makefile.am.inc2
-rw-r--r--test/smoke-tests/webhelper/webview2.js83
-rw-r--r--test/webhelper/testTranslate2.js119
-rw-r--r--webhelper/webextensions/wh2extension.c298
-rw-r--r--webhelper/webhelper2.js322
-rw-r--r--webhelper/webhelper_private/config.js.in1
10 files changed, 871 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index 79a0993..bffe54c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ endless/eosresource.c
endless/eosresource-private.h
tools/eos-application-manifest/eos-application-manifest
tools/eos-json-extractor/eos-json-extractor
+webhelper/webhelper_private/config.js
*.py[cod]
diff --git a/Makefile.am b/Makefile.am
index 575d6f2..5dc8696 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -46,6 +46,27 @@ dist-hook::
@false
endif
+# # # SUBSTITUTED FILES # # #
+# These files need to be filled in with make variables
+
+subst = $(SED) \
+ -e 's,%libexecdir%,$(libexecdir),g' \
+ $(NULL)
+
+subst_files = \
+ webhelper/webhelper_private/config.js \
+ $(NULL)
+
+$(subst_files): %: %.in Makefile
+ $(AM_V_GEN)$(MKDIR_P) $(@D) && \
+ rm -f $@ $@.tmp && \
+ $(subst) $< > $@.tmp && \
+ chmod a-w $@.tmp && \
+ mv $@.tmp $@
+
+CLEANFILES += $(subst_files)
+EXTRA_DIST += $(patsubst %,%.in,$(subst_files))
+
# # # LIBRARY # # #
# Main Open Endless SDK library
@@ -64,16 +85,28 @@ DISTCLEANFILES += @EOS_SDK_API_NAME@.pc
# # # WEBHELPER LIBRARY # # #
-webhelper_sources = webhelper/webhelper.js
+webhelper_sources = \
+ webhelper/webhelper.js \
+ webhelper/webhelper2.js \
+ $(NULL)
gjsmodulesdir = $(datadir)/gjs-1.0
webhelperdir = $(gjsmodulesdir)
+webhelper_privatedir = $(webhelperdir)/webhelper_private
dist_webhelper_DATA = \
$(webhelper_sources) \
$(NULL)
+dist_webhelper_private_DATA = webhelper/webhelper_private/config.js
EOS_JS_COVERAGE_FILES = $(webhelper_sources)
+webhelper2extensionsdir = $(libexecdir)/webhelper2
+webhelper2extensions_LTLIBRARIES = wh2extension.la
+wh2extension_la_SOURCES = webhelper/webextensions/wh2extension.c
+wh2extension_la_CPPFLAGS = @WEBHELPER2_EXTENSION_CFLAGS@
+wh2extension_la_LIBADD = @WEBHELPER2_EXTENSION_LIBS@
+wh2extension_la_LDFLAGS = -module -avoid-version -no-undefined
+
# # # INTROSPECTION FILES # # #
-include $(INTROSPECTION_MAKEFILE)
diff --git a/configure.ac b/configure.ac
index 2f1d344..fe37a8b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -83,6 +83,7 @@ GOBJECT_REQUIREMENT="gobject-2.0"
GIO_REQUIREMENT="gio-2.0"
GTK_REQUIREMENT="gtk+-3.0 >= 3.10"
JSON_GLIB_REQUIREMENT="json-glib-1.0 >= 0.12"
+WEBKIT2_REQUIREMENT="webkit2gtk-4.0"
# These go into the pkg-config file as Requires: and Requires.private:
# (Generally, use Requires.private: instead of Requires:)
EOS_REQUIRED_MODULES=
@@ -216,10 +217,15 @@ AC_SUBST([JASMINE_REPORT_ARGUMENT])
PKG_CHECK_MODULES([EOS_SDK], [
$EOS_REQUIRED_MODULES
$EOS_REQUIRED_MODULES_PRIVATE])
+PKG_CHECK_MODULES([WEBHELPER2_EXTENSION], [
+ $GLIB_REQUIREMENT
+ $GOBJECT_REQUIREMENT
+ $WEBKIT2_REQUIREMENT])
# Check installed GIRs for webhelper JS module
EOS_CHECK_GJS_GIR([GLib], [2.0])
EOS_CHECK_GJS_GIR([WebKit], [3.0])
+EOS_CHECK_GJS_GIR([WebKit2], [4.0])
# Code coverage reports support
EOS_COVERAGE_REPORT([c js])
diff --git a/jasmine.json b/jasmine.json
index ff37382..4d2ef8b 100644
--- a/jasmine.json
+++ b/jasmine.json
@@ -6,6 +6,11 @@
"test/webhelper",
"test/tools/eos-application-manifest"
],
+ "exclude": [
+ "test/webhelper/testTranslate.js",
+ "test/webhelper/testUpdateFontSize.js",
+ "test/webhelper/testWebActions.js"
+ ],
"environment": {
"GI_TYPELIB_PATH": ".",
"LD_LIBRARY_PATH": ".libs",
diff --git a/test/Makefile.am.inc b/test/Makefile.am.inc
index 11e4dea..7e10953 100644
--- a/test/Makefile.am.inc
+++ b/test/Makefile.am.inc
@@ -44,6 +44,7 @@ EXTRA_DIST += \
javascript_tests = \
test/tools/eos-application-manifest/testInit.js \
test/webhelper/testTranslate.js \
+ test/webhelper/testTranslate2.js \
test/webhelper/testWebActions.js \
test/webhelper/testUpdateFontSize.js \
test/endless/testCustomContainer.js \
@@ -78,4 +79,5 @@ TESTS_ENVIRONMENT = \
export GI_TYPELIB_PATH="$(top_builddir)$${GI_TYPELIB_PATH:+:$$GI_TYPELIB_PATH}"; \
export LD_LIBRARY_PATH="$(top_builddir)/.libs$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH}"; \
export XDG_CONFIG_HOME=`mktemp -d $${TMPDIR:-/tmp}/sdktestconfig.XXXXXXXX`; \
+ export WEBHELPER_UNINSTALLED_EXTENSION_DIR="$(top_builddir)/.libs"; \
$(NULL)
diff --git a/test/smoke-tests/webhelper/webview2.js b/test/smoke-tests/webhelper/webview2.js
new file mode 100644
index 0000000..604819c
--- /dev/null
+++ b/test/smoke-tests/webhelper/webview2.js
@@ -0,0 +1,83 @@
+// Copyright 2015 Endless Mobile, Inc.
+
+const Endless = imports.gi.Endless;
+const Gettext = imports.gettext;
+const Lang = imports.lang;
+const WebHelper2 = imports.webhelper2;
+const WebKit2 = imports.gi.WebKit2;
+
+const TEST_APPLICATION_ID = 'com.endlessm.example.test-webview2';
+const TEST_HTML = '\
+<html> \
+<head> \
+<title>First page</title> \
+<style> \
+p, form { \
+ width: 50%; \
+ padding: 1em; \
+ background: #FFFFFF; \
+} \
+body { \
+ background: #EEEEEE; \
+} \
+</style> \
+</head> \
+\
+<body> \
+<h1>First page</h1> \
+\
+<p><a href="http://wikipedia.org">Regular link to a Web site</a></p> \
+\
+<p>This is text that will be italicized: <span name="translatable">Hello, \
+world!</span></p> \
+\
+</body> \
+</html>';
+
+const TestApplication = new Lang.Class({
+ Name: 'TestApplication',
+ Extends: Endless.Application,
+
+ vfunc_dbus_register: function (connection, object_path) {
+ this._webhelper = new WebHelper2.WebHelper({
+ well_known_name: this.application_id,
+ connection: connection,
+ });
+ return this.parent(connection, object_path);
+ },
+
+ vfunc_startup: function () {
+ this.parent();
+
+ this._webhelper.set_gettext((s) => s.italics());
+
+ this._webview = new WebKit2.WebView();
+ this._webview.connect('load-changed', (webview, event) => {
+ if (event === WebKit2.LoadEvent.FINISHED)
+ this._webhelper.translate_html(webview, null, (obj, res) => {
+ this._webhelper.translate_html_finish(res);
+ });
+ });
+ this._webview.load_html(TEST_HTML, 'file://');
+
+ this._window = new Endless.Window({
+ application: this,
+ border_width: 16,
+ });
+
+ this._pm = this._window.page_manager;
+ this._pm.add(this._webview, { name: 'page1' });
+
+ this._window.show_all();
+ },
+
+ vfunc_dbus_unregister: function (connection, object_path) {
+ this.parent(connection, object_path);
+ this._webhelper.unregister();
+ },
+});
+
+let app = new TestApplication({
+ application_id: TEST_APPLICATION_ID,
+});
+app.run(ARGV);
diff --git a/test/webhelper/testTranslate2.js b/test/webhelper/testTranslate2.js
new file mode 100644
index 0000000..67f7b1f
--- /dev/null
+++ b/test/webhelper/testTranslate2.js
@@ -0,0 +1,119 @@
+// Copyright 2015 Endless Mobile, Inc.
+
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const WebHelper2 = imports.webhelper2;
+const WebKit2 = imports.gi.WebKit2;
+
+const WELL_KNOWN_NAME = 'com.endlessm.WebHelper.testTranslate2';
+
+Gtk.init(null);
+
+describe('WebHelper2 translator', function () {
+ let webhelper, owner_id, connection;
+
+ beforeAll(function (done) {
+ owner_id = Gio.DBus.own_name(Gio.BusType.SESSION, WELL_KNOWN_NAME,
+ Gio.BusNameOwnerFlags.NONE,
+ null, // bus acquired
+ (con, name) => { // name acquired
+ connection = con;
+ done();
+ },
+ null); // name lost
+ });
+
+ afterAll(function () {
+ Gio.DBus.unown_name(owner_id);
+ });
+
+ beforeEach(function () {
+ webhelper = new WebHelper2.WebHelper({
+ well_known_name: WELL_KNOWN_NAME,
+ connection: connection,
+ });
+ });
+
+ afterEach(function () {
+ webhelper.unregister();
+ });
+
+ it('complains about a bad gettext function', function () {
+ expect(function () {
+ webhelper.set_gettext('I am not a function');
+ }).toThrow();
+ });
+
+ it('gets and sets the gettext function', function () {
+ let translation_function = (s) => s;
+ webhelper.set_gettext(translation_function);
+ expect(webhelper.get_gettext()).toBe(translation_function);
+ });
+
+ it('has a null gettext function by default', function () {
+ expect(webhelper.get_gettext()).toBeNull();
+ });
+
+ it('can remove the gettext function by setting null', function () {
+ webhelper.set_gettext((s) => s);
+ expect(webhelper.get_gettext()).not.toBeNull();
+ webhelper.set_gettext(null);
+ expect(webhelper.get_gettext()).toBeNull();
+ });
+
+ describe('translating a page', function () {
+ let webview;
+
+ function run_loop() {
+ webview.connect('load-changed', (webview, event) => {
+ if (event === WebKit2.LoadEvent.FINISHED) {
+ webhelper.translate_html(webview, null, (obj, res) => {
+ webhelper.translate_html_finish(res);
+ Mainloop.quit('webhelper2');
+ });
+ }
+ });
+ webview.load_html('<html><body><p name="translatable">Translate Me</p></body></html>',
+ null);
+ Mainloop.run('webhelper2');
+ }
+
+ beforeEach(function () {
+ webview = new WebKit2.WebView();
+ });
+
+ it('translates a string', function () {
+ let gettext_spy = jasmine.createSpy('gettext_spy').and.callFake((s) => s);
+ webhelper.set_gettext(gettext_spy);
+ run_loop();
+ expect(gettext_spy).toHaveBeenCalledWith('Translate Me');
+ });
+
+ // The following test is disabled because GJS cannot catch exceptions
+ // across FFI interfaces (e.g. in GObject callbacks.)
+ xit('complains about a gettext function not being set', function () {
+ expect(function () {
+ run_loop();
+ }).toThrow();
+ });
+
+ it('can cancel the translation operation', function (done) {
+ webhelper.set_gettext((s) => s);
+ webview.connect('load-changed', (webview, event) => {
+ if (event === WebKit2.LoadEvent.FINISHED) {
+ let cancellable = new Gio.Cancellable();
+ cancellable.cancel();
+ webhelper.translate_html(webview, cancellable, (obj, res) => {
+ expect(function () {
+ webhelper.translate_html_finish(res);
+ }).toThrow();
+ done();
+ });
+ }
+ });
+ webview.load_html('<html><body></body></html>', null);
+ });
+ });
+});
diff --git a/webhelper/webextensions/wh2extension.c b/webhelper/webextensions/wh2extension.c
new file mode 100644
index 0000000..c51171e
--- /dev/null
+++ b/webhelper/webextensions/wh2extension.c
@@ -0,0 +1,298 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#include <string.h>
+
+#include <glib.h>
+#include <webkit2/webkit-web-extension.h>
+#include <webkitdom/webkitdom.h>
+
+#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"
+
+/* Declaration of externally visible symbol */
+void webkit_web_extension_initialize_with_user_data (WebKitWebExtension *, const GVariant *);
+
+typedef struct {
+ GDBusConnection *connection; /* unowned */
+ GDBusNodeInfo *node; /* owned */
+ GDBusInterfaceInfo *interface; /* owned by node */
+ GSList *bus_ids; /* GSList<guint>; owned */
+ GSList *page_ctxts; /* GSList<PageContext *>; owned */
+ gchar *main_program_name; /* owned; well-known-name of main program */
+} Context;
+
+typedef struct {
+ Context *ctxt; /* unowned */
+ WebKitWebPage *page; /* unowned */
+} PageContext;
+
+static const gchar introspection_xml[] =
+ "<node>"
+ "<interface name='" WH2_DBUS_INTERFACE_NAME "'>"
+ "<method name='Translate'/>"
+ "</interface>"
+ "</node>";
+
+static void
+context_free (Context *ctxt)
+{
+ g_clear_pointer (&ctxt->node, g_dbus_node_info_unref);
+ g_clear_pointer (&ctxt->bus_ids, g_slist_free);
+ g_clear_pointer (&ctxt->main_program_name, g_free);
+ g_slist_free_full (ctxt->page_ctxts, (GDestroyNotify) g_free);
+ ctxt->page_ctxts = NULL;
+ g_free (ctxt);
+}
+
+static gchar *
+translation_function (const gchar *message,
+ Context *ctxt)
+{
+ GError *error = NULL;
+ GVariant *result =
+ g_dbus_connection_call_sync (ctxt->connection, ctxt->main_program_name,
+ MAIN_PROGRAM_OBJECT_PATH,
+ MAIN_PROGRAM_INTERFACE_NAME, "Gettext",
+ g_variant_new ("(s)", message),
+ (GVariantType *) "(s)",
+ G_DBUS_CALL_FLAGS_NO_AUTO_START,
+ -1 /* timeout */, NULL /* cancellable */,
+ &error);
+ if (result == NULL)
+ {
+ g_warning ("No return value from gettext: %s", error->message);
+ g_clear_error (&error);
+ return g_strdup (message);
+ }
+
+ gchar *retval;
+ g_variant_get (result, "(s)", &retval);
+ g_variant_unref (result);
+ return retval;
+}
+
+static void
+translate_html (WebKitDOMDocument *dom,
+ Context *ctxt)
+{
+ WebKitDOMNodeList *translatable;
+ GError *error = NULL;
+
+ translatable = webkit_dom_document_get_elements_by_name (dom, "translatable");
+
+ gulong index, length = webkit_dom_node_list_get_length (translatable);
+ for (index = 0; index < length; index++)
+ {
+ WebKitDOMNode *element = webkit_dom_node_list_item (translatable, index);
+
+ /* Translate the text */
+ if (WEBKIT_DOM_IS_HTML_ELEMENT (element))
+ {
+ WebKitDOMHTMLElement *el_html = WEBKIT_DOM_HTML_ELEMENT (element);
+ gchar *inner_html = webkit_dom_html_element_get_inner_html (el_html);
+ gchar *translated_html = translation_function (inner_html, ctxt);
+ webkit_dom_html_element_set_inner_html (el_html, translated_html,
+ &error);
+ if (error != NULL)
+ {
+ g_warning ("There was a problem translating '%s' to '%s': %s",
+ inner_html, translated_html, error->message);
+ g_clear_error (&error);
+ }
+
+ g_free (translated_html);
+ g_free (inner_html);
+ }
+ else
+ {
+ gchar *text = webkit_dom_node_get_text_content (element);
+ gchar *translated_text = translation_function (text, ctxt);
+ webkit_dom_node_set_text_content (element, translated_text, &error);
+ if (error != NULL)
+ {
+ g_warning ("There was a problem translating '%s' to '%s': %s",
+ text, translated_text, error->message);
+ g_clear_error (&error);
+ }
+
+ g_free (translated_text);
+ g_free (text);
+ }
+ }
+
+ g_object_unref (translatable);
+}
+
+static void
+on_wh2_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ PageContext *pctxt)
+{
+ if (strcmp (method_name, "Translate") != 0)
+ {
+ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_UNKNOWN_METHOD,
+ "Unknown method %s invoked on interface %s",
+ method_name, interface_name);
+ return;
+ }
+
+ WebKitDOMDocument *document = webkit_web_page_get_dom_document (pctxt->page);
+ if (document == NULL)
+ {
+ g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR,
+ G_IO_ERROR_NOT_INITIALIZED,
+ "The web page has not loaded a document yet");
+ return;
+ }
+
+ translate_html (document, pctxt->ctxt);
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+}
+
+static GDBusInterfaceVTable dbus_impl_vtable = {
+ (GDBusInterfaceMethodCallFunc) on_wh2_method_call,
+ NULL, /* get property */
+ NULL /* set property */
+};
+
+static gboolean
+register_object (PageContext *pctxt)
+{
+ GError *error = NULL;
+
+ if (pctxt->ctxt->connection == NULL)
+ return G_SOURCE_CONTINUE; /* Try again when the connection is ready */
+
+ /* The ID is known to the main process and the web process. So we can address
+ a specific web page over DBus. */
+ guint64 id = webkit_web_page_get_id (pctxt->page);
+
+ gchar *object_path = g_strdup_printf("/com/endlessm/webview/%"
+ G_GUINT64_FORMAT, id);
+
+ guint bus_id = g_dbus_connection_register_object (pctxt->ctxt->connection,
+ object_path,
+ pctxt->ctxt->interface,
+ &dbus_impl_vtable,
+ pctxt, NULL,
+ &error);
+ if (bus_id == 0)
+ {
+ g_critical ("Failed to export webview object on bus: %s", error->message);
+ g_clear_error (&error);
+ goto out;
+ }
+
+ pctxt->ctxt->bus_ids = g_slist_prepend (pctxt->ctxt->bus_ids,
+ GUINT_TO_POINTER (bus_id));
+
+out:
+ g_free (object_path);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+on_page_created (WebKitWebExtension *extension,
+ WebKitWebPage *page,
+ Context *ctxt)
+{
+ PageContext *pctxt = g_new0 (PageContext, 1);
+ pctxt->ctxt = ctxt;
+ pctxt->page = page;
+
+ ctxt->page_ctxts = g_slist_prepend (ctxt->page_ctxts, pctxt);
+
+ g_idle_add_full (G_PRIORITY_HIGH_IDLE, (GSourceFunc) register_object,
+ pctxt, NULL);
+}
+
+static void
+on_bus_acquired (GDBusConnection *connection,
+ const gchar *name,
+ Context *ctxt)
+{
+ GError *error = NULL;
+
+ ctxt->connection = connection;
+
+ /* Export our interface on the bus */
+ ctxt->node = g_dbus_node_info_new_for_xml (introspection_xml, &error);
+ if (ctxt->node == NULL)
+ goto fail;
+ ctxt->interface = g_dbus_node_info_lookup_interface (ctxt->node,
+ WH2_DBUS_INTERFACE_NAME);
+ if (ctxt->interface == NULL)
+ goto fail;
+
+ return;
+
+fail:
+ if (error != NULL)
+ {
+ g_critical ("Error hooking up web extension DBus interface: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+ else
+ {
+ g_critical ("Unknown error hooking up web extension DBus interface");
+ }
+}
+
+static void
+unregister_object (gpointer data,
+ GDBusConnection *connection)
+{
+ guint bus_id = GPOINTER_TO_UINT (data);
+ if (!g_dbus_connection_unregister_object (connection, bus_id))
+ g_critical ("Trouble unregistering object");
+}
+
+static void
+on_name_lost (GDBusConnection *connection,
+ const gchar *name,
+ Context *ctxt)
+{
+ if (connection == NULL)
+ {
+ g_warning ("Could not initialize DBus interface for WebHelper2 "
+ "extension; the name %s was lost.", name);
+ return;
+ }
+
+ g_slist_foreach (ctxt->bus_ids, (GFunc) unregister_object, connection);
+}
+
+/* Receives the main program's unique DBus name as user data. */
+G_MODULE_EXPORT void
+webkit_web_extension_initialize_with_user_data (WebKitWebExtension *extension,
+ const GVariant *data_from_app)
+{
+ const gchar *name = g_variant_get_string ((GVariant *) data_from_app, NULL);
+
+ Context *ctxt = g_new0 (Context, 1);
+ ctxt->main_program_name = g_strdup (name);
+ gchar *well_known_name = g_strconcat (name, ".webhelper", NULL);
+
+ g_signal_connect (extension, "page-created",
+ G_CALLBACK (on_page_created), ctxt);
+
+ g_bus_own_name (G_BUS_TYPE_SESSION, well_known_name,
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ (GBusAcquiredCallback) on_bus_acquired,
+ NULL, /* name acquired callback */
+ (GBusNameLostCallback) on_name_lost,
+ ctxt, (GDestroyNotify) context_free);
+
+ g_free (well_known_name);
+}
diff --git a/webhelper/webhelper2.js b/webhelper/webhelper2.js
new file mode 100644
index 0000000..beb0333
--- /dev/null
+++ b/webhelper/webhelper2.js
@@ -0,0 +1,322 @@
+// Copyright 2015 Endless Mobile, Inc.
+
+imports.gi.versions.WebKit2 = '4.0';
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Lang = imports.lang;
+const WebKit2 = imports.gi.WebKit2;
+
+const Config = imports.webhelper_private.config;
+
+const DBUS_WEBVIEW_EXPORT_PATH = '/com/endlessm/webview/';
+const WH2_DBUS_EXTENSION_INTERFACE = '\
+ <node> \
+ <interface name="com.endlessm.WebHelper2.Translation"> \
+ <method name="Translate"/> \
+ </interface> \
+ </node>';
+const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\
+ <node> \
+ <interface name="com.endlessm.WebHelper2.Gettext"> \
+ <method name="Gettext"> \
+ <arg name="message" type="s" direction="in"/> \
+ <arg name="translation" type="s" direction="out"/> \
+ </method> \
+ </interface> \
+ </node>';
+
+/**
+ * Namespace: WebHelper2
+ * Convenience library for running web applications
+ *
+ * WebHelper is a convenience library for displaying web applications inside a
+ * GTK container running WebKitGTK.
+ * WebHelper2 is the WebKit2 version.
+ *
+ * One often-encountered problem is localizing text through the same API as
+ * your main GTK program.
+ * 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()>.
+ */
+
+/**
+ * Class: WebHelper
+ * Helper object for a WebKit2 web application
+ *
+ * Constructor parameters:
+ * props - a dictionary of construction properties and values (default {})
+ *
+ * The application class for your web application should create <WebHelper> in
+ * its *vfunc_dbus_register()* implementation, with appropriate
+ * <well-known-name> and <connection> parameters.
+ * After that, you can do further setup on it, such as defining web actions, in
+ * your *vfunc_startup()* implementation.
+ *
+ * There is no need to set up specially any web views that you create, unlike
+ * WebKit1 where you must set up <Application.web_actions_handler()>.
+ *
+ * Example:
+ * > const TestApplication = new Lang.Class({
+ * > Name: 'TestApplication',
+ * > Extends: Gtk.Application,
+ * >
+ * > vfunc_dbus_register: function (connection, object_path) {
+ * > this._webhelper = new WebHelper2.WebHelper({
+ * > well_known_name: this.application_id,
+ * > connection: connection,
+ * > });
+ * > return this.parent(connection, object_path);
+ * > },
+ * >
+ * > vfunc_startup: function () {
+ * > this.parent();
+ * >
+ * > this._webhelper.set_gettext(Gettext.dgettext.bind(null,
+ * > GETTEXT_DOMAIN));
+ * >
+ * > let window = new Gtk.Window();
+ * > let webview = new WebKit2.WebView();
+ * > webview.connect('load-changed', (webview, event) => {
+ * > if (event === WebKit2.LoadEvent.FINISHED)
+ * > this._webhelper.translate_html(webview, null, (obj, res) => {
+ * > this._webhelper.translate_html_finish(res);
+ * > window.show_all();
+ * > });
+ * > });
+ * > window.add(webview);
+ * > webview.load_uri('file:///path/to/my/page.html');
+ * > },
+ * >
+ * > vfunc_dbus_unregister: function (connection, object_path) {
+ * > this.parent(connection, object_path);
+ * > this._webhelper.unregister();
+ * > },
+ * >});
+ * >
+ * >let app = new TestApplication({
+ * > application_id: 'com.example.SmokeGrinder',
+ * >});
+ * >app.run(ARGV);
+ */
+const WebHelper = new Lang.Class({
+ Name: 'WebHelper',
+ GTypeName: 'Wh2WebHelper',
+ Extends: GObject.Object,
+ Properties: {
+ /**
+ * Property: well-known-name
+ * Well-known bus name owned by the calling program
+ *
+ * Type:
+ * string
+ *
+ * This property is required at construction time.
+ * It must conform to <the rules for well-known bus names at
+ * http://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names>.
+ *
+ * This must be a well-known bus name that your program owns.
+ * The easiest way to ensure that is to use your application's ID, since
+ * every application class registers its ID as a bus name.
+ */
+ 'well-known-name': GObject.ParamSpec.string('well-known-name',
+ 'Well-known name',
+ 'Well-known bus name owned by the calling program',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ ''),
+ /**
+ * Property: connection
+ * DBus connection owned by the calling program
+ *
+ * Type:
+ * *Gio.DBusConnection*
+ *
+ * This property is required at construction time.
+ *
+ * This must be a DBus connection object that your program owns.
+ * The easiest way to ensure that is to use the connection object passed
+ * in to your application's *vfunc_dbus_register()* function.
+ */
+ 'connection': GObject.ParamSpec.object('connection', 'Connection',
+ 'DBus connection owned by the calling program',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ Gio.DBusConnection.$gtype),
+ },
+
+ _init: function (props={}) {
+ this._gettext = null;
+ this._ProxyConstructor =
+ Gio.DBusProxy.makeProxyWrapper(WH2_DBUS_EXTENSION_INTERFACE);
+ this.parent(props);
+
+ if (this.well_known_name === '')
+ throw new Error('The "well-known-name" parameter is required.');
+ this._extension_name = this.well_known_name + '.webhelper';
+
+ // Set up Webkit to load our web extension
+ let context = WebKit2.WebContext.get_default();
+ context.connect('initialize-web-extensions', () => {
+ let libexec = Gio.File.new_for_path(Config.LIBEXECDIR);
+ let path = libexec.get_child('webhelper2').get_path();
+
+ let localpath = GLib.getenv('WEBHELPER_UNINSTALLED_EXTENSION_DIR');
+ if (localpath)
+ path = localpath;
+
+ context.set_web_extensions_directory(path);
+ context.set_web_extensions_initialization_user_data(new GLib.Variant('s',
+ this.well_known_name));
+ });
+
+ // Export our own DBus interface
+ this._dbus_impl =
+ Gio.DBusExportedObject.wrapJSObject(WH2_DBUS_MAIN_PROGRAM_INTERFACE,
+ this);
+ this._dbus_impl.export(this.connection, '/com/endlessm/gettext');
+ },
+
+ // DBus implementations
+
+ Gettext: function (string) {
+ return this._gettext(string);
+ },
+
+ // Public API
+
+ /**
+ * Method: set_gettext
+ * Define function which translates text
+ *
+ * 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.
+ * The function must take one parameter, a string, and also return a
+ * string.
+ * The canonical example is gettext().
+ *
+ * Pass null for _gettext_func_ to unset the translation function.
+ *
+ * If this function has not been called, or has most recently been called
+ * with null, then it is illegal to call <translate_html()>.
+ *
+ * Example:
+ * > const Gettext = imports.gettext;
+ * > const GETTEXT_DOMAIN = 'smoke-grinder';
+ * >
+ * > webhelper.set_gettext(Gettext.dgettext.bind(null, GETTEXT_DOMAIN));
+ */
+ set_gettext: function (gettext_func) {
+ if (gettext_func !== null && typeof gettext_func !== 'function')
+ throw new Error('The translation function must be a function, or ' +
+ 'null.');
+ this._gettext = gettext_func;
+ },
+
+ /**
+ * Method: get_gettext
+ * Retrieve the currently set translation function
+ *
+ * Returns:
+ * the translation function previously set with <set_gettext()>, or null
+ * if none is currently set.
+ */
+ get_gettext: function () {
+ return this._gettext;
+ },
+
+ /**
+ * Method: translate_html
+ * Translate the HTML page in a webview asynchronously
+ *
+ * Parameters:
+ * webview - a *WebKit2.WebView* with HTML loaded
+ * cancellable - a *Gio.Cancellable*, or null
+ * callback - a function that takes two parameters: this <WebHelper>
+ * object, and a result object; or null if you don't want a callback
+ *
+ * Perform translation on all HTML elements marked with name="translatable"
+ * in the HTML document displaying in _webview_.
+ * The translation will be performed using the function you have set using
+ * <set_gettext()>.
+ *
+ * When the translation is finished, _callback_ will be called.
+ * You can get the result of the operation by calling
+ * <translate_html_finish()> with the _result_ object passed to _callback_.
+ *
+ * You can optionally pass _cancellable_ if you want to be able to cancel
+ * the operation.
+ *
+ * Example:
+ * > webview.connect('load-changed', (webview, event) => {
+ * > if (event === WebKit2.LoadEvent.FINISHED)
+ * > webhelper.translate_html(webview, null, (obj, res) => {
+ * > webhelper.translate_html_finish(res);
+ * > // Translation done, show the page
+ * > webview.show_all();
+ * > });
+ * > });
+ */
+ translate_html: function (webview, cancellable, callback) {
+ let task = { callback: callback };
+ // Wait for the DBus interface to appear on the bus
+ task.watch_id = Gio.DBus.watch_name(Gio.BusType.SESSION,
+ this._extension_name, Gio.BusNameWatcherFlags.NONE,
+ (connection, name, owner) => {
+ // name appeared
+ let webview_object_path = DBUS_WEBVIEW_EXPORT_PATH +
+ webview.get_page_id();
+ let proxy = new this._ProxyConstructor(connection,
+ this._extension_name, webview_object_path);
+ if (cancellable)
+ proxy.TranslateRemote(cancellable,
+ this._translate_callback.bind(this, task));
+ else
+ proxy.TranslateRemote(this._translate_callback.bind(this,
+ task));
+ },
+ null); // do nothing when name vanishes
+ return task;
+ },
+
+ _translate_callback: function (task, result, error) {
+ Gio.DBus.unwatch_name(task.watch_id);
+ if (error)
+ task.error = error;
+ if (task.callback)
+ task.callback(this, task);
+ },
+
+ /**
+ * Method: translate_html_finish
+ * Get the result of <translate_html()>
+ *
+ * Parameters:
+ * res - result object passed to your callback
+ *
+ * Finishes an operation started by <translate_html()>.
+ * If the operation ended in an error, this function will throw that error.
+ */
+ translate_html_finish: function (res) {
+ if (res.error)
+ throw res.error;
+ },
+
+ /**
+ * Method: unregister
+ * Break the connection to WebKit
+ *
+ * Breaks the connection to all webviews and removes all DBus objects.
+ * You should call this in your application's *vfunc_dbus_unregister()*
+ * implementation.
+ *
+ * After this function has been called, no WebHelper functionality will
+ * work.
+ */
+ unregister: function () {
+ this._dbus_impl.unexport_from_connection(this.connection);
+ },
+});
diff --git a/webhelper/webhelper_private/config.js.in b/webhelper/webhelper_private/config.js.in
new file mode 100644
index 0000000..f9d87cb
--- /dev/null
+++ b/webhelper/webhelper_private/config.js.in
@@ -0,0 +1 @@
+const LIBEXECDIR = '%libexecdir%';