From a8f62de0aeb0e630da3610882236ea9e4a41623e Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Wed, 11 Apr 2018 13:06:35 -0700 Subject: [PATCH] Bug 1453480 - Update fluent to 0.6.4 and fluent-dom to 0.2.0. r=stas MozReview-Commit-ID: La8uSw0sq4p --HG-- extra : rebase_source : 95181fb06ec75c36ebc2c234fdac68956b9e049e --- intl/l10n/DOMLocalization.jsm | 291 ++++++------ intl/l10n/Localization.jsm | 12 +- intl/l10n/MessageContext.jsm | 16 +- intl/l10n/fluent.js.patch | 421 +++++++++++------- intl/l10n/test/dom/test_domloc_overlay.html | 4 +- .../test_domloc_overlay_missing_children.html | 8 +- .../dom/test_domloc_overlay_repeated.html | 6 +- .../test/dom/test_domloc_repeated_l10nid.html | 6 +- 8 files changed, 444 insertions(+), 320 deletions(-) diff --git a/intl/l10n/DOMLocalization.jsm b/intl/l10n/DOMLocalization.jsm index aa0ce26ce563..0d9641cca178 100644 --- a/intl/l10n/DOMLocalization.jsm +++ b/intl/l10n/DOMLocalization.jsm @@ -16,7 +16,7 @@ */ -/* fluent@0.6.3 */ +/* fluent-dom@0.2.0 */ const { Localization } = ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); @@ -26,15 +26,18 @@ const { Localization } = const reOverlay = /<|&#?\w+;/; /** - * The list of elements that are allowed to be inserted into a localization. + * Elements allowed in translations even if they are not present in the source + * HTML. They are text-level elements as defined by the HTML5 spec: + * https://www.w3.org/TR/html5/text-level-semantics.html with the exception of: * - * Source: https://www.w3.org/TR/html5/text-level-semantics.html + * - a - because we don't allow href on it anyways, + * - ruby, rt, rp - because we don't allow nested elements to be inserted. */ -const LOCALIZABLE_ELEMENTS = { +const TEXT_LEVEL_ELEMENTS = { "http://www.w3.org/1999/xhtml": [ - "a", "em", "strong", "small", "s", "cite", "q", "dfn", "abbr", "data", + "em", "strong", "small", "s", "cite", "q", "dfn", "abbr", "data", "time", "code", "var", "samp", "kbd", "sub", "sup", "i", "b", "u", - "mark", "ruby", "rt", "rp", "bdi", "bdo", "span", "br", "wbr" + "mark", "bdi", "bdo", "span", "br", "wbr" ], }; @@ -66,153 +69,172 @@ const LOCALIZABLE_ATTRIBUTES = { /** - * Overlay translation onto a DOM element. + * Translate an element. * - * @param {Element} targetElement - * @param {string|Object} translation + * 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. + * + * @param {Element} element + * @param {Object} translation * @private */ -function overlayElement(targetElement, translation) { +function translateElement(element, translation) { const value = translation.value; if (typeof value === "string") { if (!reOverlay.test(value)) { // If the translation doesn't contain any markup skip the overlay logic. - targetElement.textContent = value; + element.textContent = value; } else { // Else parse the translation's HTML using an inert template element, - // sanitize it and replace the targetElement's content. - const templateElement = targetElement.ownerDocument.createElementNS( - "http://www.w3.org/1999/xhtml", "template"); + // sanitize it and replace the element's content. + const templateElement = element.ownerDocument.createElementNS( + "http://www.w3.org/1999/xhtml", "template" + ); // eslint-disable-next-line no-unsanitized/property templateElement.innerHTML = value; - targetElement.appendChild( - // The targetElement will be cleared at the end of sanitization. - sanitizeUsing(templateElement.content, targetElement) - ); + overlayChildNodes(templateElement.content, element); } } - const explicitlyAllowed = targetElement.hasAttribute("data-l10n-attrs") - ? targetElement.getAttribute("data-l10n-attrs") + // Even if the translation doesn't define any localizable attributes, run + // overlayAttributes to remove any localizable attributes set by previous + // translations. + overlayAttributes(translation, element); +} + +/** + * 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. + * + * @param {DocumentFragment} fromElement - The source of children to overlay. + * @param {Element} toElement - The target of the overlay. + * @private + */ +function overlayChildNodes(fromElement, toElement) { + const content = toElement.ownerDocument.createDocumentFragment(); + + for (const childNode of fromElement.childNodes) { + content.appendChild(sanitizeUsing(toElement, childNode)); + } + + toElement.textContent = ""; + toElement.appendChild(content); +} + +/** + * Transplant localizable attributes of an element to another element. + * + * Any localizable attributes already set on the target element will be + * cleared. + * + * @param {Element|Object} fromElement - The source of child nodes to overlay. + * @param {Element} toElement - The target of the overlay. + * @private + */ +function overlayAttributes(fromElement, toElement) { + const explicitlyAllowed = toElement.hasAttribute("data-l10n-attrs") + ? toElement.getAttribute("data-l10n-attrs") .split(",").map(i => i.trim()) : null; - // Remove localizable attributes which may have been set by a previous - // translation. - for (const attr of Array.from(targetElement.attributes)) { - if (isAttrNameLocalizable(attr.name, targetElement, explicitlyAllowed)) { - targetElement.removeAttribute(attr.name); + // Remove existing localizable attributes. + for (const attr of Array.from(toElement.attributes)) { + if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed)) { + toElement.removeAttribute(attr.name); } } - if (translation.attrs) { - for (const {name, value} of translation.attrs) { - if (isAttrNameLocalizable(name, targetElement, explicitlyAllowed)) { - targetElement.setAttribute(name, value); - } + // fromElement might be a {value, attributes} object as returned by + // Localization.messageFromContext. In which case attributes may be null to + // save GC cycles. + if (!fromElement.attributes) { + return; + } + + // Set localizable attributes. + for (const attr of Array.from(fromElement.attributes)) { + if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed)) { + toElement.setAttribute(attr.name, attr.value); } } } /** - * Sanitize `translationFragment` using `sourceElement` to add functional - * HTML attributes to children. `sourceElement` will have all its child nodes - * removed. + * Sanitize a child node created by the translation. * - * The sanitization is conducted according to the following rules: + * If childNode has the data-l10n-name attribute, try to find a corresponding + * child in sourceElement and use it as the base for the sanitization. This + * will preserve functional attribtues defined on the child element in the + * source HTML. * - * - Allow text nodes. - * - Replace forbidden children with their textContent. - * - Remove forbidden attributes from allowed children. + * This function must return new nodes or clones in all code paths. The + * returned nodes are immediately appended to the intermediate DocumentFragment + * which also _removes_ them from the constructed