L10nMutations.cpp (10596B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 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/. */ 6 7 #include "L10nMutations.h" 8 9 #include "DOMLocalization.h" 10 #include "mozilla/dom/DocumentInlines.h" 11 #include "mozilla/intl/Localization.h" 12 #include "nsRefreshDriver.h" 13 #include "nsThreadManager.h" 14 15 using namespace mozilla; 16 using namespace mozilla::intl; 17 using namespace mozilla::dom; 18 19 NS_IMPL_CYCLE_COLLECTION_CLASS(L10nMutations) 20 21 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(L10nMutations) 22 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElements) 23 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElementsHash) 24 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 25 26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(L10nMutations) 27 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElements) 28 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElementsHash) 29 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 30 31 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutations) 32 NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) 33 NS_INTERFACE_MAP_ENTRY(nsISupports) 34 NS_INTERFACE_MAP_END 35 36 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutations) 37 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutations) 38 39 L10nMutations::L10nMutations(DOMLocalization* aDOMLocalization) 40 : mDOMLocalization(aDOMLocalization) { 41 mObserving = true; 42 } 43 44 L10nMutations::~L10nMutations() { 45 StopRefreshObserver(); 46 MOZ_ASSERT(!mDOMLocalization, 47 "DOMLocalization<-->L10nMutations cycle should be broken."); 48 } 49 50 void L10nMutations::AttributeChanged(Element* aElement, int32_t aNameSpaceID, 51 nsAtom* aAttribute, AttrModType, 52 const nsAttrValue* aOldValue) { 53 if (!mObserving) { 54 return; 55 } 56 57 if (aNameSpaceID == kNameSpaceID_None && 58 (aAttribute == nsGkAtoms::datal10nid || 59 aAttribute == nsGkAtoms::datal10nargs)) { 60 if (IsInRoots(aElement)) { 61 L10nElementChanged(aElement); 62 } 63 } 64 } 65 66 void L10nMutations::ContentAppended(nsIContent* aChild, 67 const ContentAppendInfo&) { 68 if (!mObserving) { 69 return; 70 } 71 72 if (!IsInRoots(aChild)) { 73 return; 74 } 75 76 Sequence<OwningNonNull<Element>> elements; 77 for (nsIContent* node = aChild; node; node = node->GetNextSibling()) { 78 if (node->IsElement()) { 79 DOMLocalization::GetTranslatables(*node, elements, IgnoreErrors()); 80 } 81 } 82 83 for (auto& elem : elements) { 84 L10nElementChanged(elem); 85 } 86 } 87 88 void L10nMutations::ContentInserted(nsIContent* aChild, 89 const ContentInsertInfo&) { 90 if (!mObserving) { 91 return; 92 } 93 94 if (!aChild->IsElement()) { 95 return; 96 } 97 Element* elem = aChild->AsElement(); 98 99 if (!IsInRoots(elem)) { 100 return; 101 } 102 103 Sequence<OwningNonNull<Element>> elements; 104 DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors()); 105 106 for (auto& elem : elements) { 107 L10nElementChanged(elem); 108 } 109 } 110 111 void L10nMutations::ContentWillBeRemoved(nsIContent* aChild, 112 const ContentRemoveInfo&) { 113 if (!mObserving || mPendingElements.IsEmpty()) { 114 return; 115 } 116 117 Element* elem = Element::FromNode(*aChild); 118 if (!elem || !IsInRoots(elem)) { 119 return; 120 } 121 122 Sequence<OwningNonNull<Element>> elements; 123 DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors()); 124 125 for (auto& elem : elements) { 126 if (mPendingElementsHash.EnsureRemoved(elem)) { 127 mPendingElements.RemoveElement(elem); 128 } 129 } 130 131 if (!HasPendingMutations()) { 132 nsContentUtils::AddScriptRunner(NewRunnableMethod( 133 "MaybeFirePendingTranslationsFinished", this, 134 &L10nMutations::MaybeFirePendingTranslationsFinished)); 135 } 136 } 137 138 void L10nMutations::L10nElementChanged(Element* aElement) { 139 const bool wasEmpty = mPendingElements.IsEmpty(); 140 141 if (mPendingElementsHash.EnsureInserted(aElement)) { 142 mPendingElements.AppendElement(aElement); 143 } 144 145 if (!wasEmpty) { 146 return; 147 } 148 149 if (!mRefreshDriver) { 150 StartRefreshObserver(); 151 } 152 153 if (!mBlockingLoad) { 154 Document* doc = GetDocument(); 155 if (doc && doc->GetReadyStateEnum() != Document::READYSTATE_COMPLETE) { 156 doc->BlockOnload(); 157 mBlockingLoad = true; 158 } 159 } 160 161 if (mBlockingLoad && !mPendingBlockingLoadFlush) { 162 // We want to make sure we flush translations and don't block the load 163 // indefinitely (and, in fact, that we do it rather soon, even if the 164 // refresh driver is not ticking yet). 165 // 166 // In some platforms (mainly Wayland) the load of the main document 167 // causes vsync to start running and start ticking the refresh driver, 168 // so we can't rely on the refresh driver ticking yet. 169 RefPtr<nsIRunnable> task = 170 NewRunnableMethod("FlushPendingTranslationsBeforeLoad", this, 171 &L10nMutations::FlushPendingTranslationsBeforeLoad); 172 nsThreadManager::get().DispatchDirectTaskToCurrentThread(task); 173 mPendingBlockingLoadFlush = true; 174 } 175 } 176 177 void L10nMutations::PauseObserving() { mObserving = false; } 178 179 void L10nMutations::ResumeObserving() { mObserving = true; } 180 181 void L10nMutations::WillRefresh(mozilla::TimeStamp aTime) { 182 StopRefreshObserver(); 183 FlushPendingTranslations(); 184 } 185 186 /** 187 * The handler for the `TranslateElements` promise used to turn 188 * a potential rejection into a console warning. 189 **/ 190 class L10nMutationFinalizationHandler final : public PromiseNativeHandler { 191 public: 192 NS_DECL_CYCLE_COLLECTING_ISUPPORTS 193 NS_DECL_CYCLE_COLLECTION_CLASS(L10nMutationFinalizationHandler) 194 195 explicit L10nMutationFinalizationHandler(L10nMutations* aMutations, 196 nsIGlobalObject* aGlobal) 197 : mMutations(aMutations), mGlobal(aGlobal) {} 198 199 MOZ_CAN_RUN_SCRIPT void Settled() { 200 if (RefPtr mutations = mMutations) { 201 mutations->PendingPromiseSettled(); 202 } 203 } 204 205 MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx, 206 JS::Handle<JS::Value> aValue, 207 ErrorResult& aRv) override { 208 Settled(); 209 } 210 211 MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx, 212 JS::Handle<JS::Value> aValue, 213 ErrorResult& aRv) override { 214 nsTArray<nsCString> errors{ 215 "[dom/l10n] Errors during l10n mutation frame."_ns, 216 }; 217 MaybeReportErrorsToGecko(errors, IgnoreErrors(), mGlobal); 218 Settled(); 219 } 220 221 private: 222 ~L10nMutationFinalizationHandler() = default; 223 224 RefPtr<L10nMutations> mMutations; 225 nsCOMPtr<nsIGlobalObject> mGlobal; 226 }; 227 228 NS_IMPL_CYCLE_COLLECTION(L10nMutationFinalizationHandler, mGlobal, mMutations) 229 230 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutationFinalizationHandler) 231 NS_INTERFACE_MAP_ENTRY(nsISupports) 232 NS_INTERFACE_MAP_END 233 234 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutationFinalizationHandler) 235 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutationFinalizationHandler) 236 237 void L10nMutations::FlushPendingTranslationsBeforeLoad() { 238 MOZ_ASSERT(mPendingBlockingLoadFlush); 239 mPendingBlockingLoadFlush = false; 240 FlushPendingTranslations(); 241 } 242 243 void L10nMutations::FlushPendingTranslations() { 244 if (!mDOMLocalization) { 245 return; 246 } 247 248 nsTArray<OwningNonNull<Element>> elements; 249 for (auto& elem : mPendingElements) { 250 if (elem->HasAttr(nsGkAtoms::datal10nid)) { 251 elements.AppendElement(*elem); 252 } 253 } 254 255 mPendingElementsHash.Clear(); 256 mPendingElements.Clear(); 257 258 RefPtr<Promise> promise = 259 mDOMLocalization->TranslateElements(elements, IgnoreErrors()); 260 if (promise && promise->State() == Promise::PromiseState::Pending) { 261 mPendingPromises++; 262 auto l10nMutationFinalizationHandler = 263 MakeRefPtr<L10nMutationFinalizationHandler>( 264 this, mDOMLocalization->GetParentObject()); 265 promise->AppendNativeHandler(l10nMutationFinalizationHandler); 266 } 267 268 MaybeFirePendingTranslationsFinished(); 269 } 270 271 void L10nMutations::PendingPromiseSettled() { 272 MOZ_DIAGNOSTIC_ASSERT(mPendingPromises); 273 mPendingPromises--; 274 MaybeFirePendingTranslationsFinished(); 275 } 276 277 void L10nMutations::MaybeFirePendingTranslationsFinished() { 278 if (HasPendingMutations()) { 279 return; 280 } 281 282 RefPtr doc = GetDocument(); 283 if (NS_WARN_IF(!doc)) { 284 return; 285 } 286 287 if (mBlockingLoad) { 288 mBlockingLoad = false; 289 doc->UnblockOnload(false); 290 } 291 nsContentUtils::DispatchEventOnlyToChrome( 292 doc, doc, u"L10nMutationsFinished"_ns, CanBubble::eNo, Cancelable::eNo, 293 Composed::eNo, nullptr); 294 } 295 296 void L10nMutations::Disconnect() { 297 StopRefreshObserver(); 298 mDOMLocalization = nullptr; 299 } 300 301 Document* L10nMutations::GetDocument() const { 302 if (!mDOMLocalization) { 303 return nullptr; 304 } 305 auto* innerWindow = mDOMLocalization->GetParentObject()->GetAsInnerWindow(); 306 if (!innerWindow) { 307 return nullptr; 308 } 309 return innerWindow->GetExtantDoc(); 310 } 311 312 void L10nMutations::StartRefreshObserver() { 313 if (!mDOMLocalization || mRefreshDriver) { 314 return; 315 } 316 if (Document* doc = GetDocument()) { 317 if (nsPresContext* ctx = doc->GetPresContext()) { 318 mRefreshDriver = ctx->RefreshDriver(); 319 } 320 } 321 322 // If we can't start the refresh driver, it means 323 // that the presContext is not available yet. 324 // In that case, we'll trigger the flush of pending 325 // elements in Document::CreatePresShell. 326 if (mRefreshDriver) { 327 mRefreshDriver->AddRefreshObserver(this, FlushType::Style, 328 "L10n mutations"); 329 } else { 330 NS_WARNING("[l10n][mutations] Failed to start a refresh observer."); 331 } 332 } 333 334 void L10nMutations::StopRefreshObserver() { 335 if (mRefreshDriver) { 336 mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style); 337 mRefreshDriver = nullptr; 338 } 339 } 340 341 void L10nMutations::OnCreatePresShell() { 342 StopRefreshObserver(); 343 if (!mPendingElements.IsEmpty()) { 344 StartRefreshObserver(); 345 } 346 } 347 348 bool L10nMutations::IsInRoots(nsINode* aNode) { 349 // If the root of the mutated element is in the light DOM, 350 // we know it must be covered by our observer directly. 351 // 352 // Otherwise, we need to check if its subtree root is the same 353 // as any of the `DOMLocalization::mRoots` subtree roots. 354 nsINode* root = aNode->SubtreeRoot(); 355 356 // If element is in light DOM, it must be covered by one of 357 // the DOMLocalization roots to end up here. 358 MOZ_ASSERT_IF(!root->IsShadowRoot(), 359 mDOMLocalization->SubtreeRootInRoots(root)); 360 361 return !root->IsShadowRoot() || mDOMLocalization->SubtreeRootInRoots(root); 362 }