зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1503657 - Implement Fluent DOMOverlays in C++. r=smaug,Pike
Differential Revision: https://phabricator.services.mozilla.com/D27200 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
d6b38f2162
Коммит
0890bbe4c2
|
@ -239,6 +239,10 @@ DOMInterfaces = {
|
|||
'headerFile': 'mozilla/dom/DOMMatrix.h',
|
||||
},
|
||||
|
||||
'DOMOverlays': {
|
||||
'nativeType': 'mozilla::dom::l10n::DOMOverlays',
|
||||
},
|
||||
|
||||
'DOMPointReadOnly': {
|
||||
'headerFile': 'mozilla/dom/DOMPoint.h',
|
||||
},
|
||||
|
|
|
@ -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<DOMOverlaysError>? translateElement(Element element, optional L10nValue translation);
|
||||
};
|
|
@ -36,6 +36,7 @@ WEBIDL_FILES = [
|
|||
'BrowsingContext.webidl',
|
||||
'ChannelWrapper.webidl',
|
||||
'DominatorTree.webidl',
|
||||
'DOMOverlays.webidl',
|
||||
'Flex.webidl',
|
||||
'HeapSnapshot.webidl',
|
||||
'InspectorUtils.webidl',
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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<nsString>* 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<nsINode> 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<Sequence<AttributeNameValue>>& aTranslation,
|
||||
Element* aToElement, ErrorResult& aRv) {
|
||||
nsTArray<nsString> 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<nsAtom> 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<Sequence<AttributeNameValue>> attributes;
|
||||
uint32_t attrCount = aFromElement->GetAttrCount();
|
||||
|
||||
if (attrCount == 0) {
|
||||
attributes.SetNull();
|
||||
} else {
|
||||
Sequence<AttributeNameValue> 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<nsINode> DOMOverlays::GetNodeForNamedElement(
|
||||
Element* aSourceElement, Element* aTranslatedChild,
|
||||
nsTArray<DOMOverlaysError>& aErrors, ErrorResult& aRv) {
|
||||
nsAutoString childName;
|
||||
aTranslatedChild->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nname,
|
||||
childName);
|
||||
RefPtr<Element> 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<nsINode> 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<Element> 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<Element> 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<DOMOverlaysError>& 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<Element> childElement = childNode->AsElement();
|
||||
|
||||
if (childElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nname)) {
|
||||
RefPtr<nsINode> 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<Element> 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<nsINode> 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<nsTArray<DOMOverlaysError>>& aErrors) {
|
||||
nsTArray<DOMOverlaysError> 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<DOMOverlaysError>& 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<DocumentFragment> 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;
|
||||
}
|
||||
}
|
|
@ -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<nsTArray<mozilla::dom::DOMOverlaysError>>& aErrors);
|
||||
static void TranslateElement(
|
||||
Element& aElement, const L10nValue& aTranslation,
|
||||
nsTArray<mozilla::dom::DOMOverlaysError>& 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<nsString>* aExplicitlyAllowed);
|
||||
|
||||
/**
|
||||
* Create a text node from text content of an Element.
|
||||
*/
|
||||
static already_AddRefed<nsINode> 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<Sequence<AttributeNameValue>>& 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<nsINode> GetNodeForNamedElement(
|
||||
Element* aSourceElement, Element* aTranslatedChild,
|
||||
nsTArray<DOMOverlaysError>& 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<Element> 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<DOMOverlaysError>& 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
|
|
@ -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'],
|
||||
},
|
||||
]
|
|
@ -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']
|
||||
|
|
|
@ -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<Document> SetUpDocument() {
|
||||
nsCOMPtr<nsIURI> uri;
|
||||
NS_NewURI(getter_AddRefs(uri), "about:blank");
|
||||
nsCOMPtr<nsIPrincipal> principal =
|
||||
NullPrincipal::CreateWithoutOriginAttributes();
|
||||
nsCOMPtr<Document> 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<Document> doc = SetUpDocument();
|
||||
|
||||
// 2. Create a simple Element with a child.
|
||||
//
|
||||
// <div>
|
||||
// <a data-l10n-name="link" href="https://www.mozilla.org"></a>
|
||||
// </div>
|
||||
//
|
||||
RefPtr<Element> elem = doc->CreateHTMLElement(nsGkAtoms::div);
|
||||
RefPtr<Element> 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 <a data-l10n-name=\"link\">World</a>.");
|
||||
|
||||
// 4. Translate the element.
|
||||
nsTArray<DOMOverlaysError> 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 <a data-l10n-name=\"link\" "
|
||||
"href=\"https://www.mozilla.org\">World</a>.");
|
||||
}
|
|
@ -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'
|
|
@ -16,7 +16,7 @@
|
|||
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
|
||||
<script type="application/javascript">
|
||||
<![CDATA[
|
||||
const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
|
||||
/* global DOMOverlays */
|
||||
|
||||
function elem(name) {
|
||||
return function(str) {
|
||||
|
@ -27,7 +27,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
const { translateElement } = DOMLocalization.DOMOverlays;
|
||||
const { translateElement } = DOMOverlays;
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<script type="application/javascript">
|
||||
/* global DOMOverlays */
|
||||
"use strict";
|
||||
|
||||
const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
|
||||
|
||||
function elem(name) {
|
||||
return function(str) {
|
||||
const element = document.createElement(name);
|
||||
|
@ -19,7 +18,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
const { translateElement } = DOMLocalization.DOMOverlays;
|
||||
const { translateElement } = DOMOverlays;
|
||||
|
||||
{
|
||||
// Allowed attribute
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<script type="application/javascript">
|
||||
/* global DOMOverlays */
|
||||
"use strict";
|
||||
|
||||
const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
|
||||
|
||||
function elem(name) {
|
||||
return function(str) {
|
||||
const element = document.createElement(name);
|
||||
|
@ -19,7 +18,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
const { translateElement } = DOMLocalization.DOMOverlays;
|
||||
const { translateElement } = DOMOverlays;
|
||||
|
||||
// Localized text markup
|
||||
{
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<script type="application/javascript">
|
||||
/* global DOMOverlays */
|
||||
"use strict";
|
||||
const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
|
||||
|
||||
function elem(name) {
|
||||
return function(str) {
|
||||
|
@ -18,7 +18,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
const { translateElement } = DOMLocalization.DOMOverlays;
|
||||
const { translateElement } = DOMOverlays;
|
||||
|
||||
// Child without name
|
||||
{
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<script type="application/javascript">
|
||||
/* global DOMOverlays */
|
||||
"use strict";
|
||||
const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
|
||||
|
||||
function elem(name) {
|
||||
return function(str) {
|
||||
|
@ -18,7 +18,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
const { translateElement } = DOMLocalization.DOMOverlays;
|
||||
const { translateElement } = DOMOverlays;
|
||||
|
||||
{
|
||||
// without data-l10n-name
|
||||
|
|
|
@ -854,8 +854,6 @@ class DOMLocalization extends Localization {
|
|||
}
|
||||
}
|
||||
|
||||
DOMLocalization.DOMOverlays = { translateElement };
|
||||
|
||||
/**
|
||||
* Helper function which allows us to construct a new
|
||||
* DOMLocalization from DocumentL10n.
|
||||
|
|
|
@ -289,6 +289,7 @@ STATIC_ATOMS = [
|
|||
Atom("datal10nid", "data-l10n-id"),
|
||||
Atom("datal10nargs", "data-l10n-args"),
|
||||
Atom("datal10nattrs", "data-l10n-attrs"),
|
||||
Atom("datal10nname", "data-l10n-name"),
|
||||
Atom("dataType", "data-type"),
|
||||
Atom("dateTime", "date-time"),
|
||||
Atom("date", "date"),
|
||||
|
|
Загрузка…
Ссылка в новой задаче