Comparing version 0.0.1 to 0.1.0
# Changelog | ||
## Unreleased | ||
- … | ||
## fluent-dom 0.1.0 | ||
- Extend formatWithFallback to accept async iterator (#46) | ||
- Documented all methods | ||
- Removed `DOMLocalization.prototype.translateRoot` method | ||
- Simplified initial version of DOM Overlays (to be extended in 0.2) (#71) | ||
- Only children of the white-listed types are allowed now. It's not possible | ||
anymore to put elements of other types in the source HTML to make exceptions. | ||
- The identity of the source element's children is explicitly not kept | ||
anymore. This allows us to treat the translation DocumentFragment as the | ||
reference for iteration over child nodes. | ||
- The overlay function is also no longer recursive. Any nested HTML | ||
will be lost and only its textContent will be preserved. | ||
- Added `data-l10n-attrs` to allow for whitelisting localizable attributes (#70) | ||
- Added a guard to prevent registering nested roots (#72) | ||
- Added a guard to prevent leaking attributes between translations (#73) | ||
- Added a performance optimization coalescing all translations from mutations | ||
per animation frame (#113) | ||
## fluent-dom 0.0.1 | ||
@@ -8,0 +26,0 @@ |
{ | ||
"name": "fluent-dom", | ||
"version": "0.0.1", | ||
"version": "0.1.0", | ||
"description": "Fluent bindings for DOM", | ||
@@ -28,7 +28,18 @@ "repository": { | ||
"localization", | ||
"l10n" | ||
"l10n", | ||
"internationalization", | ||
"i18n", | ||
"locale", | ||
"language", | ||
"formatting", | ||
"translate", | ||
"translation", | ||
"format" | ||
], | ||
"engine": { | ||
"node": ">=6" | ||
"engines": { | ||
"node": ">=8.9.0" | ||
}, | ||
"devDependencies": { | ||
"jsdom": "^11.6.2" | ||
} | ||
} |
@@ -68,3 +68,3 @@ # fluent-dom | ||
Find out more about Project Fluent at [projectfluent.io][], including | ||
Find out more about Project Fluent at [projectfluent.org][], including | ||
documentation of the Fluent file format ([FTL][]), links to other packages and | ||
@@ -74,3 +74,3 @@ implementations, and information about how to get involved. | ||
[projectfluent.io]: http://projectfluent.io | ||
[FTL]: http://projectfluent.io/fluent/guide/ | ||
[projectfluent.org]: http://projectfluent.org | ||
[FTL]: http://projectfluent.org/fluent/guide/ |
import overlayElement from './overlay'; | ||
import Localization from './localization'; | ||
const L10NID_ATTR_NAME = 'data-l10n-id'; | ||
const L10NARGS_ATTR_NAME = 'data-l10n-args'; | ||
const L10N_ELEMENT_QUERY = `[${L10NID_ATTR_NAME}]`; | ||
/** | ||
@@ -14,11 +19,19 @@ * The `DOMLocalization` class is responsible for fetching resources and | ||
/** | ||
* @param {Window} windowElement | ||
* @param {Array<String>} resourceIds - List of resource IDs | ||
* @param {Function} generateMessages - Function that returns a | ||
* generator over MessageContexts | ||
* @returns {DOMLocalization} | ||
*/ | ||
constructor(MutationObserver, resIds, generateMessages) { | ||
super(resIds, generateMessages); | ||
this.query = '[data-l10n-id]'; | ||
constructor(windowElement, resourceIds, generateMessages) { | ||
super(resourceIds, generateMessages); | ||
// A Set of DOM trees observed by the `MutationObserver`. | ||
this.roots = new Set(); | ||
this.mutationObserver = new MutationObserver( | ||
// requestAnimationFrame handler. | ||
this.pendingrAF = null; | ||
// list of elements pending for translation. | ||
this.pendingElements = new Set(); | ||
this.windowElement = windowElement; | ||
this.mutationObserver = new windowElement.MutationObserver( | ||
mutations => this.translateMutations(mutations) | ||
@@ -32,10 +45,6 @@ ); | ||
subtree: true, | ||
attributeFilter: ['data-l10n-id', 'data-l10n-args'] | ||
attributeFilter: [L10NID_ATTR_NAME, L10NARGS_ATTR_NAME] | ||
}; | ||
} | ||
handleEvent() { | ||
this.onLanguageChange(); | ||
} | ||
onLanguageChange() { | ||
@@ -74,12 +83,15 @@ super.onLanguageChange(); | ||
* </p> | ||
* ``` | ||
* | ||
* @param {Element} element - Element to set attributes on | ||
* @param {string} id - l10n-id string | ||
* @param {Element} element - Element to set attributes on | ||
* @param {string} id - l10n-id string | ||
* @param {Object<string, string>} args - KVP list of l10n arguments | ||
* ``` | ||
* @returns {Element} | ||
*/ | ||
setAttributes(element, id, args) { | ||
element.setAttribute('data-l10n-id', id); | ||
element.setAttribute(L10NID_ATTR_NAME, id); | ||
if (args) { | ||
element.setAttribute('data-l10n-args', JSON.stringify(args)); | ||
element.setAttribute(L10NARGS_ATTR_NAME, JSON.stringify(args)); | ||
} else { | ||
element.removeAttribute(L10NARGS_ATTR_NAME); | ||
} | ||
@@ -104,4 +116,4 @@ return element; | ||
return { | ||
id: element.getAttribute('data-l10n-id'), | ||
args: JSON.parse(element.getAttribute('data-l10n-args')) | ||
id: element.getAttribute(L10NID_ATTR_NAME), | ||
args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null) | ||
}; | ||
@@ -111,12 +123,20 @@ } | ||
/** | ||
* Add `root` to the list of roots managed by this `DOMLocalization`. | ||
* Add `newRoot` to the list of roots managed by this `DOMLocalization`. | ||
* | ||
* Additionally, if this `DOMLocalization` has an observer, start observing | ||
* `root` in order to translate mutations in it. | ||
* `newRoot` in order to translate mutations in it. | ||
* | ||
* @param {Element} root - Root to observe. | ||
* @param {Element} newRoot - Root to observe. | ||
*/ | ||
connectRoot(root) { | ||
this.roots.add(root); | ||
this.mutationObserver.observe(root, this.observerConfig); | ||
connectRoot(newRoot) { | ||
for (const root of this.roots) { | ||
if (root === newRoot || | ||
root.contains(newRoot) || | ||
newRoot.contains(root)) { | ||
throw new Error('Cannot add a root that overlaps with existing root.'); | ||
} | ||
} | ||
this.roots.add(newRoot); | ||
this.mutationObserver.observe(newRoot, this.observerConfig); | ||
} | ||
@@ -153,3 +173,3 @@ | ||
return Promise.all( | ||
roots.map(root => this.translateRoot(root)) | ||
roots.map(root => this.translateElements(this.getTranslatables(root))) | ||
); | ||
@@ -159,11 +179,2 @@ } | ||
/** | ||
* Translate `root`. | ||
* | ||
* @returns {Promise} | ||
*/ | ||
translateRoot(root) { | ||
return this.translateFragment(root); | ||
} | ||
/** | ||
* Pauses the `MutationObserver`. | ||
@@ -174,2 +185,3 @@ * | ||
pauseObserving() { | ||
this.translateMutations(this.mutationObserver.takeRecords()); | ||
this.mutationObserver.disconnect(); | ||
@@ -198,3 +210,3 @@ } | ||
case 'attributes': | ||
this.translateElement(mutation.target); | ||
this.pendingElements.add(mutation.target); | ||
break; | ||
@@ -205,5 +217,7 @@ case 'childList': | ||
if (addedNode.childElementCount) { | ||
this.translateFragment(addedNode); | ||
} else if (addedNode.hasAttribute('data-l10n-id')) { | ||
this.translateElement(addedNode); | ||
for (const element of this.getTranslatables(addedNode)) { | ||
this.pendingElements.add(element); | ||
} | ||
} else if (addedNode.hasAttribute(L10NID_ATTR_NAME)) { | ||
this.pendingElements.add(addedNode); | ||
} | ||
@@ -215,4 +229,17 @@ } | ||
} | ||
// This fragment allows us to coalesce all pending translations | ||
// into a single requestAnimationFrame. | ||
if (this.pendingElements.size > 0) { | ||
if (this.pendingrAF === null) { | ||
this.pendingrAF = this.windowElement.requestAnimationFrame(() => { | ||
this.translateElements(Array.from(this.pendingElements)); | ||
this.pendingElements.clear(); | ||
this.pendingrAF = null; | ||
}); | ||
} | ||
} | ||
} | ||
/** | ||
@@ -232,27 +259,35 @@ * Translate a DOM element or fragment asynchronously using this | ||
translateFragment(frag) { | ||
const elements = this.getTranslatables(frag); | ||
if (!elements.length) { | ||
return Promise.resolve([]); | ||
} | ||
const keys = elements.map(this.getKeysForElement); | ||
return this.formatMessages(keys).then( | ||
translations => this.applyTranslations(elements, translations) | ||
); | ||
return this.translateElements(this.getTranslatables(frag)); | ||
} | ||
/** | ||
* Translate a single DOM element asynchronously. | ||
* Translate a list of DOM elements asynchronously using this | ||
* `DOMLocalization` object. | ||
* | ||
* Manually trigger the translation (or re-translation) of a list of elements. | ||
* Use the `data-l10n-id` and `data-l10n-args` attributes to mark up the DOM | ||
* with information about which translations to use. | ||
* | ||
* Returns a `Promise` that gets resolved once the translation is complete. | ||
* | ||
* @param {Element} element - HTML element to be translated | ||
* @param {Array<Element>} elements - List of elements to be translated | ||
* @returns {Promise} | ||
*/ | ||
translateElement(element) { | ||
return this.formatMessages([this.getKeysForElement(element)]).then( | ||
translations => this.applyTranslations([element], translations) | ||
); | ||
async translateElements(elements) { | ||
if (!elements.length) { | ||
return undefined; | ||
} | ||
const keys = elements.map(this.getKeysForElement); | ||
const translations = await this.formatMessages(keys); | ||
return this.applyTranslations(elements, translations); | ||
} | ||
/** | ||
* Applies translations onto elements. | ||
* | ||
* @param {Array<Element>} elements | ||
* @param {Array<Object>} translations | ||
* @private | ||
*/ | ||
applyTranslations(elements, translations) { | ||
@@ -268,7 +303,14 @@ this.pauseObserving(); | ||
/** | ||
* Collects all translatable child elements of the element. | ||
* | ||
* @param {Element} element | ||
* @returns {Array<Element>} | ||
* @private | ||
*/ | ||
getTranslatables(element) { | ||
const nodes = Array.from(element.querySelectorAll(this.query)); | ||
const nodes = Array.from(element.querySelectorAll(L10N_ELEMENT_QUERY)); | ||
if (typeof element.hasAttribute === 'function' && | ||
element.hasAttribute('data-l10n-id')) { | ||
element.hasAttribute(L10NID_ATTR_NAME)) { | ||
nodes.push(element); | ||
@@ -280,8 +322,16 @@ } | ||
/** | ||
* Get the `data-l10n-*` attributes from DOM elements as a two-element | ||
* array. | ||
* | ||
* @param {Element} element | ||
* @returns {Array<string, Object>} | ||
* @private | ||
*/ | ||
getKeysForElement(element) { | ||
return [ | ||
element.getAttribute('data-l10n-id'), | ||
JSON.parse(element.getAttribute('data-l10n-args') || null) | ||
element.getAttribute(L10NID_ATTR_NAME), | ||
JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null) | ||
]; | ||
} | ||
} |
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */ | ||
/* global console */ | ||
import { CachedIterable } from '../../fluent/src/index'; | ||
/** | ||
* Specialized version of an Error used to indicate errors that are result | ||
* of a problem during the localization process. | ||
* | ||
* We use them to identify the class of errors the require a fallback | ||
* mechanism to be triggered vs errors that should be reported, but | ||
* do not prevent the message from being used. | ||
* | ||
* An example of an L10nError is a missing entry. | ||
*/ | ||
class L10nError extends Error { | ||
constructor(message, id, lang) { | ||
constructor(message) { | ||
super(); | ||
this.name = 'L10nError'; | ||
this.message = message; | ||
this.id = id; | ||
this.lang = lang; | ||
} | ||
@@ -15,13 +25,19 @@ } | ||
/** | ||
* The `Localization` class is responsible for fetching resources and | ||
* formatting translations. | ||
* The `Localization` class is a central high-level API for vanilla | ||
* JavaScript use of Fluent. | ||
* It combines language negotiation, MessageContext and I/O to | ||
* provide a scriptable API to format translations. | ||
*/ | ||
export default class Localization { | ||
/** | ||
* @param {Array<String>} resourceIds - List of resource IDs | ||
* @param {Function} generateMessages - Function that returns a | ||
* generator over MessageContexts | ||
* | ||
* @returns {Localization} | ||
*/ | ||
constructor(resIds, generateMessages) { | ||
this.resIds = resIds; | ||
constructor(resourceIds, generateMessages) { | ||
this.resourceIds = resourceIds; | ||
this.generateMessages = generateMessages; | ||
this.ctxs = this.generateMessages(this.id, this.resIds); | ||
this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds)); | ||
} | ||
@@ -43,3 +59,8 @@ | ||
const translations = []; | ||
for (const ctx of this.ctxs) { | ||
for (let ctx of this.ctxs) { | ||
// This can operate on synchronous and asynchronous | ||
// contexts coming from the iterator. | ||
if (typeof ctx.then === 'function') { | ||
ctx = await ctx; | ||
} | ||
const errors = keysFromContext(method, ctx, keys, translations); | ||
@@ -88,3 +109,4 @@ if (!errors) { | ||
* ['hello', { who: 'Mary' }], | ||
* ['hello', { who: 'John' }] | ||
* ['hello', { who: 'John' }], | ||
* ['welcome'] | ||
* ]).then(console.log); | ||
@@ -134,4 +156,8 @@ * | ||
/** | ||
* This method should be called when there's a reason to believe | ||
* that language negotiation or available resources changed. | ||
*/ | ||
onLanguageChange() { | ||
this.ctxs = this.generateMessages(this.id, this.resIds); | ||
this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds)); | ||
} | ||
@@ -159,3 +185,3 @@ } | ||
function valueFromContext(ctx, errors, id, args) { | ||
const msg = ctx.getMessages(id); | ||
const msg = ctx.getMessage(id); | ||
@@ -222,4 +248,2 @@ if (msg === undefined) { | ||
/** | ||
* @private | ||
* | ||
* This function is an inner function for `Localization.formatWithFallback`. | ||
@@ -236,3 +260,3 @@ * | ||
* | ||
* In the function, we loop oer `keys` and check if we have the `prev` | ||
* In the function, we loop over `keys` and check if we have the `prev` | ||
* passed and if it has an error entry for the position we're in. | ||
@@ -255,2 +279,3 @@ * | ||
* @returns {Boolean} | ||
* @private | ||
*/ | ||
@@ -257,0 +282,0 @@ function keysFromContext(method, ctx, keys, translations) { |
@@ -5,4 +5,8 @@ // Match the opening angle bracket (<) in HTML tags, and HTML entities like | ||
// XXX The allowed list should be amendable; https://bugzil.la/922573. | ||
const ALLOWED_ELEMENTS = { | ||
/** | ||
* The list of elements that are allowed to be inserted into a localization. | ||
* | ||
* Source: https://www.w3.org/TR/html5/text-level-semantics.html | ||
*/ | ||
const LOCALIZABLE_ELEMENTS = { | ||
'http://www.w3.org/1999/xhtml': [ | ||
@@ -15,3 +19,3 @@ 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data', | ||
const ALLOWED_ATTRIBUTES = { | ||
const LOCALIZABLE_ATTRIBUTES = { | ||
'http://www.w3.org/1999/xhtml': { | ||
@@ -21,3 +25,3 @@ global: ['title', 'aria-label', 'aria-valuetext', 'aria-moz-hint'], | ||
area: ['download', 'alt'], | ||
// value is special-cased in isAttrAllowed | ||
// value is special-cased in isAttrNameLocalizable | ||
input: ['alt', 'placeholder'], | ||
@@ -47,7 +51,7 @@ menuitem: ['label'], | ||
* | ||
* @param {Element} element | ||
* @param {string} translation | ||
* @param {Element} targetElement | ||
* @param {string|Object} translation | ||
* @private | ||
*/ | ||
export default function overlayElement(element, translation) { | ||
export default function overlayElement(targetElement, translation) { | ||
const value = translation.value; | ||
@@ -58,21 +62,34 @@ | ||
// If the translation doesn't contain any markup skip the overlay logic. | ||
element.textContent = value; | ||
targetElement.textContent = value; | ||
} else { | ||
// Else start with an inert template element and move its children into | ||
// `element` but such that `element`'s own children are not replaced. | ||
const tmpl = element.ownerDocument.createElementNS( | ||
// 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'); | ||
tmpl.innerHTML = value; | ||
// Overlay the node with the DocumentFragment. | ||
overlay(element, tmpl.content); | ||
templateElement.innerHTML = value; | ||
targetElement.appendChild( | ||
// The targetElement will be cleared at the end of sanitization. | ||
sanitizeUsing(templateElement.content, targetElement) | ||
); | ||
} | ||
} | ||
if (translation.attrs === null) { | ||
return; | ||
const explicitlyAllowed = targetElement.hasAttribute('data-l10n-attrs') | ||
? targetElement.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); | ||
} | ||
} | ||
for (const [name, val] of translation.attrs) { | ||
if (isAttrAllowed({ name }, element)) { | ||
element.setAttribute(name, val); | ||
if (translation.attrs) { | ||
for (const [name, val] of translation.attrs) { | ||
if (isAttrNameLocalizable(name, targetElement, explicitlyAllowed)) { | ||
targetElement.setAttribute(name, val); | ||
} | ||
} | ||
@@ -82,67 +99,100 @@ } | ||
// The goal of overlay is to move the children of `translationElement` | ||
// into `sourceElement` such that `sourceElement`'s own children are not | ||
// replaced, but only have their text nodes and their attributes modified. | ||
// | ||
// We want to make it possible for localizers to apply text-level semantics to | ||
// the translations and make use of HTML entities. At the same time, we | ||
// don't trust translations so we need to filter unsafe elements and | ||
// attributes out and we don't want to break the Web by replacing elements to | ||
// which third-party code might have created references (e.g. two-way | ||
// bindings in MVC frameworks). | ||
function overlay(sourceElement, translationElement) { | ||
const result = translationElement.ownerDocument.createDocumentFragment(); | ||
let k, attr; | ||
// Take one node from translationElement at a time and check it against | ||
/** | ||
* Sanitize `translationFragment` using `sourceElement` to add functional | ||
* HTML attributes to children. `sourceElement` will have all its child nodes | ||
* removed. | ||
* | ||
* The sanitization is conducted according to the following rules: | ||
* | ||
* - Allow text nodes. | ||
* - Replace forbidden children with their textContent. | ||
* - Remove forbidden attributes from allowed children. | ||
* | ||
* Additionally when a child of the same type is present in `sourceElement` its | ||
* attributes will be merged into the translated child. Whitelisted attributes | ||
* of the translated child will then overwrite the ones present in the source. | ||
* | ||
* The overlay logic is subject to the following limitations: | ||
* | ||
* - Children are always cloned. Event handlers attached to them are lost. | ||
* - Nested HTML in source and in translations is not supported. | ||
* - Multiple children of the same type will be matched in order. | ||
* | ||
* @param {DocumentFragment} translationFragment | ||
* @param {Element} sourceElement | ||
* @returns {DocumentFragment} | ||
* @private | ||
*/ | ||
function sanitizeUsing(translationFragment, sourceElement) { | ||
const ownerDocument = translationFragment.ownerDocument; | ||
// Take one node from translationFragment at a time and check it against | ||
// the allowed list or try to match it with a corresponding element | ||
// in the source. | ||
let childElement; | ||
while ((childElement = translationElement.childNodes[0])) { | ||
translationElement.removeChild(childElement); | ||
for (const childNode of translationFragment.childNodes) { | ||
if (childElement.nodeType === childElement.TEXT_NODE) { | ||
result.appendChild(childElement); | ||
if (childNode.nodeType === childNode.TEXT_NODE) { | ||
continue; | ||
} | ||
const index = getIndexOfType(childElement); | ||
const sourceChild = getNthElementOfType(sourceElement, childElement, index); | ||
if (sourceChild) { | ||
// There is a corresponding element in the source, let's use it. | ||
overlay(sourceChild, childElement); | ||
result.appendChild(sourceChild); | ||
// If the child is forbidden just take its textContent. | ||
if (!isElementLocalizable(childNode)) { | ||
const text = ownerDocument.createTextNode(childNode.textContent); | ||
translationFragment.replaceChild(text, childNode); | ||
continue; | ||
} | ||
if (isElementAllowed(childElement)) { | ||
const sanitizedChild = childElement.ownerDocument.createElement( | ||
childElement.nodeName); | ||
overlay(sanitizedChild, childElement); | ||
result.appendChild(sanitizedChild); | ||
continue; | ||
// Start the sanitization with an empty element. | ||
const mergedChild = ownerDocument.createElement(childNode.localName); | ||
// Explicitly discard nested HTML by serializing childNode to a TextNode. | ||
mergedChild.textContent = childNode.textContent; | ||
// If a child of the same type exists in sourceElement, take its functional | ||
// (i.e. non-localizable) attributes. This also removes the child from | ||
// sourceElement. | ||
const sourceChild = shiftNamedElement(sourceElement, childNode.localName); | ||
// Find the union of all safe attributes: localizable attributes from | ||
// childNode and functional attributes from sourceChild. | ||
const safeAttributes = sanitizeAttrsUsing(childNode, sourceChild); | ||
for (const attr of safeAttributes) { | ||
mergedChild.setAttribute(attr.name, attr.value); | ||
} | ||
// Otherwise just take this child's textContent. | ||
result.appendChild( | ||
translationElement.ownerDocument.createTextNode( | ||
childElement.textContent)); | ||
translationFragment.replaceChild(mergedChild, childNode); | ||
} | ||
// Clear `sourceElement` and append `result` which by this time contains | ||
// `sourceElement`'s original children, overlayed with translation. | ||
// SourceElement might have been already modified by shiftNamedElement. | ||
// Let's clear it to make sure other code doesn't rely on random leftovers. | ||
sourceElement.textContent = ''; | ||
sourceElement.appendChild(result); | ||
// If we're overlaying a nested element, translate the allowed | ||
// attributes; top-level attributes are handled in `overlayElement`. | ||
// XXX Attributes previously set here for another language should be | ||
// cleared if a new language doesn't use them; https://bugzil.la/922577 | ||
if (translationElement.attributes) { | ||
for (k = 0, attr; (attr = translationElement.attributes[k]); k++) { | ||
if (isAttrAllowed(attr, sourceElement)) { | ||
sourceElement.setAttribute(attr.name, attr.value); | ||
} | ||
} | ||
return translationFragment; | ||
} | ||
/** | ||
* Sanitize and merge attributes. | ||
* | ||
* Only localizable attributes from the translated child element and only | ||
* functional attributes from the source child element are considered safe. | ||
* | ||
* @param {Element} translatedElement | ||
* @param {Element} sourceElement | ||
* @returns {Array<Attr>} | ||
* @private | ||
*/ | ||
function sanitizeAttrsUsing(translatedElement, sourceElement) { | ||
const localizedAttrs = Array.from(translatedElement.attributes).filter( | ||
attr => isAttrNameLocalizable(attr.name, translatedElement) | ||
); | ||
if (!sourceElement) { | ||
return localizedAttrs; | ||
} | ||
const functionalAttrs = Array.from(sourceElement.attributes).filter( | ||
attr => !isAttrNameLocalizable(attr.name, sourceElement) | ||
); | ||
return localizedAttrs.concat(functionalAttrs); | ||
} | ||
@@ -160,9 +210,5 @@ | ||
*/ | ||
function isElementAllowed(element) { | ||
const allowed = ALLOWED_ELEMENTS[element.namespaceURI]; | ||
if (!allowed) { | ||
return false; | ||
} | ||
return allowed.indexOf(element.tagName.toLowerCase()) !== -1; | ||
function isElementLocalizable(element) { | ||
const allowed = LOCALIZABLE_ELEMENTS[element.namespaceURI]; | ||
return allowed && allowed.includes(element.localName); | ||
} | ||
@@ -177,9 +223,17 @@ | ||
* | ||
* @param {{name: string}} attr | ||
* `explicitlyAllowed` can be passed as a list of attributes explicitly | ||
* allowed on this element. | ||
* | ||
* @param {string} name | ||
* @param {Element} element | ||
* @param {Array} explicitlyAllowed | ||
* @returns {boolean} | ||
* @private | ||
*/ | ||
function isAttrAllowed(attr, element) { | ||
const allowed = ALLOWED_ATTRIBUTES[element.namespaceURI]; | ||
function isAttrNameLocalizable(name, element, explicitlyAllowed = null) { | ||
if (explicitlyAllowed && explicitlyAllowed.includes(name)) { | ||
return true; | ||
} | ||
const allowed = LOCALIZABLE_ATTRIBUTES[element.namespaceURI]; | ||
if (!allowed) { | ||
@@ -189,7 +243,7 @@ return false; | ||
const attrName = attr.name.toLowerCase(); | ||
const elemName = element.tagName.toLowerCase(); | ||
const attrName = name.toLowerCase(); | ||
const elemName = element.localName; | ||
// Is it a globally safe attribute? | ||
if (allowed.global.indexOf(attrName) !== -1) { | ||
if (allowed.global.includes(attrName)) { | ||
return true; | ||
@@ -204,3 +258,3 @@ } | ||
// Is it allowed on this element? | ||
if (allowed[elemName].indexOf(attrName) !== -1) { | ||
if (allowed[elemName].includes(attrName)) { | ||
return true; | ||
@@ -221,15 +275,15 @@ } | ||
// Get n-th immediate child of context that is of the same type as element. | ||
// XXX Use querySelector(':scope > ELEMENT:nth-of-type(index)'), when: | ||
// 1) :scope is widely supported in more browsers and 2) it works with | ||
// DocumentFragments. | ||
function getNthElementOfType(context, element, index) { | ||
let nthOfType = 0; | ||
for (let i = 0, child; (child = context.children[i]); i++) { | ||
if (child.nodeType === child.ELEMENT_NODE && | ||
child.tagName.toLowerCase() === element.tagName.toLowerCase()) { | ||
if (nthOfType === index) { | ||
return child; | ||
} | ||
nthOfType++; | ||
/** | ||
* Remove and return the first child of the given type. | ||
* | ||
* @param {DOMFragment} element | ||
* @param {string} localName | ||
* @returns {Element | null} | ||
* @private | ||
*/ | ||
function shiftNamedElement(element, localName) { | ||
for (const child of element.children) { | ||
if (child.localName === localName) { | ||
element.removeChild(child); | ||
return child; | ||
} | ||
@@ -239,13 +293,1 @@ } | ||
} | ||
// Get the index of the element among siblings of the same type. | ||
function getIndexOfType(element) { | ||
let index = 0; | ||
let child; | ||
while ((child = element.previousElementSibling)) { | ||
if (child.tagName === element.tagName) { | ||
index++; | ||
} | ||
} | ||
return index; | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
41391
1
10
1116
2