Skip to content

Instantly share code, notes, and snippets.

@ephys
Last active June 2, 2018 13:24
Show Gist options
  • Save ephys/e70c35ed597f08509a9016368d2ae3ab to your computer and use it in GitHub Desktop.
Save ephys/e70c35ed597f08509a9016368d2ae3ab to your computer and use it in GitHub Desktop.
An extension to React-Intl to format messages that contain XML
// @flow
// V2 on NPM fixes notes below
// https://github.com/Ephys/react-intl-formatted-xml-message
// NOTE: Currently with this implementation, XML present in the variables you pass to FormattedMessageTag will also be converted to react components.
// I'm working on a way to fix this issue. Right now I think that'll mean re-implementing FormattedMessage so the XML parsing part skips {variable} tags
// TODO:
// Replace all object or string values with placeholder tags
// when mapping, if node = text node, search for placeholder tags in it
// and replace these tags with the actual values.
import React from 'react';
import { FormattedMessage } from 'react-intl';
// on node, you need a DOMParser polyfill (https://github.com/jsdom/jsdom/issues/1368 or https://www.npmjs.com/package/xmldom or https://www.npmjs.com/package/dom-parser)
const parser = new DOMParser();
type Props = {
id: string,
defaultMessage: string,
tags: { [string]: any },
};
/**
* Similar to FormattedMessage but allows inserting XML tag in the formatted message
* that will mapped to react elements when rendering.
*
* The "tags" property of the props object is provides the mapping.
* Allowed replacements are:
* - Strings or React Components: New instances of these components will be created and used as replacement.
* - React elements: These elements will be cloned and their props will be merged with the ones provided in the formatted text, with the latter taking precedence.
*/
export default function FormattedMessageTag(props: Props) {
const { tags, ...formattedMessageProps } = props;
return (
<FormattedMessage {...formattedMessageProps}>
{localizedMessage => {
const xmlMessage = `<?xml version="1.0" ?><root>${localizedMessage}</root>`;
// we force XML (not html) so it's easier to parse
const doc = parser.parseFromString(xmlMessage, 'text/xml');
const root = doc.children[0];
return xmlToJsx(Array.from(root.childNodes), tags);
}}
</FormattedMessage>
);
}
function getMappedTag(node: Node, replacements) {
if (hasOwnProperty(replacements, node.nodeName)) {
return replacements[node.nodeName];
}
return node.nodeName;
}
function xmlAttributesToJsx(attributeList) {
const jsxAttributes = {};
for (let i = 0; i < attributeList.length; i++) {
const attribute = attributeList[i];
jsxAttributes[attribute.name] = attribute.value;
}
return jsxAttributes;
}
function xmlToJsx(nodes: Node[], replacements) {
return nodes.map((node, i) => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
if (node.nodeType !== Node.ELEMENT_NODE) {
console.error('Only Text and Element nodes are supported.', node);
return null;
}
const nodeReplacement = getMappedTag(node, replacements);
const attributes = xmlAttributesToJsx(node.attributes);
// if anyone has a better idea for a key here, feedback would be highly appreciated!
// Although it should not matter as the order is never going to change.
attributes.key = i;
let children = xmlToJsx(Array.from(node.childNodes), replacements);
// some tags, such as <br />, cannot have any children. Even if it's an empty array.
if (children.length === 0) {
children = null;
}
// replacement is a Component, make a new instance of it.
if (typeof nodeReplacement === 'function' || typeof nodeReplacement === 'string') {
// JSX elements must start with an uppercase letter if they are a variable.
const Tag = nodeReplacement;
return <Tag {...attributes}>{children}</Tag>;
}
// replacement is an instantiated react node. Merge props (formatted text takes precedence).
if (React.isValidElement(nodeReplacement)) {
return React.cloneElement(nodeReplacement, attributes, children);
}
// is invalid
console.error('Invalid replacement: Must be a tag name, a Component, or a React Element', nodeReplacement);
return null;
});
}
function hasOwnProperty(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
By registering on our service, you <em>agree</em> to our <a href="https://example.com" rel="" target="_blank" data-event="click-tos">Terms of Service and Privacy Policy</a>
import React from 'react';
import FormattedMessageTag from './FormattedMessageTag';
export default function TermsOfService() {
return (
<FormattedMessageTag
id: 'tos',
defaultMessage: 'By registering on our service, you <em>agree</em> to our <tos-link target="_blank" data-event="click-tos" rel="">Terms of Service and Privacy Policy</tos-link>',
tags={{
'tos-link': <a href="https://example.com" rel="noreferrer noopener" />
}}
/>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment