Backed out changeset b462e7b742d8 (bug 1908261) for causing multiple reftest failures...
[gecko.git] / dom / l10n / DocumentL10n.cpp
blob9cf1d96c392794ccc88e11c621ecd6a7d341283e
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "DocumentL10n.h"
8 #include "nsIContentSink.h"
9 #include "nsContentUtils.h"
10 #include "mozilla/dom/AutoEntryScript.h"
11 #include "mozilla/dom/Document.h"
12 #include "mozilla/dom/DocumentL10nBinding.h"
14 using namespace mozilla;
15 using namespace mozilla::intl;
16 using namespace mozilla::dom;
18 NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentL10n)
19 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentL10n, DOMLocalization)
20 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
21 NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady)
22 NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentSink)
23 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
24 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
25 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentL10n, DOMLocalization)
26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
27 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady)
28 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentSink)
29 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
31 NS_IMPL_ADDREF_INHERITED(DocumentL10n, DOMLocalization)
32 NS_IMPL_RELEASE_INHERITED(DocumentL10n, DOMLocalization)
34 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n)
35 NS_INTERFACE_MAP_END_INHERITING(DOMLocalization)
37 /* static */
38 RefPtr<DocumentL10n> DocumentL10n::Create(Document* aDocument, bool aSync) {
39 RefPtr<DocumentL10n> l10n = new DocumentL10n(aDocument, aSync);
41 IgnoredErrorResult rv;
42 l10n->mReady = Promise::Create(l10n->mGlobal, rv);
43 if (NS_WARN_IF(rv.Failed())) {
44 return nullptr;
47 return l10n.forget();
50 DocumentL10n::DocumentL10n(Document* aDocument, bool aSync)
51 : DOMLocalization(aDocument->GetScopeObject(), aSync),
52 mDocument(aDocument),
53 mState(DocumentL10nState::Constructed) {
54 mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink());
57 JSObject* DocumentL10n::WrapObject(JSContext* aCx,
58 JS::Handle<JSObject*> aGivenProto) {
59 return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto);
62 class L10nReadyHandler final : public PromiseNativeHandler {
63 public:
64 NS_DECL_CYCLE_COLLECTING_ISUPPORTS
65 NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler)
67 explicit L10nReadyHandler(Promise* aPromise, DocumentL10n* aDocumentL10n)
68 : mPromise(aPromise), mDocumentL10n(aDocumentL10n) {}
70 void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
71 ErrorResult& aRv) override {
72 mDocumentL10n->InitialTranslationCompleted(true);
73 mPromise->MaybeResolveWithUndefined();
76 void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
77 ErrorResult& aRv) override {
78 mDocumentL10n->InitialTranslationCompleted(false);
80 nsTArray<nsCString> errors{
81 "[dom/l10n] Could not complete initial document translation."_ns,
83 IgnoredErrorResult rv;
84 MaybeReportErrorsToGecko(errors, rv, mDocumentL10n->GetParentObject());
86 /**
87 * We resolve the mReady here even if we encountered failures, because
88 * there is nothing actionable for the user pending on `mReady` to do here
89 * and we don't want to incentivized consumers of this API to plan the
90 * same pending operation for resolve and reject scenario.
92 * Additionally, without it, the stderr received "uncaught promise
93 * rejection" warning, which is noisy and not-actionable.
95 * So instead, we just resolve and report errors.
97 * However, in automated tests we do reject so that errors with missing
98 * messages and resources can be caught.
100 if (xpc::IsInAutomation()) {
101 mPromise->MaybeRejectWithClone(aCx, aValue);
102 } else {
103 mPromise->MaybeResolveWithUndefined();
107 private:
108 ~L10nReadyHandler() = default;
110 RefPtr<Promise> mPromise;
111 RefPtr<DocumentL10n> mDocumentL10n;
114 NS_IMPL_CYCLE_COLLECTION(L10nReadyHandler, mPromise, mDocumentL10n)
116 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nReadyHandler)
117 NS_INTERFACE_MAP_ENTRY(nsISupports)
118 NS_INTERFACE_MAP_END
120 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nReadyHandler)
121 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nReadyHandler)
123 void DocumentL10n::TriggerInitialTranslation() {
124 MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
125 if (mState >= DocumentL10nState::InitialTranslationTriggered) {
126 return;
128 if (!mReady) {
129 // If we don't have `mReady` it means that we are in shutdown mode.
130 // See bug 1687118 for details.
131 InitialTranslationCompleted(false);
132 return;
135 AutoAllowLegacyScriptExecution exemption;
137 nsTArray<RefPtr<Promise>> promises;
139 ErrorResult rv;
140 promises.AppendElement(TranslateDocument(rv));
141 if (NS_WARN_IF(rv.Failed())) {
142 rv.SuppressException();
143 InitialTranslationCompleted(false);
144 mReady->MaybeRejectWithUndefined();
145 return;
147 promises.AppendElement(TranslateRoots(rv));
148 Element* documentElement = mDocument->GetDocumentElement();
149 if (!documentElement) {
150 InitialTranslationCompleted(false);
151 mReady->MaybeRejectWithUndefined();
152 return;
155 DOMLocalization::ConnectRoot(*documentElement);
157 AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslation");
158 RefPtr<Promise> promise = Promise::All(aes.cx(), promises, rv);
160 if (promise->State() == Promise::PromiseState::Resolved) {
161 // If the promise is already resolved, we can fast-track
162 // to initial translation completed.
163 InitialTranslationCompleted(true);
164 mReady->MaybeResolveWithUndefined();
165 } else {
166 RefPtr<PromiseNativeHandler> l10nReadyHandler =
167 new L10nReadyHandler(mReady, this);
168 promise->AppendNativeHandler(l10nReadyHandler);
170 mState = DocumentL10nState::InitialTranslationTriggered;
174 already_AddRefed<Promise> DocumentL10n::TranslateDocument(ErrorResult& aRv) {
175 MOZ_ASSERT(mState == DocumentL10nState::Constructed,
176 "This method should be called only from Constructed state.");
177 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
178 if (NS_WARN_IF(aRv.Failed())) {
179 return nullptr;
182 Element* elem = mDocument->GetDocumentElement();
183 if (!elem) {
184 promise->MaybeRejectWithUndefined();
185 return promise.forget();
188 // 1. Collect all localizable elements.
189 Sequence<OwningNonNull<Element>> elements;
190 GetTranslatables(*elem, elements, aRv);
191 if (NS_WARN_IF(aRv.Failed())) {
192 promise->MaybeRejectWithUndefined();
193 return promise.forget();
196 RefPtr<nsXULPrototypeDocument> proto = mDocument->GetPrototype();
198 // 2. Check if the document has a prototype that may cache
199 // translated elements.
200 if (proto) {
201 // 2.1. Handle the case when we have proto.
203 // 2.1.1. Move elements that are not in the proto to a separate
204 // array.
205 Sequence<OwningNonNull<Element>> nonProtoElements;
207 uint32_t i = elements.Length();
208 while (i > 0) {
209 Element* elem = elements.ElementAt(i - 1);
210 MOZ_RELEASE_ASSERT(elem->HasAttr(nsGkAtoms::datal10nid));
211 if (!elem->HasElementCreatedFromPrototypeAndHasUnmodifiedL10n()) {
212 if (NS_WARN_IF(!nonProtoElements.AppendElement(*elem, fallible))) {
213 promise->MaybeRejectWithUndefined();
214 return promise.forget();
216 elements.RemoveElement(elem);
218 i--;
221 // We populate the sequence in reverse order. Let's bring it
222 // back to top->bottom one.
223 nonProtoElements.Reverse();
225 AutoAllowLegacyScriptExecution exemption;
227 nsTArray<RefPtr<Promise>> promises;
229 // 2.1.2. If we're not loading from cache, push the elements that
230 // are in the prototype to be translated and cached.
231 if (!proto->WasL10nCached() && !elements.IsEmpty()) {
232 RefPtr<Promise> translatePromise =
233 TranslateElements(elements, proto, aRv);
234 if (NS_WARN_IF(!translatePromise || aRv.Failed())) {
235 promise->MaybeRejectWithUndefined();
236 return promise.forget();
238 promises.AppendElement(translatePromise);
241 // 2.1.3. If there are elements that are not in the prototype,
242 // localize them without attempting to cache and
243 // independently of if we're loading from cache.
244 if (!nonProtoElements.IsEmpty()) {
245 RefPtr<Promise> nonProtoTranslatePromise =
246 TranslateElements(nonProtoElements, nullptr, aRv);
247 if (NS_WARN_IF(!nonProtoTranslatePromise || aRv.Failed())) {
248 promise->MaybeRejectWithUndefined();
249 return promise.forget();
251 promises.AppendElement(nonProtoTranslatePromise);
254 // 2.1.4. Collect promises with Promise::All (maybe empty).
255 AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslationCompleted");
256 promise = Promise::All(aes.cx(), promises, aRv);
257 if (NS_WARN_IF(aRv.Failed())) {
258 return nullptr;
260 } else {
261 // 2.2. Handle the case when we don't have proto.
263 // 2.2.1. Otherwise, translate all available elements,
264 // without attempting to cache them.
265 promise = TranslateElements(elements, nullptr, aRv);
266 if (NS_WARN_IF(aRv.Failed())) {
267 return nullptr;
271 return promise.forget();
274 void DocumentL10n::InitialTranslationCompleted(bool aL10nCached) {
275 if (mState >= DocumentL10nState::Ready) {
276 return;
279 Element* documentElement = mDocument->GetDocumentElement();
280 if (documentElement) {
281 SetRootInfo(documentElement);
284 mState = DocumentL10nState::Ready;
286 RefPtr<Document> doc = mDocument;
287 doc->InitialTranslationCompleted(aL10nCached);
289 // In XUL scenario contentSink is nullptr.
290 if (mContentSink) {
291 nsCOMPtr<nsIContentSink> sink = mContentSink.forget();
292 sink->InitialTranslationCompleted();
295 // From now on, the state of Localization is unconditionally
296 // async.
297 SetAsync();
300 void DocumentL10n::ConnectRoot(nsINode& aNode, bool aTranslate,
301 ErrorResult& aRv) {
302 if (aTranslate) {
303 if (mState >= DocumentL10nState::InitialTranslationTriggered) {
304 RefPtr<Promise> promise = TranslateFragment(aNode, aRv);
307 DOMLocalization::ConnectRoot(aNode);
310 Promise* DocumentL10n::Ready() { return mReady; }
312 void DocumentL10n::OnCreatePresShell() { mMutations->OnCreatePresShell(); }