Last active
June 2, 2018 13:24
-
-
Save ephys/e70c35ed597f08509a9016368d2ae3ab to your computer and use it in GitHub Desktop.
An extension to React-Intl to format messages that contain XML
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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