Bug 1700051: part 26) Correct typo in comment of `mozInlineSpellWordUtil::BuildSoftTe...
[gecko.git] / dom / l10n / L10nOverlays.cpp
blob89fc8fa1d6009e1553a65b5b4b5e93919e942695
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/. */
7 #include "L10nOverlays.h"
8 #include "mozilla/dom/HTMLTemplateElement.h"
9 #include "mozilla/dom/HTMLInputElement.h"
10 #include "HTMLSplitOnSpacesTokenizer.h"
11 #include "nsHtml5StringParser.h"
12 #include "nsTextNode.h"
14 using namespace mozilla::dom;
15 using namespace mozilla;
17 bool L10nOverlays::IsAttrNameLocalizable(
18 const nsAtom* nameAtom, Element* aElement,
19 nsTArray<nsString>* aExplicitlyAllowed) {
20 nsAutoString name;
21 nameAtom->ToString(name);
23 if (aExplicitlyAllowed->Contains(name)) {
24 return true;
27 nsAtom* elemName = aElement->NodeInfo()->NameAtom();
29 uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
31 if (nameSpace == kNameSpaceID_XHTML) {
32 // Is it a globally safe attribute?
33 if (nameAtom == nsGkAtoms::title || nameAtom == nsGkAtoms::aria_label ||
34 nameAtom == nsGkAtoms::aria_valuetext) {
35 return true;
38 // Is it allowed on this element?
39 if (elemName == nsGkAtoms::a) {
40 return nameAtom == nsGkAtoms::download;
42 if (elemName == nsGkAtoms::area) {
43 return nameAtom == nsGkAtoms::download || nameAtom == nsGkAtoms::alt;
45 if (elemName == nsGkAtoms::input) {
46 // Special case for value on HTML inputs with type button, reset, submit
47 if (nameAtom == nsGkAtoms::value) {
48 HTMLInputElement* input = HTMLInputElement::FromNode(aElement);
49 if (input) {
50 uint32_t type = input->ControlType();
51 if (type == NS_FORM_INPUT_SUBMIT || type == NS_FORM_INPUT_BUTTON ||
52 type == NS_FORM_INPUT_RESET) {
53 return true;
57 return nameAtom == nsGkAtoms::alt || nameAtom == nsGkAtoms::placeholder;
59 if (elemName == nsGkAtoms::menuitem) {
60 return nameAtom == nsGkAtoms::label;
62 if (elemName == nsGkAtoms::menu) {
63 return nameAtom == nsGkAtoms::label;
65 if (elemName == nsGkAtoms::optgroup) {
66 return nameAtom == nsGkAtoms::label;
68 if (elemName == nsGkAtoms::option) {
69 return nameAtom == nsGkAtoms::label;
71 if (elemName == nsGkAtoms::track) {
72 return nameAtom == nsGkAtoms::label;
74 if (elemName == nsGkAtoms::img) {
75 return nameAtom == nsGkAtoms::alt;
77 if (elemName == nsGkAtoms::textarea) {
78 return nameAtom == nsGkAtoms::placeholder;
80 if (elemName == nsGkAtoms::th) {
81 return nameAtom == nsGkAtoms::abbr;
84 } else if (nameSpace == kNameSpaceID_XUL) {
85 // Is it a globally safe attribute?
86 if (nameAtom == nsGkAtoms::accesskey || nameAtom == nsGkAtoms::aria_label ||
87 nameAtom == nsGkAtoms::aria_valuetext || nameAtom == nsGkAtoms::label ||
88 nameAtom == nsGkAtoms::title || nameAtom == nsGkAtoms::tooltiptext) {
89 return true;
92 // Is it allowed on this element?
93 if (elemName == nsGkAtoms::description) {
94 return nameAtom == nsGkAtoms::value;
96 if (elemName == nsGkAtoms::key) {
97 return nameAtom == nsGkAtoms::key || nameAtom == nsGkAtoms::keycode;
99 if (elemName == nsGkAtoms::label) {
100 return nameAtom == nsGkAtoms::value;
104 return false;
107 already_AddRefed<nsINode> L10nOverlays::CreateTextNodeFromTextContent(
108 Element* aElement, ErrorResult& aRv) {
109 nsAutoString content;
110 aElement->GetTextContent(content, aRv);
112 if (NS_WARN_IF(aRv.Failed())) {
113 return nullptr;
116 return aElement->OwnerDoc()->CreateTextNode(content);
119 class AttributeNameValueComparator {
120 public:
121 bool Equals(const AttributeNameValue& aAttribute,
122 const nsAttrName* aAttrName) const {
123 return aAttrName->Equals(NS_ConvertUTF8toUTF16(aAttribute.mName));
127 void L10nOverlays::OverlayAttributes(
128 const Nullable<Sequence<AttributeNameValue>>& aTranslation,
129 Element* aToElement, ErrorResult& aRv) {
130 nsTArray<nsString> explicitlyAllowed;
132 nsAutoString l10nAttrs;
133 aToElement->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nattrs, l10nAttrs);
135 HTMLSplitOnSpacesTokenizer tokenizer(l10nAttrs, ',');
136 while (tokenizer.hasMoreTokens()) {
137 const nsAString& token = tokenizer.nextToken();
138 if (!token.IsEmpty() && !explicitlyAllowed.Contains(token)) {
139 explicitlyAllowed.AppendElement(token);
143 uint32_t i = aToElement->GetAttrCount();
144 while (i > 0) {
145 const nsAttrName* attrName = aToElement->GetAttrNameAt(i - 1);
147 if (IsAttrNameLocalizable(attrName->LocalName(), aToElement,
148 &explicitlyAllowed) &&
149 (aTranslation.IsNull() ||
150 !aTranslation.Value().Contains(attrName,
151 AttributeNameValueComparator()))) {
152 nsAutoString name;
153 attrName->LocalName()->ToString(name);
154 aToElement->RemoveAttribute(name, aRv);
155 if (NS_WARN_IF(aRv.Failed())) {
156 return;
159 i--;
162 if (aTranslation.IsNull()) {
163 return;
166 for (auto& attribute : aTranslation.Value()) {
167 RefPtr<nsAtom> nameAtom = NS_Atomize(attribute.mName);
168 if (IsAttrNameLocalizable(nameAtom, aToElement, &explicitlyAllowed)) {
169 NS_ConvertUTF8toUTF16 value(attribute.mValue);
170 if (!aToElement->AttrValueIs(kNameSpaceID_None, nameAtom, value,
171 eCaseMatters)) {
172 aToElement->SetAttr(nameAtom, value, aRv);
173 if (NS_WARN_IF(aRv.Failed())) {
174 return;
181 void L10nOverlays::OverlayAttributes(Element* aFromElement, Element* aToElement,
182 ErrorResult& aRv) {
183 Nullable<Sequence<AttributeNameValue>> attributes;
184 uint32_t attrCount = aFromElement->GetAttrCount();
186 if (attrCount == 0) {
187 attributes.SetNull();
188 } else {
189 Sequence<AttributeNameValue> sequence;
191 uint32_t i = 0;
192 while (BorrowedAttrInfo info = aFromElement->GetAttrInfoAt(i++)) {
193 AttributeNameValue* attr = sequence.AppendElement(fallible);
194 MOZ_ASSERT(info.mName->NamespaceEquals(kNameSpaceID_None),
195 "No namespaced attributes allowed.");
196 info.mName->LocalName()->ToUTF8String(attr->mName);
198 nsAutoString value;
199 info.mValue->ToString(value);
200 attr->mValue.Assign(NS_ConvertUTF16toUTF8(value));
203 attributes.SetValue(sequence);
206 return OverlayAttributes(attributes, aToElement, aRv);
209 void L10nOverlays::ShallowPopulateUsing(Element* aFromElement,
210 Element* aToElement, ErrorResult& aRv) {
211 nsAutoString content;
212 aFromElement->GetTextContent(content, aRv);
213 if (NS_WARN_IF(aRv.Failed())) {
214 return;
217 aToElement->SetTextContent(content, aRv);
218 if (NS_WARN_IF(aRv.Failed())) {
219 return;
222 OverlayAttributes(aFromElement, aToElement, aRv);
223 if (NS_WARN_IF(aRv.Failed())) {
224 return;
228 already_AddRefed<nsINode> L10nOverlays::GetNodeForNamedElement(
229 Element* aSourceElement, Element* aTranslatedChild,
230 nsTArray<L10nOverlaysError>& aErrors, ErrorResult& aRv) {
231 nsAutoString childName;
232 aTranslatedChild->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nname,
233 childName);
234 RefPtr<Element> sourceChild = nullptr;
236 nsINodeList* childNodes = aSourceElement->ChildNodes();
237 for (uint32_t i = 0; i < childNodes->Length(); i++) {
238 nsINode* childNode = childNodes->Item(i);
240 if (!childNode->IsElement()) {
241 continue;
243 Element* childElement = childNode->AsElement();
245 if (childElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nname,
246 childName, eCaseMatters)) {
247 sourceChild = childElement;
248 break;
252 if (!sourceChild) {
253 L10nOverlaysError error;
254 error.mCode.Construct(L10nOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING);
255 error.mL10nName.Construct(childName);
256 aErrors.AppendElement(error);
257 return CreateTextNodeFromTextContent(aTranslatedChild, aRv);
260 nsAtom* sourceChildName = sourceChild->NodeInfo()->NameAtom();
261 nsAtom* translatedChildName = aTranslatedChild->NodeInfo()->NameAtom();
262 if (sourceChildName != translatedChildName &&
263 // Create a specific exception for img vs. image mismatches,
264 // see bug 1543493
265 !(translatedChildName == nsGkAtoms::img &&
266 sourceChildName == nsGkAtoms::image)) {
267 L10nOverlaysError error;
268 error.mCode.Construct(
269 L10nOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH);
270 error.mL10nName.Construct(childName);
271 error.mTranslatedElementName.Construct(
272 aTranslatedChild->NodeInfo()->LocalName());
273 error.mSourceElementName.Construct(sourceChild->NodeInfo()->LocalName());
274 aErrors.AppendElement(error);
275 return CreateTextNodeFromTextContent(aTranslatedChild, aRv);
278 aSourceElement->RemoveChild(*sourceChild, aRv);
279 if (NS_WARN_IF(aRv.Failed())) {
280 return nullptr;
282 RefPtr<nsINode> clone = sourceChild->CloneNode(false, aRv);
283 if (NS_WARN_IF(aRv.Failed())) {
284 return nullptr;
286 ShallowPopulateUsing(aTranslatedChild, clone->AsElement(), aRv);
287 if (NS_WARN_IF(aRv.Failed())) {
288 return nullptr;
290 return clone.forget();
293 bool L10nOverlays::IsElementAllowed(Element* aElement) {
294 uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
295 if (nameSpace != kNameSpaceID_XHTML) {
296 return false;
299 nsAtom* nameAtom = aElement->NodeInfo()->NameAtom();
301 return nameAtom == nsGkAtoms::em || nameAtom == nsGkAtoms::strong ||
302 nameAtom == nsGkAtoms::small || nameAtom == nsGkAtoms::s ||
303 nameAtom == nsGkAtoms::cite || nameAtom == nsGkAtoms::q ||
304 nameAtom == nsGkAtoms::dfn || nameAtom == nsGkAtoms::abbr ||
305 nameAtom == nsGkAtoms::data || nameAtom == nsGkAtoms::time ||
306 nameAtom == nsGkAtoms::code || nameAtom == nsGkAtoms::var ||
307 nameAtom == nsGkAtoms::samp || nameAtom == nsGkAtoms::kbd ||
308 nameAtom == nsGkAtoms::sub || nameAtom == nsGkAtoms::sup ||
309 nameAtom == nsGkAtoms::i || nameAtom == nsGkAtoms::b ||
310 nameAtom == nsGkAtoms::u || nameAtom == nsGkAtoms::mark ||
311 nameAtom == nsGkAtoms::bdi || nameAtom == nsGkAtoms::bdo ||
312 nameAtom == nsGkAtoms::span || nameAtom == nsGkAtoms::br ||
313 nameAtom == nsGkAtoms::wbr;
316 already_AddRefed<Element> L10nOverlays::CreateSanitizedElement(
317 Element* aElement, ErrorResult& aRv) {
318 // Start with an empty element of the same type to remove nested children
319 // and non-localizable attributes defined by the translation.
321 nsAutoString nameSpaceURI;
322 aElement->NodeInfo()->GetNamespaceURI(nameSpaceURI);
323 ElementCreationOptionsOrString options;
324 RefPtr<Element> clone = aElement->OwnerDoc()->CreateElementNS(
325 nameSpaceURI, aElement->NodeInfo()->LocalName(), options, aRv);
326 if (NS_WARN_IF(aRv.Failed())) {
327 return nullptr;
330 ShallowPopulateUsing(aElement, clone, aRv);
331 if (NS_WARN_IF(aRv.Failed())) {
332 return nullptr;
334 return clone.forget();
337 void L10nOverlays::OverlayChildNodes(DocumentFragment* aFromFragment,
338 Element* aToElement,
339 nsTArray<L10nOverlaysError>& aErrors,
340 ErrorResult& aRv) {
341 nsINodeList* childNodes = aFromFragment->ChildNodes();
342 for (uint32_t i = 0; i < childNodes->Length(); i++) {
343 nsINode* childNode = childNodes->Item(i);
345 if (!childNode->IsElement()) {
346 // Keep the translated text node.
347 continue;
350 RefPtr<Element> childElement = childNode->AsElement();
352 if (childElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nname)) {
353 RefPtr<nsINode> sanitized =
354 GetNodeForNamedElement(aToElement, childElement, aErrors, aRv);
355 if (NS_WARN_IF(aRv.Failed())) {
356 return;
358 aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
359 if (NS_WARN_IF(aRv.Failed())) {
360 return;
362 continue;
365 if (IsElementAllowed(childElement)) {
366 RefPtr<Element> sanitized = CreateSanitizedElement(childElement, aRv);
367 if (NS_WARN_IF(aRv.Failed())) {
368 return;
370 aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
371 if (NS_WARN_IF(aRv.Failed())) {
372 return;
374 continue;
377 L10nOverlaysError error;
378 error.mCode.Construct(L10nOverlays_Binding::ERROR_FORBIDDEN_TYPE);
379 error.mTranslatedElementName.Construct(
380 childElement->NodeInfo()->LocalName());
381 aErrors.AppendElement(error);
383 // If all else fails, replace the element with its text content.
384 RefPtr<nsINode> textNode = CreateTextNodeFromTextContent(childElement, aRv);
385 if (NS_WARN_IF(aRv.Failed())) {
386 return;
389 aFromFragment->ReplaceChild(*textNode, *childNode, aRv);
390 if (NS_WARN_IF(aRv.Failed())) {
391 return;
395 while (aToElement->HasChildren()) {
396 nsIContent* child = aToElement->GetLastChild();
397 #ifdef DEBUG
398 if (child->IsElement()) {
399 if (child->AsElement()->HasAttr(kNameSpaceID_None,
400 nsGkAtoms::datal10nid)) {
401 L10nOverlaysError error;
402 error.mCode.Construct(
403 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISCONNECTED);
404 nsAutoString id;
405 child->AsElement()->GetAttr(nsGkAtoms::datal10nid, id);
406 error.mL10nName.Construct(id);
407 error.mTranslatedElementName.Construct(
408 aToElement->NodeInfo()->LocalName());
409 aErrors.AppendElement(error);
410 } else if (child->AsElement()->ChildElementCount() > 0) {
411 L10nOverlaysError error;
412 error.mCode.Construct(
413 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM);
414 nsAutoString id;
415 aToElement->GetAttr(nsGkAtoms::datal10nid, id);
416 error.mL10nName.Construct(id);
417 error.mTranslatedElementName.Construct(
418 aToElement->NodeInfo()->LocalName());
419 aErrors.AppendElement(error);
422 #endif
423 aToElement->RemoveChildNode(child, true);
425 aToElement->AppendChild(*aFromFragment, aRv);
426 if (NS_WARN_IF(aRv.Failed())) {
427 return;
431 void L10nOverlays::TranslateElement(
432 const GlobalObject& aGlobal, Element& aElement,
433 const L10nMessage& aTranslation,
434 Nullable<nsTArray<L10nOverlaysError>>& aErrors) {
435 nsTArray<L10nOverlaysError> errors;
437 ErrorResult rv;
439 TranslateElement(aElement, aTranslation, errors, rv);
440 if (NS_WARN_IF(rv.Failed())) {
441 L10nOverlaysError error;
442 error.mCode.Construct(L10nOverlays_Binding::ERROR_UNKNOWN);
443 errors.AppendElement(error);
445 if (!errors.IsEmpty()) {
446 aErrors.SetValue(std::move(errors));
450 bool L10nOverlays::ContainsMarkup(const nsACString& aStr) {
451 // We use our custom ContainsMarkup rather than the
452 // one from FragmentOrElement.cpp, because we don't
453 // want to trigger HTML parsing on every `Preferences & Options`
454 // type of string.
455 const char* start = aStr.BeginReading();
456 const char* end = aStr.EndReading();
458 while (start != end) {
459 char c = *start;
460 if (c == '<') {
461 return true;
463 ++start;
465 if (c == '&' && start != end) {
466 c = *start;
467 if (c == '#' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
468 (c >= 'A' && c <= 'Z')) {
469 return true;
471 ++start;
475 return false;
478 void L10nOverlays::TranslateElement(Element& aElement,
479 const L10nMessage& aTranslation,
480 nsTArray<L10nOverlaysError>& aErrors,
481 ErrorResult& aRv) {
482 if (!aTranslation.mValue.IsVoid()) {
483 NodeInfo* nodeInfo = aElement.NodeInfo();
484 if (nodeInfo->NameAtom() == nsGkAtoms::title &&
485 nodeInfo->NamespaceID() == kNameSpaceID_XHTML) {
486 // A special case for the HTML title element whose content must be text.
487 aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv);
488 if (NS_WARN_IF(aRv.Failed())) {
489 return;
491 } else if (!ContainsMarkup(aTranslation.mValue)) {
492 #ifdef DEBUG
493 if (aElement.ChildElementCount() > 0) {
494 L10nOverlaysError error;
495 error.mCode.Construct(
496 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM);
497 nsAutoString id;
498 aElement.GetAttr(nsGkAtoms::datal10nid, id);
499 error.mL10nName.Construct(id);
500 error.mTranslatedElementName.Construct(
501 aElement.GetLastElementChild()->NodeInfo()->LocalName());
502 aErrors.AppendElement(error);
504 #endif
505 // If the translation doesn't contain any markup skip the overlay logic.
506 aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv);
507 if (NS_WARN_IF(aRv.Failed())) {
508 return;
510 } else {
511 // Else parse the translation's HTML into a DocumentFragment,
512 // sanitize it and replace the element's content.
513 RefPtr<DocumentFragment> fragment =
514 new (aElement.OwnerDoc()->NodeInfoManager())
515 DocumentFragment(aElement.OwnerDoc()->NodeInfoManager());
516 nsContentUtils::ParseFragmentHTML(
517 NS_ConvertUTF8toUTF16(aTranslation.mValue), fragment,
518 nsGkAtoms::_template, kNameSpaceID_XHTML, false, true);
519 if (NS_WARN_IF(aRv.Failed())) {
520 return;
523 OverlayChildNodes(fragment, &aElement, aErrors, aRv);
524 if (NS_WARN_IF(aRv.Failed())) {
525 return;
530 // Even if the translation doesn't define any localizable attributes, run
531 // overlayAttributes to remove any localizable attributes set by previous
532 // translations.
533 OverlayAttributes(aTranslation.mAttributes, &aElement, aRv);
534 if (NS_WARN_IF(aRv.Failed())) {
535 return;