From 0890bbe4c281b5491c37dff3d88680d1d23c0b75 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Wed, 24 Apr 2019 05:05:11 +0000 Subject: [PATCH] Bug 1503657 - Implement Fluent DOMOverlays in C++. r=smaug,Pike Differential Revision: https://phabricator.services.mozilla.com/D27200 --HG-- extra : moz-landing-system : lando --- dom/bindings/Bindings.conf | 4 + dom/chrome-webidl/DOMOverlays.webidl | 21 + dom/chrome-webidl/moz.build | 1 + dom/html/HTMLTemplateElement.h | 2 + dom/l10n/DOMOverlays.cpp | 483 ++++++++++++++++++ dom/l10n/DOMOverlays.h | 118 +++++ dom/l10n/components.conf | 8 + dom/l10n/moz.build | 17 + dom/l10n/tests/gtest/TestDOMOverlays.cpp | 81 +++ dom/l10n/tests/gtest/moz.build | 11 + dom/l10n/tests/mochitest/test_domoverlays.xul | 4 +- .../test_domoverlays_attributes.html | 5 +- .../test_domoverlays_extra_text_markup.html | 5 +- .../test_domoverlays_functional_children.html | 4 +- .../test_domoverlays_text_children.html | 4 +- intl/l10n/DOMLocalization.jsm | 2 - xpcom/ds/StaticAtoms.py | 1 + 17 files changed, 757 insertions(+), 14 deletions(-) create mode 100644 dom/chrome-webidl/DOMOverlays.webidl create mode 100644 dom/l10n/DOMOverlays.cpp create mode 100644 dom/l10n/DOMOverlays.h create mode 100644 dom/l10n/components.conf create mode 100644 dom/l10n/tests/gtest/TestDOMOverlays.cpp create mode 100644 dom/l10n/tests/gtest/moz.build diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 891f31369ffc..c9aafe8870f8 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -239,6 +239,10 @@ DOMInterfaces = { 'headerFile': 'mozilla/dom/DOMMatrix.h', }, +'DOMOverlays': { + 'nativeType': 'mozilla::dom::l10n::DOMOverlays', +}, + 'DOMPointReadOnly': { 'headerFile': 'mozilla/dom/DOMPoint.h', }, diff --git a/dom/chrome-webidl/DOMOverlays.webidl b/dom/chrome-webidl/DOMOverlays.webidl new file mode 100644 index 000000000000..a7b85204f149 --- /dev/null +++ b/dom/chrome-webidl/DOMOverlays.webidl @@ -0,0 +1,21 @@ +/* -*- Mode: C++; 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 DOMOverlaysError { + short code; + DOMString translatedElementName; + DOMString sourceElementName; + DOMString l10nName; +}; + +[ChromeOnly] +namespace DOMOverlays { + const unsigned short ERROR_FORBIDDEN_TYPE = 1; + const unsigned short ERROR_NAMED_ELEMENT_MISSING = 2; + const unsigned short ERROR_NAMED_ELEMENT_TYPE_MISMATCH = 3; + const unsigned short ERROR_UNKNOWN = 4; + + sequence? translateElement(Element element, optional L10nValue translation); +}; diff --git a/dom/chrome-webidl/moz.build b/dom/chrome-webidl/moz.build index a50504ad530c..ad5b5b9bc3a1 100644 --- a/dom/chrome-webidl/moz.build +++ b/dom/chrome-webidl/moz.build @@ -36,6 +36,7 @@ WEBIDL_FILES = [ 'BrowsingContext.webidl', 'ChannelWrapper.webidl', 'DominatorTree.webidl', + 'DOMOverlays.webidl', 'Flex.webidl', 'HeapSnapshot.webidl', 'InspectorUtils.webidl', diff --git a/dom/html/HTMLTemplateElement.h b/dom/html/HTMLTemplateElement.h index 06784012e0d6..1e0de4678d35 100644 --- a/dom/html/HTMLTemplateElement.h +++ b/dom/html/HTMLTemplateElement.h @@ -22,6 +22,8 @@ class HTMLTemplateElement final : public nsGenericHTMLElement { // nsISupports NS_DECL_ISUPPORTS_INHERITED + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTemplateElement, _template); + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTemplateElement, nsGenericHTMLElement) diff --git a/dom/l10n/DOMOverlays.cpp b/dom/l10n/DOMOverlays.cpp new file mode 100644 index 000000000000..d00b07c6ad31 --- /dev/null +++ b/dom/l10n/DOMOverlays.cpp @@ -0,0 +1,483 @@ +#include "DOMOverlays.h" +#include "mozilla/dom/HTMLTemplateElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "HTMLSplitOnSpacesTokenizer.h" +#include "nsHtml5StringParser.h" +#include "nsTextNode.h" + +using namespace mozilla::dom::l10n; +using namespace mozilla::dom; +using namespace mozilla; + +bool DOMOverlays::IsAttrNameLocalizable( + const nsAtom* nameAtom, Element* aElement, + nsTArray* aExplicitlyAllowed) { + nsAutoString name; + nameAtom->ToString(name); + + if (aExplicitlyAllowed->Contains(name)) { + return true; + } + + nsAtom* elemName = aElement->NodeInfo()->NameAtom(); + + uint32_t nameSpace = aElement->NodeInfo()->NamespaceID(); + + if (nameSpace == kNameSpaceID_XHTML) { + // Is it a globally safe attribute? + if (nameAtom == nsGkAtoms::title || nameAtom == nsGkAtoms::aria_label || + nameAtom == nsGkAtoms::aria_valuetext) { + return true; + } + + // Is it allowed on this element? + if (elemName == nsGkAtoms::a) { + return nameAtom == nsGkAtoms::download; + } + if (elemName == nsGkAtoms::area) { + return nameAtom == nsGkAtoms::download || nameAtom == nsGkAtoms::alt; + } + if (elemName == nsGkAtoms::input) { + // Special case for value on HTML inputs with type button, reset, submit + if (nameAtom == nsGkAtoms::value) { + HTMLInputElement* input = HTMLInputElement::FromNode(aElement); + if (input) { + uint32_t type = input->ControlType(); + if (type == NS_FORM_INPUT_SUBMIT || type == NS_FORM_INPUT_BUTTON || + type == NS_FORM_INPUT_RESET) { + return true; + } + } + } + return nameAtom == nsGkAtoms::alt || nameAtom == nsGkAtoms::placeholder; + } + if (elemName == nsGkAtoms::menuitem) { + return nameAtom == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::menu) { + return nameAtom == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::optgroup) { + return nameAtom == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::option) { + return nameAtom == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::track) { + return nameAtom == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::img) { + return nameAtom == nsGkAtoms::alt; + } + if (elemName == nsGkAtoms::textarea) { + return nameAtom == nsGkAtoms::placeholder; + } + if (elemName == nsGkAtoms::th) { + return nameAtom == nsGkAtoms::abbr; + } + + } else if (nameSpace == kNameSpaceID_XUL) { + // Is it a globally safe attribute? + if (nameAtom == nsGkAtoms::accesskey || nameAtom == nsGkAtoms::aria_label || + nameAtom == nsGkAtoms::aria_valuetext || nameAtom == nsGkAtoms::label || + nameAtom == nsGkAtoms::title || nameAtom == nsGkAtoms::tooltiptext) { + return true; + } + + // Is it allowed on this element? + if (elemName == nsGkAtoms::description) { + return nameAtom == nsGkAtoms::value; + } + if (elemName == nsGkAtoms::key) { + return nameAtom == nsGkAtoms::key || nameAtom == nsGkAtoms::keycode; + } + if (elemName == nsGkAtoms::label) { + return nameAtom == nsGkAtoms::value; + } + if (elemName == nsGkAtoms::textbox) { + return nameAtom == nsGkAtoms::placeholder || nameAtom == nsGkAtoms::value; + } + } + + return false; +} + +already_AddRefed DOMOverlays::CreateTextNodeFromTextContent( + Element* aElement, ErrorResult& aRv) { + nsAutoString content; + aElement->GetTextContent(content, aRv); + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return aElement->OwnerDoc()->CreateTextNode(content); +} + +class AttributeNameValueComparator { + public: + bool Equals(const AttributeNameValue& aAttribute, + const nsAttrName* aAttrName) const { + return aAttrName->Equals(aAttribute.mName); + } +}; + +void DOMOverlays::OverlayAttributes( + const Nullable>& aTranslation, + Element* aToElement, ErrorResult& aRv) { + nsTArray explicitlyAllowed; + + nsAutoString l10nAttrs; + aToElement->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nattrs, l10nAttrs); + + HTMLSplitOnSpacesTokenizer tokenizer(l10nAttrs, ','); + while (tokenizer.hasMoreTokens()) { + const nsAString& token = tokenizer.nextToken(); + if (!token.IsEmpty() && !explicitlyAllowed.Contains(token)) { + explicitlyAllowed.AppendElement(token); + } + } + + uint32_t i = aToElement->GetAttrCount(); + while (i > 0) { + const nsAttrName* attrName = aToElement->GetAttrNameAt(i - 1); + + if (IsAttrNameLocalizable(attrName->LocalName(), aToElement, + &explicitlyAllowed) && + (aTranslation.IsNull() || + !aTranslation.Value().Contains(attrName, + AttributeNameValueComparator()))) { + nsAutoString name; + attrName->LocalName()->ToString(name); + aToElement->RemoveAttribute(name, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + i--; + } + + if (aTranslation.IsNull()) { + return; + } + + for (auto& attribute : aTranslation.Value()) { + nsString attrName = attribute.mName; + RefPtr nameAtom = NS_Atomize(attrName); + if (IsAttrNameLocalizable(nameAtom, aToElement, &explicitlyAllowed)) { + nsString value = attribute.mValue; + if (!aToElement->AttrValueIs(kNameSpaceID_None, nameAtom, value, + eCaseMatters)) { + aToElement->SetAttr(nameAtom, value, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + } + } +} + +void DOMOverlays::OverlayAttributes(Element* aFromElement, Element* aToElement, + ErrorResult& aRv) { + Nullable> attributes; + uint32_t attrCount = aFromElement->GetAttrCount(); + + if (attrCount == 0) { + attributes.SetNull(); + } else { + Sequence sequence; + + uint32_t i = 0; + while (BorrowedAttrInfo info = aFromElement->GetAttrInfoAt(i++)) { + AttributeNameValue* attr = sequence.AppendElement(fallible); + MOZ_ASSERT(info.mName->NamespaceEquals(kNameSpaceID_None), + "No namespaced attributes allowed."); + info.mName->LocalName()->ToString(attr->mName); + info.mValue->ToString(attr->mValue); + } + + attributes.SetValue(sequence); + } + + return OverlayAttributes(attributes, aToElement, aRv); +} + +void DOMOverlays::ShallowPopulateUsing(Element* aFromElement, + Element* aToElement, ErrorResult& aRv) { + nsAutoString content; + aFromElement->GetTextContent(content, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aToElement->SetTextContent(content, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + OverlayAttributes(aFromElement, aToElement, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +already_AddRefed DOMOverlays::GetNodeForNamedElement( + Element* aSourceElement, Element* aTranslatedChild, + nsTArray& aErrors, ErrorResult& aRv) { + nsAutoString childName; + aTranslatedChild->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nname, + childName); + RefPtr sourceChild = nullptr; + + nsINodeList* childNodes = aSourceElement->ChildNodes(); + for (uint32_t i = 0; i < childNodes->Length(); i++) { + nsINode* childNode = childNodes->Item(i); + + if (!childNode->IsElement()) { + continue; + } + Element* childElement = childNode->AsElement(); + + if (childElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nname, + childName, eCaseMatters)) { + sourceChild = childElement; + break; + } + } + + if (!sourceChild) { + DOMOverlaysError error; + error.mCode.Construct(DOMOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING); + error.mL10nName.Construct(childName); + aErrors.AppendElement(error); + return CreateTextNodeFromTextContent(aTranslatedChild, aRv); + } + + nsAtom* sourceChildName = sourceChild->NodeInfo()->NameAtom(); + nsAtom* translatedChildName = aTranslatedChild->NodeInfo()->NameAtom(); + if (sourceChildName != translatedChildName && + // Create a specific exception for img vs. image mismatches, + // see bug 1543493 + !(translatedChildName == nsGkAtoms::img && + sourceChildName == nsGkAtoms::image)) { + DOMOverlaysError error; + error.mCode.Construct( + DOMOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH); + error.mL10nName.Construct(childName); + error.mTranslatedElementName.Construct( + aTranslatedChild->NodeInfo()->LocalName()); + error.mSourceElementName.Construct(sourceChild->NodeInfo()->LocalName()); + aErrors.AppendElement(error); + return CreateTextNodeFromTextContent(aTranslatedChild, aRv); + } + + aSourceElement->RemoveChild(*sourceChild, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + RefPtr clone = sourceChild->CloneNode(false, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + ShallowPopulateUsing(aTranslatedChild, clone->AsElement(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + return clone.forget(); +} + +bool DOMOverlays::IsElementAllowed(Element* aElement) { + uint32_t nameSpace = aElement->NodeInfo()->NamespaceID(); + if (nameSpace != kNameSpaceID_XHTML) { + return false; + } + + nsAtom* nameAtom = aElement->NodeInfo()->NameAtom(); + + return nameAtom == nsGkAtoms::em || nameAtom == nsGkAtoms::strong || + nameAtom == nsGkAtoms::small || nameAtom == nsGkAtoms::s || + nameAtom == nsGkAtoms::cite || nameAtom == nsGkAtoms::q || + nameAtom == nsGkAtoms::dfn || nameAtom == nsGkAtoms::abbr || + nameAtom == nsGkAtoms::data || nameAtom == nsGkAtoms::time || + nameAtom == nsGkAtoms::code || nameAtom == nsGkAtoms::var || + nameAtom == nsGkAtoms::samp || nameAtom == nsGkAtoms::kbd || + nameAtom == nsGkAtoms::sub || nameAtom == nsGkAtoms::sup || + nameAtom == nsGkAtoms::i || nameAtom == nsGkAtoms::b || + nameAtom == nsGkAtoms::u || nameAtom == nsGkAtoms::mark || + nameAtom == nsGkAtoms::bdi || nameAtom == nsGkAtoms::bdo || + nameAtom == nsGkAtoms::span || nameAtom == nsGkAtoms::br || + nameAtom == nsGkAtoms::wbr; +} + +already_AddRefed DOMOverlays::CreateSanitizedElement( + Element* aElement, ErrorResult& aRv) { + // Start with an empty element of the same type to remove nested children + // and non-localizable attributes defined by the translation. + + ElementCreationOptionsOrString options; + RefPtr clone = aElement->OwnerDoc()->CreateElement( + aElement->NodeInfo()->LocalName(), options, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + ShallowPopulateUsing(aElement, clone, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + return clone.forget(); +} + +void DOMOverlays::OverlayChildNodes(DocumentFragment* aFromFragment, + Element* aToElement, + nsTArray& aErrors, + ErrorResult& aRv) { + nsINodeList* childNodes = aFromFragment->ChildNodes(); + for (uint32_t i = 0; i < childNodes->Length(); i++) { + nsINode* childNode = childNodes->Item(i); + + if (!childNode->IsElement()) { + // Keep the translated text node. + continue; + } + + RefPtr childElement = childNode->AsElement(); + + if (childElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nname)) { + RefPtr sanitized = + GetNodeForNamedElement(aToElement, childElement, aErrors, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + aFromFragment->ReplaceChild(*sanitized, *childNode, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + continue; + } + + if (IsElementAllowed(childElement)) { + RefPtr sanitized = CreateSanitizedElement(childElement, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + aFromFragment->ReplaceChild(*sanitized, *childNode, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + continue; + } + + DOMOverlaysError error; + error.mCode.Construct(DOMOverlays_Binding::ERROR_FORBIDDEN_TYPE); + error.mTranslatedElementName.Construct( + childElement->NodeInfo()->LocalName()); + aErrors.AppendElement(error); + + // If all else fails, replace the element with its text content. + RefPtr textNode = CreateTextNodeFromTextContent(childElement, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aFromFragment->ReplaceChild(*textNode, *childNode, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + while (aToElement->HasChildren()) { + aToElement->RemoveChildNode(aToElement->GetLastChild(), true); + } + aToElement->AppendChild(*aFromFragment, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +void DOMOverlays::TranslateElement( + const GlobalObject& aGlobal, Element& aElement, + const L10nValue& aTranslation, + Nullable>& aErrors) { + nsTArray errors; + + ErrorResult rv; + + TranslateElement(aElement, aTranslation, errors, rv); + if (NS_WARN_IF(rv.Failed())) { + DOMOverlaysError error; + error.mCode.Construct(DOMOverlays_Binding::ERROR_UNKNOWN); + errors.AppendElement(error); + } + if (!errors.IsEmpty()) { + aErrors.SetValue(errors); + } +} + +bool DOMOverlays::ContainsMarkup(const nsAString& aStr) { + // We use our custom ContainsMarkup rather than the + // one from FragmentOrElement.cpp, because we don't + // want to trigger HTML parsing on every `Preferences & Options` + // type of string. + const char16_t* start = aStr.BeginReading(); + const char16_t* end = aStr.EndReading(); + + while (start != end) { + char16_t c = *start; + if (c == char16_t('<')) { + return true; + } + ++start; + + if (c == char16_t('&') && start != end) { + c = *start; + if (c == char16_t('#') || (c >= char16_t('0') && c <= char16_t('9')) || + (c >= char16_t('a') && c <= char16_t('z')) || + (c >= char16_t('A') && c <= char16_t('Z'))) { + return true; + } + ++start; + } + } + + return false; +} + +void DOMOverlays::TranslateElement(Element& aElement, + const L10nValue& aTranslation, + nsTArray& aErrors, + ErrorResult& aRv) { + if (!aTranslation.mValue.IsVoid()) { + if (!ContainsMarkup(aTranslation.mValue)) { + // If the translation doesn't contain any markup skip the overlay logic. + aElement.SetTextContent(aTranslation.mValue, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else { + // Else parse the translation's HTML into a DocumentFragment, + // sanitize it and replace the element's content. + RefPtr fragment = + new DocumentFragment(aElement.OwnerDoc()->NodeInfoManager()); + nsContentUtils::ParseFragmentHTML( + aTranslation.mValue, fragment, nsGkAtoms::_template, + kNameSpaceID_XHTML, false, true); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + OverlayChildNodes(fragment, &aElement, aErrors, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + } + + // Even if the translation doesn't define any localizable attributes, run + // overlayAttributes to remove any localizable attributes set by previous + // translations. + OverlayAttributes(aTranslation.mAttributes, &aElement, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} diff --git a/dom/l10n/DOMOverlays.h b/dom/l10n/DOMOverlays.h new file mode 100644 index 000000000000..9378b98617fd --- /dev/null +++ b/dom/l10n/DOMOverlays.h @@ -0,0 +1,118 @@ +#ifndef mozilla_dom_l10n_DOMOverlays_h__ +#define mozilla_dom_l10n_DOMOverlays_h__ + +#include "mozilla/dom/Element.h" +#include "mozilla/dom/L10nUtilsBinding.h" +#include "mozilla/dom/DOMOverlaysBinding.h" + +namespace mozilla { +namespace dom { +namespace l10n { + +class DOMOverlays { + public: + /** + * Translate an element. + * + * Translate the element's text content and attributes. Some HTML markup is + * allowed in the translation. The element's children with the data-l10n-name + * attribute will be treated as arguments to the translation. If the + * translation defines the same children, their attributes and text contents + * will be used for translating the matching source child. + */ + static void TranslateElement( + const GlobalObject& aGlobal, Element& aElement, + const L10nValue& aTranslation, + Nullable>& aErrors); + static void TranslateElement( + Element& aElement, const L10nValue& aTranslation, + nsTArray& aErrors, ErrorResult& aRv); + + private: + /** + * Check if attribute is allowed for the given element. + * + * This method is used by the sanitizer when the translation markup contains + * DOM attributes, or when the translation has traits which map to DOM + * attributes. + * + * `aExplicitlyAllowed` can be passed as a list of attributes explicitly + * allowed on this element. + */ + static bool IsAttrNameLocalizable(const nsAtom* nameAtom, Element* aElement, + nsTArray* aExplicitlyAllowed); + + /** + * Create a text node from text content of an Element. + */ + static already_AddRefed CreateTextNodeFromTextContent( + Element* aElement, ErrorResult& aRv); + + /** + * Transplant localizable attributes of an element to another element. + * + * Any localizable attributes already set on the target element will be + * cleared. + */ + static void OverlayAttributes( + const Nullable>& aTranslation, + Element* aToElement, ErrorResult& aRv); + static void OverlayAttributes(Element* aFromElement, Element* aToElement, + ErrorResult& aRv); + + /** + * Helper to set textContent and localizable attributes on an element. + */ + static void ShallowPopulateUsing(Element* aFromElement, Element* aToElement, + ErrorResult& aRv); + + /** + * Sanitize a child element created by the translation. + * + * Try to find a corresponding child in sourceElement and use it as the base + * for the sanitization. This will preserve functional attributes defined on + * the child element in the source HTML. + */ + static already_AddRefed GetNodeForNamedElement( + Element* aSourceElement, Element* aTranslatedChild, + nsTArray& aErrors, ErrorResult& aRv); + + /** + * Check if element is allowed in the translation. + * + * This method is used by the sanitizer when the translation markup contains + * an element which is not present in the source code. + */ + static bool IsElementAllowed(Element* aElement); + + /** + * Sanitize an allowed element. + * + * Text-level elements allowed in translations may only use safe attributes + * and will have any nested markup stripped to text content. + */ + static already_AddRefed CreateSanitizedElement(Element* aElement, + ErrorResult& aRv); + + /** + * Replace child nodes of an element with child nodes of another element. + * + * The contents of the target element will be cleared and fully replaced with + * sanitized contents of the source element. + */ + static void OverlayChildNodes(DocumentFragment* aFromFragment, + Element* aToElement, + nsTArray& aErrors, + ErrorResult& aRv); + + /** + * A helper used to test if the string contains HTML markup. + */ + static bool ContainsMarkup(const nsAString& aStr); +}; + +} // namespace l10n +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/l10n/components.conf b/dom/l10n/components.conf new file mode 100644 index 000000000000..c7f23be4479b --- /dev/null +++ b/dom/l10n/components.conf @@ -0,0 +1,8 @@ +Classes = [ + { + 'cid': '{8d85597c-3a92-11e9-9ffc-73d225b2d53f}', + 'contract_ids': ['@mozilla.org/dom/l10n/domoverlays;1'], + 'type': 'mozilla::dom::l10n::DOMOverlays', + 'headers': ['/dom/l10n/DOMOverlays.h'], + }, +] diff --git a/dom/l10n/moz.build b/dom/l10n/moz.build index a59617ace2ce..d917134e17f6 100644 --- a/dom/l10n/moz.build +++ b/dom/l10n/moz.build @@ -7,4 +7,21 @@ with Files("**"): BUG_COMPONENT = ("Core", "Internationalization") +EXPORTS.mozilla.dom.l10n += [ + 'DOMOverlays.h', +] + +UNIFIED_SOURCES += [ + 'DOMOverlays.cpp', +] + +LOCAL_INCLUDES += [ + '/dom/base', +] + +FINAL_LIBRARY = 'xul' + MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini'] + +if CONFIG['ENABLE_TESTS']: + DIRS += ['tests/gtest'] diff --git a/dom/l10n/tests/gtest/TestDOMOverlays.cpp b/dom/l10n/tests/gtest/TestDOMOverlays.cpp new file mode 100644 index 000000000000..a2ffa68307ca --- /dev/null +++ b/dom/l10n/tests/gtest/TestDOMOverlays.cpp @@ -0,0 +1,81 @@ +/* -*- Mode: C++; 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/. */ + +#include "gtest/gtest.h" +#include "mozilla/dom/l10n/DOMOverlays.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DOMOverlaysBinding.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/L10nUtilsBinding.h" +#include "mozilla/NullPrincipal.h" +#include "nsNetUtil.h" + +using mozilla::NullPrincipal; +using namespace mozilla::dom; +using namespace mozilla::dom::l10n; + +already_AddRefed SetUpDocument() { + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "about:blank"); + nsCOMPtr principal = + NullPrincipal::CreateWithoutOriginAttributes(); + nsCOMPtr document; + nsresult rv = NS_NewDOMDocument(getter_AddRefs(document), + EmptyString(), // aNamespaceURI + EmptyString(), // aQualifiedName + nullptr, // aDoctype + uri, uri, principal, + false, // aLoadedAsData + nullptr, // aEventObject + DocumentFlavorHTML); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + return document.forget(); +} + +/** + * This test verifies that the basic C++ DOMOverlays API + * works correctly. + */ +TEST(DOM_L10n_DOMOverlays, Initial) +{ + mozilla::ErrorResult rv; + + // 1. Set up an HTML document. + nsCOMPtr doc = SetUpDocument(); + + // 2. Create a simple Element with a child. + // + //
+ // + //
+ // + RefPtr elem = doc->CreateHTMLElement(nsGkAtoms::div); + RefPtr span = doc->CreateHTMLElement(nsGkAtoms::a); + span->SetAttribute(NS_LITERAL_STRING("data-l10n-name"), + NS_LITERAL_STRING("link"), rv); + span->SetAttribute(NS_LITERAL_STRING("href"), + NS_LITERAL_STRING("https://www.mozilla.org"), rv); + elem->AppendChild(*span, rv); + + // 3. Create an L10nValue with a translation for the element. + L10nValue translation; + translation.mValue.AssignLiteral( + "Hello World."); + + // 4. Translate the element. + nsTArray errors; + DOMOverlays::TranslateElement(*elem, translation, errors, rv); + + nsAutoString textContent; + elem->GetInnerHTML(textContent, rv); + + // 5. Verify that the innerHTML matches the expectations. + ASSERT_STREQ(NS_ConvertUTF16toUTF8(textContent).get(), + "Hello World."); +} diff --git a/dom/l10n/tests/gtest/moz.build b/dom/l10n/tests/gtest/moz.build new file mode 100644 index 000000000000..9b7436be127b --- /dev/null +++ b/dom/l10n/tests/gtest/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + 'TestDOMOverlays.cpp', +] + +FINAL_LIBRARY = 'xul-gtest' diff --git a/dom/l10n/tests/mochitest/test_domoverlays.xul b/dom/l10n/tests/mochitest/test_domoverlays.xul index 28335144177d..c49005862d4b 100644 --- a/dom/l10n/tests/mochitest/test_domoverlays.xul +++ b/dom/l10n/tests/mochitest/test_domoverlays.xul @@ -16,7 +16,7 @@ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />