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 #include "mozilla/dom/DocumentInlines.h"
9 #include "nsRefreshDriver.h"
10 
11 using namespace mozilla::dom;
12 
13 NS_IMPL_CYCLE_COLLECTION_CLASS(L10nMutations)
14 
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(L10nMutations)15 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(L10nMutations)
16   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElements)
17   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElementsHash)
18 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
19 
20 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(L10nMutations)
21   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElements)
22   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElementsHash)
23 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
24 
25 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutations)
26   NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
27   NS_INTERFACE_MAP_ENTRY(nsISupports)
28 NS_INTERFACE_MAP_END
29 
30 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutations)
31 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutations)
32 
33 L10nMutations::L10nMutations(DOMLocalization* aDOMLocalization)
34     : mDOMLocalization(aDOMLocalization) {
35   mObserving = true;
36 }
37 
~L10nMutations()38 L10nMutations::~L10nMutations() {
39   StopRefreshObserver();
40   MOZ_ASSERT(!mDOMLocalization,
41              "DOMLocalization<-->L10nMutations cycle should be broken.");
42 }
43 
AttributeChanged(Element * aElement,int32_t aNameSpaceID,nsAtom * aAttribute,int32_t aModType,const nsAttrValue * aOldValue)44 void L10nMutations::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
45                                      nsAtom* aAttribute, int32_t aModType,
46                                      const nsAttrValue* aOldValue) {
47   if (!mObserving) {
48     return;
49   }
50 
51   if (aNameSpaceID == kNameSpaceID_None &&
52       (aAttribute == nsGkAtoms::datal10nid ||
53        aAttribute == nsGkAtoms::datal10nargs)) {
54     if (IsInRoots(aElement)) {
55       L10nElementChanged(aElement);
56     }
57   }
58 }
59 
ContentAppended(nsIContent * aChild)60 void L10nMutations::ContentAppended(nsIContent* aChild) {
61   if (!mObserving) {
62     return;
63   }
64 
65   nsINode* node = aChild;
66   if (!IsInRoots(node)) {
67     return;
68   }
69 
70   ErrorResult rv;
71   Sequence<OwningNonNull<Element>> elements;
72   while (node) {
73     if (node->IsElement()) {
74       DOMLocalization::GetTranslatables(*node, elements, rv);
75     }
76 
77     node = node->GetNextSibling();
78   }
79 
80   for (auto& elem : elements) {
81     L10nElementChanged(elem);
82   }
83 }
84 
ContentInserted(nsIContent * aChild)85 void L10nMutations::ContentInserted(nsIContent* aChild) {
86   if (!mObserving) {
87     return;
88   }
89   ErrorResult rv;
90   Sequence<OwningNonNull<Element>> elements;
91 
92   if (!aChild->IsElement()) {
93     return;
94   }
95   Element* elem = aChild->AsElement();
96 
97   if (!IsInRoots(elem)) {
98     return;
99   }
100   DOMLocalization::GetTranslatables(*aChild, elements, rv);
101 
102   for (auto& elem : elements) {
103     L10nElementChanged(elem);
104   }
105 }
106 
L10nElementChanged(Element * aElement)107 void L10nMutations::L10nElementChanged(Element* aElement) {
108   if (!mPendingElementsHash.Contains(aElement)) {
109     mPendingElements.AppendElement(aElement);
110     mPendingElementsHash.Insert(aElement);
111   }
112 
113   if (!mRefreshObserver) {
114     StartRefreshObserver();
115   }
116 }
117 
PauseObserving()118 void L10nMutations::PauseObserving() { mObserving = false; }
119 
ResumeObserving()120 void L10nMutations::ResumeObserving() { mObserving = true; }
121 
WillRefresh(mozilla::TimeStamp aTime)122 void L10nMutations::WillRefresh(mozilla::TimeStamp aTime) {
123   StopRefreshObserver();
124   FlushPendingTranslations();
125 }
126 
FlushPendingTranslations()127 void L10nMutations::FlushPendingTranslations() {
128   if (!mDOMLocalization) {
129     return;
130   }
131 
132   ErrorResult rv;
133 
134   Sequence<OwningNonNull<Element>> elements;
135 
136   for (auto& elem : mPendingElements) {
137     if (!elem->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nid)) {
138       continue;
139     }
140 
141     if (!elements.AppendElement(*elem, fallible)) {
142       mozalloc_handle_oom(0);
143     }
144   }
145 
146   mPendingElementsHash.Clear();
147   mPendingElements.Clear();
148 
149   RefPtr<Promise> promise = mDOMLocalization->TranslateElements(elements, rv);
150 }
151 
Disconnect()152 void L10nMutations::Disconnect() {
153   StopRefreshObserver();
154   mDOMLocalization = nullptr;
155 }
156 
StartRefreshObserver()157 void L10nMutations::StartRefreshObserver() {
158   if (!mDOMLocalization || mRefreshObserver) {
159     return;
160   }
161 
162   if (!mRefreshDriver) {
163     nsPIDOMWindowInner* innerWindow =
164         mDOMLocalization->GetParentObject()->AsInnerWindow();
165     Document* doc = innerWindow ? innerWindow->GetExtantDoc() : nullptr;
166     if (doc) {
167       nsPresContext* ctx = doc->GetPresContext();
168       if (ctx) {
169         mRefreshDriver = ctx->RefreshDriver();
170       }
171     }
172   }
173 
174   // If we can't start the refresh driver, it means
175   // that the presContext is not available yet.
176   // In that case, we'll trigger the flush of pending
177   // elements in Document::CreatePresShell.
178   if (mRefreshDriver) {
179     mRefreshDriver->AddRefreshObserver(this, FlushType::Style,
180                                        "L10n mutations");
181     mRefreshObserver = true;
182   } else {
183     NS_WARNING("[l10n][mutations] Failed to start a refresh observer.");
184   }
185 }
186 
StopRefreshObserver()187 void L10nMutations::StopRefreshObserver() {
188   if (!mDOMLocalization) {
189     return;
190   }
191 
192   if (mRefreshDriver) {
193     mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
194     mRefreshObserver = false;
195   }
196 }
197 
OnCreatePresShell()198 void L10nMutations::OnCreatePresShell() {
199   if (!mPendingElements.IsEmpty()) {
200     StartRefreshObserver();
201   }
202 }
203 
IsInRoots(nsINode * aNode)204 bool L10nMutations::IsInRoots(nsINode* aNode) {
205   // If the root of the mutated element is in the light DOM,
206   // we know it must be covered by our observer directly.
207   //
208   // Otherwise, we need to check if its subtree root is the same
209   // as any of the `DOMLocalization::mRoots` subtree roots.
210   nsINode* root = aNode->SubtreeRoot();
211 
212   // If element is in light DOM, it must be covered by one of
213   // the DOMLocalization roots to end up here.
214   MOZ_ASSERT_IF(!root->IsShadowRoot(),
215                 mDOMLocalization->SubtreeRootInRoots(root));
216 
217   return !root->IsShadowRoot() || mDOMLocalization->SubtreeRootInRoots(root);
218 }
219