Bug 1455649 - DocumentL10n, part 5 - Add C++ DocumentL10n API. draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Mon, 25 Jun 2018 09:59:51 -0700
changeset 815119 3505693c7bf116c0331cb23462708f9e40df5578
parent 815118 268829f20dde3f865af4589fd86c20325c83cdab
child 815120 989759311307e74ceb9a5d3cc4c7442b01d7083a
push id115447
push userbmo:gandalf@aviary.pl
push dateFri, 06 Jul 2018 20:45:09 +0000
bugs1455649
milestone63.0a1
Bug 1455649 - DocumentL10n, part 5 - Add C++ DocumentL10n API. DocumentL10n is a DOM C++ API which serves as a bridge between nsIDocument and mozDOMLocalization APIs. MozReview-Commit-ID: 8LfOR4Haqlu
dom/payments/moz.build
dom/webidl/DocumentL10n.webidl
dom/webidl/moz.build
intl/l10n/DocumentL10n.cpp
intl/l10n/DocumentL10n.h
intl/l10n/moz.build
--- a/dom/payments/moz.build
+++ b/dom/payments/moz.build
@@ -14,16 +14,17 @@ EXPORTS += [
 ]
 
 EXPORTS.mozilla.dom += [
     'PaymentAddress.h',
     'PaymentMethodChangeEvent.h',
     'PaymentRequest.h',
     'PaymentRequestManager.h',
     'PaymentRequestUpdateEvent.h',
+    'PaymentRequestUtils.h',
     'PaymentResponse.h',
 ]
 
 UNIFIED_SOURCES += [
     'BasicCardPayment.cpp',
     'PaymentActionRequest.cpp',
     'PaymentActionResponse.cpp',
     'PaymentAddress.cpp',
new file mode 100644
--- /dev/null
+++ b/dom/webidl/DocumentL10n.webidl
@@ -0,0 +1,28 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+dictionary L10nKey {
+  required DOMString id;
+
+  object? args = null;
+};
+
+[NoInterfaceObject]
+interface DocumentL10n {
+  void onDocumentParsed();
+
+  [NewObject] Promise<sequence<DOMString>> formatMessages(sequence<L10nKey> aKeys);
+  [NewObject] Promise<sequence<DOMString>> formatValues(sequence<L10nKey> aKeys);
+  [NewObject] Promise<DOMString> formatValue(DOMString aId, optional object aArgs);
+
+  [Throws] void setAttributes(Element aElement, DOMString aId, optional object aArgs);
+  [Throws] L10nKey getAttributes(Element aElement);
+
+  [NewObject] Promise<void> translateFragment(Element aElement);
+  [NewObject] Promise<void> translateElements(sequence<Element> aElements);
+
+  [Throws] readonly attribute Promise<any> ready;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -460,16 +460,17 @@ WEBIDL_FILES = [
     'DataTransferItemList.webidl',
     'DecoderDoctorNotification.webidl',
     'DedicatedWorkerGlobalScope.webidl',
     'DelayNode.webidl',
     'DeviceMotionEvent.webidl',
     'Directory.webidl',
     'Document.webidl',
     'DocumentFragment.webidl',
+    'DocumentL10n.webidl',
     'DocumentOrShadowRoot.webidl',
     'DocumentTimeline.webidl',
     'DocumentType.webidl',
     'DOMError.webidl',
     'DOMException.webidl',
     'DOMImplementation.webidl',
     'DOMMatrix.webidl',
     'DOMParser.webidl',
new file mode 100644
--- /dev/null
+++ b/intl/l10n/DocumentL10n.cpp
@@ -0,0 +1,269 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/DocumentL10n.h"
+#include "mozilla/dom/DocumentL10nBinding.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/PaymentRequestUtils.h" // SerializeFromJSVal
+#include "nsQueryObject.h"
+#include "mozilla/dom/Promise.h"
+#include "nsISupports.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DocumentL10n, mDocument, mDOMLocalization)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DocumentL10n)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DocumentL10n)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/**
+ * This can get lazily initialized at multiple points during the lifecycle
+ * of the document. It can get initialized when the first link is registered,
+ * or while document is still being parsed, or much later.
+ *
+ * For that reason we initialize the `DocumentLocalization` with information
+ * about those two states, and if either of them is `false` at the time
+ * of initialization, the corresponding method is expected to be fired
+ * by the parser later.
+ */
+DocumentL10n::DocumentL10n(nsIDocument* aDocument)
+  : mDocument(aDocument),
+    mState(DocumentL10nState::Initialized)
+{
+  nsresult rv;
+  nsCOMPtr<mozIDOMLocalization> domL10n = do_CreateInstance("@mozilla.org/intl/domlocalization;1", &rv);
+  if (NS_FAILED(rv)) {
+    return;
+  }
+
+  mDOMLocalization = domL10n;
+}
+
+DocumentL10n::~DocumentL10n()
+{
+  if (mDOMLocalization) {
+    mDOMLocalization->UnregisterObservers();
+  }
+  mDOMLocalization = nullptr;
+}
+
+JSObject*
+DocumentL10n::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+static const char**
+CreateOutArray(const nsTArray<nsString>& aArray)
+{
+  uint32_t n = aArray.Length();
+  const char** result = static_cast<const char**>(moz_xmalloc(n * sizeof(char*)));
+  for (uint32_t i = 0; i < n; i++) {
+    result[i] = moz_xstrdup(ToNewUTF8String(aArray[i]));
+  }
+  return result;
+}
+
+uint32_t
+DocumentL10n::AddResourceIds(nsTArray<nsString>& aResourceIds)
+{
+  uint32_t ret;
+  mDOMLocalization->AddResourceIds(CreateOutArray(aResourceIds), aResourceIds.Length(), &ret);
+  return ret;
+}
+
+uint32_t
+DocumentL10n::RemoveResourceIds(nsTArray<nsString>& aResourceIds)
+{
+  uint32_t ret;
+  mDOMLocalization->RemoveResourceIds(CreateOutArray(aResourceIds), aResourceIds.Length(), &ret);
+  return ret;
+}
+
+already_AddRefed<Promise>
+DocumentL10n::FormatMessages(JSContext* cx, const Sequence<L10nKey>& aKeys, ErrorResult& aRv)
+{
+  nsTArray<JS::HandleValue> jsKeys;
+  jsKeys.SetCapacity(aKeys.Length());
+
+  for (auto& key : aKeys) {
+    JS::Rooted<JS::Value> jsKey(cx);
+    if (!ToJSValue(cx, key, &jsKey)) {
+      aRv.Throw(NS_ERROR_UNEXPECTED);
+      return nullptr;
+    }
+
+    jsKeys.AppendElement(jsKey);
+  }
+
+  RefPtr<Promise> promise;
+  nsresult rv = mDOMLocalization->FormatMessages(jsKeys.Elements(), jsKeys.Length(), getter_AddRefs(promise));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+
+  return promise.forget();
+}
+
+already_AddRefed<Promise>
+DocumentL10n::FormatValues(JSContext* cx, const Sequence<L10nKey>& aKeys, ErrorResult& aRv)
+{
+  JS::AutoValueVector vec(cx);
+  if (!vec.resize(aKeys.Length())) {
+    return nullptr;
+  }
+
+  for (auto& key : aKeys) {
+    JS::Rooted<JS::Value> jsKey(cx);
+    if (!ToJSValue(cx, key, &jsKey)) {
+      aRv.Throw(NS_ERROR_UNEXPECTED);
+      return nullptr;
+    }
+    vec.infallibleAppend(jsKey);
+  }
+
+  RefPtr<Promise> promise;
+  // XXX(bz,nika): This will not work currently as we're waiting for the
+  // fix from nika.
+  nsresult rv = mDOMLocalization->FormatValues((JS::HandleValue*)vec.begin(), vec.length(), getter_AddRefs(promise));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+
+  return promise.forget();
+}
+
+already_AddRefed<Promise>
+DocumentL10n::FormatValue(JSContext* cx, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv)
+{
+  JS::Rooted<JS::Value> args(cx);
+
+  if (aArgs.WasPassed()) {
+    args = JS::ObjectValue(*aArgs.Value());
+  } else {
+    args = JS::UndefinedValue();
+  }
+
+  RefPtr<Promise> promise;
+  nsresult rv = mDOMLocalization->FormatValue(aId, args, getter_AddRefs(promise));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+  return promise.forget();
+}
+
+void
+DocumentL10n::SetAttributes(JSContext* cx, Element& aElement, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv)
+{
+  aElement.SetAttribute(NS_LITERAL_STRING("data-l10n-id"), aId, aRv);
+  if (aRv.Failed()) {
+    return;
+  }
+  if (aArgs.WasPassed()) {
+    nsAutoString data;
+    //XXX(Mossop): We need to stringify the aArgs here and write them to `data-l10n-args`
+    JS::Rooted<JS::Value> val(cx, JS::ObjectValue(*aArgs.Value()));
+    nsresult rv = SerializeFromJSVal(cx, val, data);
+    if (NS_FAILED(rv)) {
+      aRv.Throw(rv);
+      return;
+    }
+    aElement.SetAttribute(NS_LITERAL_STRING("data-l10n-args"), data, aRv);
+  }
+}
+
+void
+DocumentL10n::GetAttributes(JSContext* cx, Element& aElement, L10nKey& aResult, ErrorResult& aRv)
+{
+  nsAutoString l10nId;
+  nsAutoString l10nArgs;
+
+  aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nid, l10nId);
+  aResult.mId = l10nId;
+
+  aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, l10nArgs);
+  if (!l10nArgs.IsEmpty()) {
+    JS::Rooted<JS::Value> json(cx);
+    if (!JS_ParseJSON(cx, l10nArgs.get(), l10nArgs.Length(), &json) ||
+        !json.isObject()) {
+      aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+      return;
+    }
+    aResult.mArgs = &json.toObject();
+  }
+}
+
+already_AddRefed<Promise>
+DocumentL10n::TranslateFragment(Element& aElement, ErrorResult& aRv)
+{
+  RefPtr<Promise> promise;
+  nsresult rv = mDOMLocalization->TranslateFragment(&aElement, getter_AddRefs(promise));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+  return promise.forget();
+}
+
+already_AddRefed<Promise>
+DocumentL10n::TranslateElements(const Sequence<OwningNonNull<Element>>& aElements, ErrorResult& aRv)
+{
+  nsTArray<Element*> elements;
+  elements.SetCapacity(aElements.Length()); 
+  for (auto& element : aElements) {
+    elements.AppendElement(element);
+  }
+  RefPtr<Promise> promise;
+  nsresult rv = mDOMLocalization->TranslateElements(
+      elements.Elements(), elements.Length(), getter_AddRefs(promise));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+  return promise.forget();
+
+}
+
+void
+DocumentL10n::OnDocumentParsed()
+{
+  if (mState == DocumentL10nState::DocumentParsed) {
+    return;
+  }
+
+  mDOMLocalization->ConnectRoot(mDocument->GetDocumentElement());
+
+  RefPtr<Promise> promise;
+  mDOMLocalization->TranslateRoots(getter_AddRefs(promise));
+  mState = DocumentL10nState::DocumentParsed;
+}
+
+already_AddRefed<Promise>
+DocumentL10n::GetReady(ErrorResult& aRv)
+{
+  RefPtr<Promise> promise;
+
+  nsresult rv = mDOMLocalization->GetReady(getter_AddRefs(promise));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+
+  return promise.forget();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/intl/l10n/DocumentL10n.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_DocumentL10n_h
+#define mozilla_dom_DocumentL10n_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsIDocument.h"
+#include "mozIDOMLocalization.h"
+
+namespace mozilla {
+namespace dom {
+
+class Element;
+class Promise;
+struct L10nKey;
+
+enum class DocumentL10nState {
+  Initialized = -1,
+  DocumentParsed
+};
+
+class DocumentL10n final : public nsISupports,
+                           public nsWrapperCache
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DocumentL10n)
+
+public:
+  explicit DocumentL10n(nsIDocument* aDocument);
+
+protected:
+  virtual ~DocumentL10n();
+
+  nsCOMPtr<nsIDocument> mDocument;
+  nsTArray<RefPtr<Promise>> mEarlyPromises;
+  DocumentL10nState mState;
+  nsCOMPtr<mozIDOMLocalization> mDOMLocalization;
+
+public:
+  nsIDocument* GetParentObject() const { return mDocument; };
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+  uint32_t AddResourceIds(nsTArray<nsString>& aResourceIds);
+  uint32_t RemoveResourceIds(nsTArray<nsString>& aResourceIds);
+
+  already_AddRefed<Promise> FormatMessages(JSContext* cx, const Sequence<L10nKey>& aKeys, ErrorResult& aRv);
+  already_AddRefed<Promise> FormatValues(JSContext* cx, const Sequence<L10nKey>& aKeys, ErrorResult& aRv);
+  already_AddRefed<Promise> FormatValue(JSContext* cx, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
+
+  void SetAttributes(JSContext* cx, Element& aElement, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
+  void GetAttributes(JSContext* cx, Element& aElement, L10nKey& aResult, ErrorResult& aRv);
+
+  already_AddRefed<Promise> TranslateFragment(Element& aElement, ErrorResult& aRv);
+  already_AddRefed<Promise> TranslateElements(const Sequence<OwningNonNull<Element>>& aElements, ErrorResult& aRv);
+
+  already_AddRefed<Promise> GetReady(ErrorResult& aRv);
+
+  void OnDocumentParsed();
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_DocumentL10n_h
--- a/intl/l10n/moz.build
+++ b/intl/l10n/moz.build
@@ -17,16 +17,28 @@ XPIDL_SOURCES += [
 
 XPIDL_MODULE = 'locale'
 
 EXTRA_COMPONENTS += [
     'mozDOMLocalization.js',
     'mozDOMLocalization.manifest',
 ]
 
+EXPORTS.mozilla.dom += [
+    'DocumentL10n.h',
+]
+
+UNIFIED_SOURCES += [
+    'DocumentL10n.cpp',
+]
+
+LOCAL_INCLUDES += [
+    '/dom/base',
+]
+
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 SPHINX_TREES['l10n'] = 'docs'