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
+ } );
+ } );
+ } );
+ } );
} );