diff --git a/README.md b/README.md index 2ee82ce..fdeef88 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Interpolate-Components takes a single options object as an argument and returns - **mixedString** A string that contains component tokens to be interpolated - **components** An object with components assigned to named attributes +- **tags** (optional) An object with custom tag syntax to be used - **throwErrors** (optional) 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. ## Component tokens @@ -37,6 +38,20 @@ const jsxExample =

{ children }

; //

This is a fine example.

``` +## Custom tag syntax + +```js +interpolateComponents( { + mixedString: 'This uses <>custom<> syntax <>', + components: { em: , icon: }, + tags: { + componentOpen: [ '<<', '>>' ], + componentClose: [ '<>' ], + componentSelfClosing: [ '<<', '/>>' ], + } +} ); +``` + ## Testing ```sh # install dependencies diff --git a/package.json b/package.json index 00253f3..a487fce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interpolate-components", - "version": "1.1.1", + "version": "1.2.1", "description": "Convert strings into structured React components.", "repository": { "type": "git", diff --git a/src/index.es6 b/src/index.es6 index f0d86f1..d10f6b3 100644 --- a/src/index.es6 +++ b/src/index.es6 @@ -9,6 +9,12 @@ import createFragment from 'react-addons-create-fragment'; */ import tokenize from './tokenize'; +const DEFAULT_TAGS = { + componentOpen: ['{{', '}}'], + componentClose: ['{{/', '}}'], + componentSelfClosing: ['{{', '/}}'], +}; + let currentMixedString; function getCloseIndex( openIndex, tokens ) { @@ -95,7 +101,7 @@ function buildChildren( tokens, components ) { } function interpolate( options ) { - const { mixedString, components, throwErrors } = options; + const { mixedString, components, throwErrors, tags = DEFAULT_TAGS } = options; currentMixedString = mixedString; @@ -111,7 +117,15 @@ function interpolate( options ) { return mixedString; } - let tokens = tokenize( mixedString ); + if ( typeof tags !== 'object' || ! tags.componentOpen || ! tags.componentClose || ! tags.componentSelfClosing ) { + if ( throwErrors ) { + throw new Error( `Interpolation Error: unable to process \`${ mixedString }\` because tags is invalid` ); + } + + return mixedString; + } + + let tokens = tokenize( mixedString, tags ); try { return buildChildren( tokens, components ); diff --git a/src/tokenize.es6 b/src/tokenize.es6 index 997531e..6d70449 100644 --- a/src/tokenize.es6 +++ b/src/tokenize.es6 @@ -1,32 +1,59 @@ -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, '' ) - }; +const TOKEN_TYPES = [ 'componentClose', 'componentSelfClosing', 'componentOpen' ]; + +function identifyToken( item, regExps ) { + for ( let i = 0; i < TOKEN_TYPES.length; i++ ) { + const type = TOKEN_TYPES[ i ]; + const match = item.match( regExps[ type ] ); + + if ( match ) { + return { + type: type, + value: match[ 1 ] + }; + } } + 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 ); +/** + * @param {string} str + * @returns {string} + */ +function escapeRegExp( str ) { + return str.replace( /[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&' ); +} + +/** + * @param {string[]} tag + * @param {boolean} match + * @returns {RegExp} + */ +function makeRegExp( tag, match ) { + const [ start, end ] = tag; + const inner = match ? '\\s*(\\w+)\\s*' : '\\s*\\w+\\s*'; + return new RegExp( `${escapeRegExp( start )}${inner}${escapeRegExp( end )}` ); +} + +module.exports = function( mixedString, tags ) { + // create regular expression that matches all components + const combinedRegExpString = TOKEN_TYPES + .map( type => tags[ type ] ) + .map( tag => makeRegExp( tag, false ).source ) + .join( '|' ); + const combinedRegExp = new RegExp( `(${combinedRegExpString})`, 'g' ); + + // split to components and strings + const tokenStrings = mixedString.split( combinedRegExp ); + + // create regular expressions for identifying tokens + const componentRegExps = {}; + TOKEN_TYPES.forEach( type => { + componentRegExps[ type ] = makeRegExp( tags[ type ], true ); + }); + + return tokenStrings.map( (tokenString) => identifyToken( tokenString, componentRegExps ) ); }; diff --git a/test/test.jsx b/test/test.jsx index 5b6f220..32fc03b 100644 --- a/test/test.jsx +++ b/test/test.jsx @@ -253,4 +253,48 @@ describe( 'interpolate-components', () => { assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); } ); } ); + + describe( 'custom tags', () => { + it( 'should allow custom tags', () => { + const expectedResultString = '
test
'; + const interpolatedComponent = interpolateComponents( { + mixedString: '<
>test<
> <>', + components: { + div: div, + input: input + }, + tags: { + componentOpen: [ '<<', '>>' ], + componentClose: [ '<>' ], + componentSelfClosing: [ '<<', '/>>' ], + } + } ); + const instance = { interpolatedComponent }; + assert.equal( expectedResultString, ReactDomServer.renderToStaticMarkup( instance ) ); + } ); + + it( 'should throw if tags is not an object', () => { + assert.throws( () => { + interpolateComponents( { + mixedString: 'test', + components: {}, + tags: '{{', + throwErrors: true + } ); + } ); + } ); + + it( 'should throw if not all tags are provided', () => { + assert.throws( () => { + interpolateComponents( { + mixedString: 'test', + components: {}, + tags: { + componentOpen: [ '{{', '}}' ], + }, + throwErrors: true + } ); + } ); + } ); + } ); } );