diff --git a/client/lib/interpolate-components/Makefile b/client/lib/interpolate-components/Makefile deleted file mode 100644 index b68d291769d5c1..00000000000000 --- a/client/lib/interpolate-components/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -REPORTER ?= spec -MOCHA ?= ../../../node_modules/.bin/mocha - -test: - @NODE_ENV=test NODE_PATH=test:../../../client $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER) - -.PHONY: test diff --git a/client/lib/interpolate-components/README.md b/client/lib/interpolate-components/README.md deleted file mode 100644 index f0ebeb1d5a2ead..00000000000000 --- a/client/lib/interpolate-components/README.md +++ /dev/null @@ -1,40 +0,0 @@ -Interpolate-Components -====================== - -The Interpolate-Components module takes an options object that includes: - -### mixedString -A string that contains component tokens to be interpolated -### components -An object with components assigned to named attributes -### throwErrors -Whether errors should be thrown (as in pre-production environments) or we should more gracefully return the un-interpolated original string (as in production). This is optional and is false by default. - -The Interpolate-Components module takes these input options and returns a child object containing a mix of strings and components that can be injected into a React element. This allows us to mix markup and content in a string without having to inject that markup using `_dangerouslySetInnerHTML()`. This is particularly useful for translation purposes, where it hinders translation to separate markup from content. - -Component tokens are strings (containing letters, numbers, or underscores only) wrapped inside double-curly braces and have an opening, closing, and self-closing syntax, similar to html. - -```js -var example = '{{link}}opening and closing syntax example{{/link}}', - example2 = 'Here is a self-closing example: {{input/}}'; -``` - -Example usage: - -```js -/** @jsx React.DOM */ - -var interpolateComponents = require( 'lib/interpolate-components' ), - example = 'This is a {{em}}fine{{/em}} example.', - components = { - em: - }, - children = interpolateComponents( { - mixedString: example, - components; components - } ), - jsxExample =

{ children }

; - -// will render as: -//

This is a fine example.

-``` diff --git a/client/lib/interpolate-components/index.js b/client/lib/interpolate-components/index.js deleted file mode 100644 index f0d86f19f83b83..00000000000000 --- a/client/lib/interpolate-components/index.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * External Dependencies - */ -import React from 'react'; -import createFragment from 'react-addons-create-fragment'; - -/** - * Internal Dependencies - */ -import tokenize from './tokenize'; - -let currentMixedString; - -function getCloseIndex( openIndex, tokens ) { - var openToken = tokens[ openIndex ], - nestLevel = 0, - token, i; - for ( i = openIndex + 1; i < tokens.length; i++ ) { - token = tokens[ i ]; - if ( token.value === openToken.value ) { - if ( token.type === 'componentOpen' ) { - nestLevel++; - continue; - } - if ( token.type === 'componentClose' ) { - if ( nestLevel === 0 ) { - return i; - } - nestLevel--; - } - } - } - // if we get this far, there was no matching close token - throw new Error( 'Missing closing component token `' + openToken.value + '`' ); -} - -function buildChildren( tokens, components ) { - var children = [], - childrenObject = {}, - openComponent, clonedOpenComponent, openIndex, closeIndex, token, i, grandChildTokens, grandChildren, siblingTokens, siblings; - - for ( i = 0; i < tokens.length; i++ ) { - token = tokens[ i ]; - if ( token.type === 'string' ) { - children.push( token.value ); - continue; - } - // component node should at least be set - if ( ! components.hasOwnProperty( token.value ) || typeof components[ token.value ] === 'undefined' ) { - throw new Error( 'Invalid interpolation, missing component node: `' + token.value + '`' ); - } - // should be either ReactElement or null (both type "object"), all other types deprecated - if ( typeof components[ token.value ] !== 'object' ) { - throw new Error( 'Invalid interpolation, component node must be a ReactElement or null: `' + token.value + '`', '\n> ' + currentMixedString ); - } - // we should never see a componentClose token in this loop - if ( token.type === 'componentClose' ) { - throw new Error( 'Missing opening component token: `' + token.value + '`' ); - } - if ( token.type === 'componentOpen' ) { - openComponent = components[ token.value ]; - openIndex = i; - break; - } - // componentSelfClosing token - children.push( components[ token.value ] ); - continue; - } - - if ( openComponent ) { - closeIndex = getCloseIndex( openIndex, tokens ); - grandChildTokens = tokens.slice( ( openIndex + 1 ), closeIndex ); - grandChildren = buildChildren( grandChildTokens, components ); - clonedOpenComponent = React.cloneElement( openComponent, {}, grandChildren ); - children.push( clonedOpenComponent ); - - if ( closeIndex < tokens.length - 1 ) { - siblingTokens = tokens.slice( closeIndex + 1 ); - siblings = buildChildren( siblingTokens, components ); - children = children.concat( siblings ); - } - } - - if ( children.length === 1 ) { - return children[ 0 ]; - } - - children.forEach( ( child, index ) => { - if ( child ) { - childrenObject[ `interpolation-child-${index}` ] = child; - } - } ); - - return createFragment( childrenObject ); -} - -function interpolate( options ) { - const { mixedString, components, throwErrors } = options; - - currentMixedString = mixedString; - - if ( ! components ) { - return mixedString; - } - - if ( typeof components !== 'object' ) { - if ( throwErrors ) { - throw new Error( `Interpolation Error: unable to process \`${ mixedString }\` because components is not an object` ); - } - - return mixedString; - } - - let tokens = tokenize( mixedString ); - - try { - return buildChildren( tokens, components ); - } catch ( error ) { - if ( throwErrors ) { - throw new Error( `Interpolation Error: unable to process \`${ mixedString }\` because of error \`${ error.message }\`` ); - } - - return mixedString; - } -}; - -export default interpolate; diff --git a/client/lib/interpolate-components/test/test.jsx b/client/lib/interpolate-components/test/test.jsx deleted file mode 100644 index 9cf76744718372..00000000000000 --- a/client/lib/interpolate-components/test/test.jsx +++ /dev/null @@ -1,259 +0,0 @@ -/** - * External dependencies - */ -import assert from 'assert'; -import ReactDomServer from 'react-dom/server'; -import React from 'react'; - -/** - * Internal dependencies - */ -import interpolateComponents from 'lib/interpolate-components'; - -describe( 'interpolate-components', () => { - const input = React.DOM.input(); - const div = React.DOM.div(); - const link = ; - const em = ; - const CustomComponentClass = React.createClass( { - displayName: 'CustomComponentClass', - render() { - return { this.props.intro }{ this.props.children }; - } - } ); - - describe( 'with default container', () => { - it( 'should return a react object with a span container', () => { - const expectedResultString = 'testtest'; - const interpolatedResult = interpolateComponents( { - mixedString: 'test{{input/}}test', - components: { - input: input - } - } ); - const instance = { interpolatedResult }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - it( 'should allow whitespace in the component placeholder', () => { - const expectedResultString = 'testtest'; - const interpolatedResult = interpolateComponents( { - mixedString: 'test{{ input /}}test', - components: { - input: input - } - } ); - const instance = { interpolatedResult }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - it( 'should not add extra span nodes if component is at end of string', () => { - const expectedResultString = 'test'; - const interpolatedResult = interpolateComponents( { - mixedString: 'test{{ input /}}', - components: { - input: input - } - } ); - const instance = { interpolatedResult }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - it( 'should not throw when component node is null', () => { - assert.doesNotThrow( () => { - interpolateComponents( { - mixedString: 'test{{input/}}test', - components: { - input: null - }, - throwErrors: true - } ); - } ); - } ); - it( 'should not throw when component node is not an object', () => { - assert.doesNotThrow( () => { - interpolateComponents( { - mixedString: 'test{{input/}}test{{input2/}}', - components: { - input: 'string', - input2: 123 - }, - } ); - } ); - } ); - it( 'should return original string when component node is not an object', () => { - const expectedResultString = 'test{{input/}}test{{input2/}}'; - const interpolatedResult = interpolateComponents( { - mixedString: 'test{{input/}}test{{input2/}}', - components: { - input: 'string', - input2: 123 - } - } ); - const instance = { interpolatedResult }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - - describe( 'when allowing thrown errors', () => { - it( 'should throw when component node is not set', () => { - assert.throws( () => { - interpolateComponents( { - mixedString: 'test{{input/}}', - components: { - mismatch: true - }, - throwErrors: true - } ); - } ); - } ); - it( 'should throw when component node is undefined', () => { - assert.throws( () => { - interpolateComponents( { - mixedString: 'test{{input/}}', - components: { - input: undefined - }, - throwErrors: true - } ); - } ); - } ); - it( 'should throw error on invalid cross-nesting', () => { - assert.throws( () => { - interpolateComponents( { - mixedString: '{{link}}a{{em}}b{{/link}}c{{/em}}', - components: { - link: link, - em: em - }, - throwErrors: true - } ); - } ); - } ); - it( 'should throw when component is unclosed', () => { - assert.throws( () => { - interpolateComponents( { - mixedString: '{{link}}test', - components: { - link: link - }, - throwErrors: true - } ); - } ); - } ); - } ); - - describe( 'when not allowing thrown errors', () => { - it( 'should return original string when component node is not set', () => { - const mixedString = 'test{{input/}}'; - const results = interpolateComponents( { - mixedString: mixedString, - components: { - mismatch: true - } - } ); - assert.equal( mixedString, results ); - } ); - it( 'should return original string when component node is undefined', () => { - const mixedString = 'test{{input/}}'; - const results = interpolateComponents( { - mixedString: mixedString, - components: { - input: undefined - } - } ); - assert.equal( mixedString, results ); - } ); - it( 'should return original string on invalid cross-nesting', () => { - const mixedString = '{{link}}a{{em}}b{{/link}}c{{/em}}'; - const results = interpolateComponents( { - mixedString: mixedString, - components: { - link: link, - em: em - } - } ); - assert.equal( mixedString, results ); - } ); - it( 'should return original string when component is unclosed', () => { - const mixedString = '{{link}}test'; - const results = interpolateComponents( { - mixedString: mixedString, - components: { - link: link - } - } ); - assert.equal( mixedString, results ); - } ); - } ); - } ); - - describe( 'with components that wrap content', () => { - it( 'should wrap the component around the inner contents', () => { - const expectedResultString = 'test link content test'; - const interpolatedComponent = interpolateComponents( { - mixedString: 'test {{link}}link content{{/link}} test', - components: { - link: link - } - } ); - const instance = { interpolatedComponent }; - - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - it( 'should handle multiple wrapping components', () => { - const expectedResultString = 'test link content link content2 test'; - const interpolatedComponent = interpolateComponents( { - mixedString: 'test {{link}}link content{{/link}} {{em}}link content2{{/em}} test', - components: { - link: link, - em: em - } - } ); - const instance = { interpolatedComponent }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - it( 'should handle nested wrapping components', () => { - const expectedResultString = 'test link content emphasis test'; - const interpolatedComponent = interpolateComponents( { - mixedString: 'test {{link}}link content {{em}}emphasis{{/em}}{{/link}} test', - components: { - link: link, - em: em - } - } ); - const instance = { interpolatedComponent }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - it( 'should work with custom components', () => { - const expectedResultString = 'here is: baba Hey!willie'; - const interpolatedComponent = interpolateComponents( { - mixedString: 'here is: {{x}}baba {{y}}willie{{/y}}{{/x}}', - components: { - x: , - y: - } - } ); - const instance = { interpolatedComponent }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - it( 'should allow repeated component tokens', () => { - const expectedResultString = 'babadyado'; - const interpolatedComponent = interpolateComponents( { - mixedString: '{{link}}baba{{/link}}{{link}}dyado{{/link}}', - components: { - link: link - } - } ); - const instance = { interpolatedComponent }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - it( 'should allow wrapping repeated components', () => { - const expectedResultString = '
baba
dyado
'; - const interpolatedComponent = interpolateComponents( { - mixedString: '{{div}}baba{{div}}dyado{{/div}}{{/div}}', - components: { - div: div - } - } ); - const instance = { interpolatedComponent }; - assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); - } ); - } ); -} ); diff --git a/client/lib/interpolate-components/tokenize.js b/client/lib/interpolate-components/tokenize.js deleted file mode 100644 index 997531e999fb3d..00000000000000 --- a/client/lib/interpolate-components/tokenize.js +++ /dev/null @@ -1,32 +0,0 @@ -function identifyToken( item ) { - // {{/example}} - if ( item.match( /^\{\{\// ) ) { - return { - type: 'componentClose', - value: item.replace( /\W/g, '' ) - }; - } - // {{example /}} - if ( item.match( /\/\}\}$/ ) ) { - return { - type: 'componentSelfClosing', - value: item.replace( /\W/g, '' ) - }; - } - // {{example}} - if ( item.match( /^\{\{/ ) ) { - return { - type: 'componentOpen', - value: item.replace( /\W/g, '' ) - }; - } - return { - type: 'string', - value: item - }; -} - -module.exports = function( mixedString ) { - const tokenStrings = mixedString.split( /(\{\{\/?\s*\w+\s*\/?\}\})/g ); // split to components and strings - return tokenStrings.map( identifyToken ); -}; diff --git a/client/lib/mixins/i18n/README.md b/client/lib/mixins/i18n/README.md index e3422e0467f1fc..6b2334b3822508 100644 --- a/client/lib/mixins/i18n/README.md +++ b/client/lib/mixins/i18n/README.md @@ -103,7 +103,7 @@ this.translate( 'My %s has 3 corners', { Because React tracks DOM nodes in the virtual DOM for rendering purposes, you cannot use string substitution with html markup as you might in a php scenario, because we don't render arbitrary html into the page, we are creating a virtual DOM in React. -Instead we use the [interpolate-components module](../../lib/interpolate-components) to inject components into the string using a component token as a placeholder in the string and a components object, similar to how string substitution works. The result of the `translate()` method can then be inserted as a child into another React component. Component tokens are strings (containing letters, numbers, or underscores only) wrapped inside double-curly braces and have an opening, closing, and self-closing syntax, similar to html. +Instead we use [interpolate-components](https://github.com/Automattic/interpolate-components) to inject components into the string using a component token as a placeholder in the string and a components object, similar to how string substitution works. The result of the `translate()` method can then be inserted as a child into another React component. Component tokens are strings (containing letters, numbers, or underscores only) wrapped inside double-curly braces and have an opening, closing, and self-closing syntax, similar to html. **NOTE: Always use a JSX element for passing components. Otherwise you will need to [wrap your React classes with `createFactory`](http://facebook.github.io/react/blog/2014/10/14/introducing-react-elements.html). Any wrapped content inside opening/closing component tokens will be inserted/replaced as the children of that component in the output. Component tokens must be unique.**: @@ -122,7 +122,7 @@ var example2 = this.translate( 'I feel {{em}}very{{/em}} strongly about this.', } } ); -// components can nest +// components can nest var example3 = this.translate( '{{a}}{{icon/}}click {{em}}here{{/em}}{{/a}} to see examples.', { components: { a: , @@ -147,7 +147,7 @@ var numHats = howManyHats(), // returns integer content = this.translate( 'My hat has three corners.', 'My hats have three corners.', - { + { count: numHats } ); diff --git a/client/lib/mixins/i18n/index.js b/client/lib/mixins/i18n/index.js index a2827b9f37b828..5ef6e5b60dc65c 100644 --- a/client/lib/mixins/i18n/index.js +++ b/client/lib/mixins/i18n/index.js @@ -13,7 +13,7 @@ var debug = require( 'debug' )( 'calypso:i18n' ), var config = require( 'config' ), numberFormatPHPJS = require( './number-format' ), emitter = require( 'lib/mixins/emitter' ), - interpolateComponents = require( 'lib/interpolate-components' ); + interpolateComponents = require( 'interpolate-components' ); /** * variables diff --git a/package.json b/package.json index 796c3cd928c0a6..c11dd146b3702b 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "immutable": "3.7.5", "imports-loader": "0.6.5", "inherits": "2.0.1", + "interpolate-components": "1.0.4", "jade": "jadejs/jade#29784fd", "jed": "1.0.2", "json-loader": "0.5.4", diff --git a/server/i18nlint/i18nlint.js b/server/i18nlint/i18nlint.js index 635dfcaf50f5d4..5d3361400cb115 100644 --- a/server/i18nlint/i18nlint.js +++ b/server/i18nlint/i18nlint.js @@ -9,7 +9,7 @@ var fs = require( 'fs' ), babel = require( 'babel-core' ), preProcessXGettextJSMatch = require( '../i18n/preprocess-xgettextjs-match.js' ), SourceMapConsumer = require( 'source-map' ).SourceMapConsumer, - tokenize = require( '../../client/lib/interpolate-components/tokenize.js' ), + tokenize = require( 'interpolate-components/lib/tokenize' ), contains = require( 'lodash' ).contains, flow = require( 'lodash' ).flow;