New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

fluent-dom

Package Overview
Dependencies
Maintainers
3
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fluent-dom - npm Package Compare versions

Comparing version 0.2.0 to 0.3.0

6

CHANGELOG.md

@@ -6,2 +6,8 @@ # Changelog

## fluent-dom 0.3.0
- Refactor the overlay sanitization methods into separate functions. (#189)
- Separate out CachedIterable and CachedAsyncIterable, and add a param to touchNext. (#191)
- Localization.formatValues should accept an array of objects. (#198)
- Allow adding and removing resource Ids from a Localization. (#197)
## fluent-dom 0.2.0

@@ -8,0 +14,0 @@

2

package.json
{
"name": "fluent-dom",
"version": "0.2.0",
"version": "0.3.0",
"description": "Fluent bindings for DOM",

@@ -5,0 +5,0 @@ "repository": {

@@ -48,4 +48,4 @@ import translateElement from "./overlay";

onLanguageChange() {
super.onLanguageChange();
onChange() {
super.onChange();
this.translateRoots();

@@ -318,11 +318,11 @@ }

* @param {Element} element
* @returns {Array<string, Object>}
* @returns {Object}
* @private
*/
getKeysForElement(element) {
return [
element.getAttribute(L10NID_ATTR_NAME),
JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null)
];
return {
id: element.getAttribute(L10NID_ATTR_NAME),
args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null)
};
}
}
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/* global console */
import { CachedIterable } from "../../fluent/src/index";
import { CachedAsyncIterable } from "../../fluent/src/index";

@@ -23,5 +23,16 @@ /**

this.generateMessages = generateMessages;
this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds));
this.ctxs =
new CachedAsyncIterable(this.generateMessages(this.resourceIds));
}
addResourceIds(resourceIds) {
this.resourceIds.push(...resourceIds);
this.onChange();
}
removeResourceIds(resourceIds) {
this.resourceIds = this.resourceIds.filter(r => !resourceIds.includes(r));
this.onChange();
}
/**

@@ -34,3 +45,3 @@ * Format translations and handle fallback if needed.

*
* @param {Array<Array>} keys - Translation keys to format.
* @param {Array<Object>} keys - Translation keys to format.
* @param {Function} method - Formatting function.

@@ -43,8 +54,3 @@ * @returns {Promise<Array<string|Object>>}

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;
}
for await (const ctx of this.ctxs) {
const missingIds = keysFromContext(method, ctx, keys, translations);

@@ -67,16 +73,16 @@

/**
* Format translations into {value, attrs} objects.
* Format translations into {value, attributes} objects.
*
* The fallback logic is the same as in `formatValues` but the argument type
* is stricter (an array of arrays) and it returns {value, attrs} objects
* which are suitable for the translation of DOM elements.
* is stricter (an array of arrays) and it returns {value, attributes}
* objects which are suitable for the translation of DOM elements.
*
* docL10n.formatMessages([
* ['hello', { who: 'Mary' }],
* ['welcome', undefined]
* {id: 'hello', args: { who: 'Mary' }},
* {id: 'welcome'}
* ]).then(console.log);
*
* // [
* // { value: 'Hello, Mary!', attrs: null },
* // { value: 'Welcome!', attrs: { title: 'Hello' } }
* // { value: 'Hello, Mary!', attributes: null },
* // { value: 'Welcome!', attributes: { title: 'Hello' } }
* // ]

@@ -86,4 +92,4 @@ *

*
* @param {Array<Array>} keys
* @returns {Promise<Array<{value: string, attrs: Object}>>}
* @param {Array<Object>} keys
* @returns {Promise<Array<{value: string, attributes: Object}>>}
* @private

@@ -102,5 +108,5 @@ */

* docL10n.formatValues([
* ['hello', { who: 'Mary' }],
* ['hello', { who: 'John' }],
* ['welcome']
* {id: 'hello', args: { who: 'Mary' }},
* {id: 'hello', args: { who: 'John' }},
* {id: 'welcome'}
* ]).then(console.log);

@@ -112,3 +118,3 @@ *

*
* @param {Array<Array>} keys
* @param {Array<Object>} keys
* @returns {Promise<Array<string>>}

@@ -143,3 +149,3 @@ */

async formatValue(id, args) {
const [val] = await this.formatValues([[id, args]]);
const [val] = await this.formatValues([{id, args}]);
return val;

@@ -149,3 +155,3 @@ }

handleEvent() {
this.onLanguageChange();
this.onChange();
}

@@ -157,4 +163,5 @@

*/
onLanguageChange() {
this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds));
onChange() {
this.ctxs =
new CachedAsyncIterable(this.generateMessages(this.resourceIds));
}

@@ -187,3 +194,3 @@ }

/**
* Format all public values of a message into a { value, attrs } object.
* Format all public values of a message into a {value, attributes} object.
*

@@ -197,3 +204,3 @@ * This function is passed as a method to `keysFromContext` and resolve

* If the function fails to retrieve the entity, the value is set to the ID of
* an entity, and attrs to `null`. If formatting fails, it will return
* an entity, and attributes to `null`. If formatting fails, it will return
* a partially resolved value and attributes.

@@ -258,3 +265,3 @@ *

* @param {Array<string>} keys
* @param {{Array<{value: string, attrs: Object}>}} translations
* @param {{Array<{value: string, attributes: Object}>}} translations
*

@@ -268,3 +275,3 @@ * @returns {Set<string>}

keys.forEach((key, i) => {
keys.forEach(({id, args}, i) => {
if (translations[i] !== undefined) {

@@ -274,8 +281,8 @@ return;

if (ctx.hasMessage(key[0])) {
if (ctx.hasMessage(id)) {
messageErrors.length = 0;
translations[i] = method(ctx, messageErrors, key[0], key[1]);
translations[i] = method(ctx, messageErrors, id, args);
// XXX: Report resolver errors
} else {
missingIds.add(key[0]);
missingIds.add(id);
}

@@ -282,0 +289,0 @@ });

@@ -65,3 +65,3 @@ /* eslint no-console: ["error", {allow: ["warn"]}] */

export default function translateElement(element, translation) {
const value = translation.value;
const {value} = translation;

@@ -95,15 +95,37 @@ if (typeof value === "string") {

*
* @param {DocumentFragment} fromElement - The source of children to overlay.
* @param {DocumentFragment} fromFragment - The source of children to overlay.
* @param {Element} toElement - The target of the overlay.
* @private
*/
function overlayChildNodes(fromElement, toElement) {
const content = toElement.ownerDocument.createDocumentFragment();
function overlayChildNodes(fromFragment, toElement) {
for (const childNode of fromFragment.childNodes) {
if (childNode.nodeType === childNode.TEXT_NODE) {
// Keep the translated text node.
continue;
}
for (const childNode of fromElement.childNodes) {
content.appendChild(sanitizeUsing(toElement, childNode));
if (childNode.hasAttribute("data-l10n-name")) {
const sanitized = namedChildFrom(toElement, childNode);
fromFragment.replaceChild(sanitized, childNode);
continue;
}
if (isElementAllowed(childNode)) {
const sanitized = allowedChild(childNode);
fromFragment.replaceChild(sanitized, childNode);
continue;
}
console.warn(
`An element of forbidden type "${childNode.localName}" was found in ` +
"the translation. Only safe text-level elements and elements with " +
"data-l10n-name are allowed."
);
// If all else fails, replace the element with its text content.
fromFragment.replaceChild(textNode(childNode), childNode);
}
toElement.textContent = "";
toElement.appendChild(content);
toElement.appendChild(fromFragment);
}

@@ -150,74 +172,78 @@

/**
* Sanitize a child node created by the translation.
* Sanitize a child element created by the translation.
*
* 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.
* 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.
*
* 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 <template> containing the
* translation, which in turn breaks the for…of iteration over its child nodes.
*
* @param {Element} sourceElement - The source for data-l10n-name lookups.
* @param {Element} childNode - The child node to be sanitized.
* @param {Element} translatedChild - The translated child to be sanitized.
* @returns {Element}
* @private
*/
function sanitizeUsing(sourceElement, childNode) {
if (childNode.nodeType === childNode.TEXT_NODE) {
return childNode.cloneNode(false);
}
function namedChildFrom(sourceElement, translatedChild) {
const childName = translatedChild.getAttribute("data-l10n-name");
const sourceChild = sourceElement.querySelector(
`[data-l10n-name="${childName}"]`
);
if (childNode.hasAttribute("data-l10n-name")) {
const childName = childNode.getAttribute("data-l10n-name");
const sourceChild = sourceElement.querySelector(
`[data-l10n-name="${childName}"]`
if (!sourceChild) {
console.warn(
`An element named "${childName}" wasn't found in the source.`
);
if (!sourceChild) {
console.warn(
`An element named "${childName}" wasn't found in the source.`
);
} else if (sourceChild.localName !== childNode.localName) {
console.warn(
`An element named "${childName}" was found in the translation ` +
`but its type ${childNode.localName} didn't match the element ` +
`found in the source (${sourceChild.localName}).`
);
} else {
// Remove it from sourceElement so that the translation cannot use
// the same reference name again.
sourceElement.removeChild(sourceChild);
// We can't currently guarantee that a translation won't remove
// sourceChild from the element completely, which could break the app if
// it relies on an event handler attached to the sourceChild. Let's make
// this limitation explicit for now by breaking the identitiy of the
// sourceChild by cloning it. This will destroy all event handlers
// attached to sourceChild via addEventListener and via on<name>
// properties.
const clone = sourceChild.cloneNode(false);
return shallowPopulateUsing(childNode, clone);
}
return textNode(translatedChild);
}
if (isElementAllowed(childNode)) {
// Start with an empty element of the same type to remove nested children
// and non-localizable attributes defined by the translation.
const clone = childNode.ownerDocument.createElement(childNode.localName);
return shallowPopulateUsing(childNode, clone);
if (sourceChild.localName !== translatedChild.localName) {
console.warn(
`An element named "${childName}" was found in the translation ` +
`but its type ${translatedChild.localName} didn't match the ` +
`element found in the source (${sourceChild.localName}).`
);
return textNode(translatedChild);
}
console.warn(
`An element of forbidden type "${childNode.localName}" was found in ` +
"the translation. Only elements with data-l10n-name can be overlaid " +
"onto source elements of the same data-l10n-name."
);
// Remove it from sourceElement so that the translation cannot use
// the same reference name again.
sourceElement.removeChild(sourceChild);
// We can't currently guarantee that a translation won't remove
// sourceChild from the element completely, which could break the app if
// it relies on an event handler attached to the sourceChild. Let's make
// this limitation explicit for now by breaking the identitiy of the
// sourceChild by cloning it. This will destroy all event handlers
// attached to sourceChild via addEventListener and via on<name>
// properties.
const clone = sourceChild.cloneNode(false);
return shallowPopulateUsing(translatedChild, clone);
}
// If all else fails, convert the element to its text content.
return childNode.ownerDocument.createTextNode(childNode.textContent);
/**
* 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.
*
* @param {Element} element - The element to be sanitized.
* @returns {Element}
* @private
*/
function allowedChild(element) {
// Start with an empty element of the same type to remove nested children
// and non-localizable attributes defined by the translation.
const clone = element.ownerDocument.createElement(element.localName);
return shallowPopulateUsing(element, clone);
}
/**
* Convert an element to a text node.
*
* @param {Element} element - The element to be sanitized.
* @returns {Node}
* @private
*/
function textNode(element) {
return element.ownerDocument.createTextNode(element.textContent);
}
/**
* Check if element is allowed in the translation.

@@ -224,0 +250,0 @@ *

@@ -5,3 +5,3 @@ import assert from "assert";

function* mockGenerateMessages(resourceIds) {
async function* mockGenerateMessages(resourceIds) {
const mc = new MessageContext(["en-US"]);

@@ -8,0 +8,0 @@ mc.addMessages("key1 = Key 1");

@@ -10,3 +10,3 @@ import assert from 'assert';

value: 'FOO <em>BAR</em> BAZ',
attrs: null
attributes: null
};

@@ -22,3 +22,3 @@

value: 'FOO <img src="img.png" />',
attrs: null
attributes: null
};

@@ -34,3 +34,3 @@

value: 'FOO <button>BUTTON</button>',
attrs: null
attributes: null
};

@@ -46,3 +46,3 @@

value: 'FOO <em><strong>BAR</strong></em> BAZ',
attrs: null
attributes: null
};

@@ -60,3 +60,3 @@

value: 'FOO <em title="BAR">BAR</em>',
attrs: null,
attributes: null,
};

@@ -73,3 +73,3 @@

value: 'FOO <em class="BAR" title="BAR">BAR</em>',
attrs: null,
attributes: null,
};

@@ -87,3 +87,3 @@

value: '<em>FOO</em>',
attrs: null
attributes: null
};

@@ -90,0 +90,0 @@

@@ -5,3 +5,3 @@ import assert from "assert";

function* mockGenerateMessages(resourceIds) {
async function* mockGenerateMessages(resourceIds) {
const mc = new MessageContext(["en-US"]);

@@ -15,3 +15,3 @@ mc.addMessages("key1 = Key 1");

const loc = new Localization(["test.ftl"], mockGenerateMessages);
const translations = await loc.formatMessages([["key1"]]);
const translations = await loc.formatMessages([{id: "key1"}]);

@@ -23,3 +23,3 @@ assert.equal(translations[0].value, "Key 1");

const loc = new Localization(["test.ftl"], mockGenerateMessages);
const translations = await loc.formatMessages([["missing_key"]]);
const translations = await loc.formatMessages([{id: "missing_key"}]);

@@ -26,0 +26,0 @@ // Make sure that the returned value here is `undefined`.

@@ -32,3 +32,3 @@ import assert from 'assert';

element.innerHTML,
'<em title="FOO">FOO</em>'
'FOO'
);

@@ -35,0 +35,0 @@ });

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc