Skip to content

Commit

Permalink
refactor of parser
Browse files Browse the repository at this point in the history
- no longer matters what order
- all replacements must be either self-closing tags or enclosed with children.
- tag text can be arbitrary but must have a definition in the map.
  • Loading branch information
nerrad committed Nov 5, 2019
1 parent bf468ed commit 01a687e
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 39 deletions.
75 changes: 61 additions & 14 deletions packages/element/src/create-interpolate-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,8 @@ const getMatchFromString = (
return match;
}

// If config has property "value", then just return on search string
// since it is not a component.
const expression = getHasPropValue( conversionConfig ) ?
new RegExp( escapeRegExp( searchString ) ) :
getSelfClosingTagExpression( searchString );
return interpolatedString.match( expression );
// no children, so just return selfClosingTag match
return interpolatedString.match( getSelfClosingTagExpression( searchString ) );
};

// index for keys
Expand Down Expand Up @@ -149,6 +145,32 @@ const recursiveCreateElement = ( potentialElement, conversionMap ) => {
);
};

/**
* This reorders the conversionMap so that it's entries match the order of
* elements in the string.
*
* This is necessary because the parser is order sensitive due to the potential
* for nested elements (eg. <a>Some linked <em>and emphasized</em></a> string).
* This ensures that the parsing will be done correctly yet still allow for the
* consumer not to worry about order in the map.
*
* @param interpolatedString {string} The string being parsed.
* @param conversionMap {Array} The map being reordered
*
* @return {Array} The new map in the correct order for the tags in the string.
*/
const reorderMapByElementOrder = ( interpolatedString, conversionMap ) => {
// if length of map is only one then we can just return as is.
if ( conversionMap.length === 1 ) {
return conversionMap;
}
return conversionMap.sort( ( [ tagA ], [ tagB ] ) => {
tagA = `<${ tagA }`;
tagB = `<${ tagB }`;
return interpolatedString.indexOf( tagA ) > interpolatedString.indexOf( tagB );
} );
};

/**
* This function creates an interpolated element from a passed in string with
* specific tags matching how the string should be converted to an element via
Expand All @@ -157,36 +179,61 @@ const recursiveCreateElement = ( potentialElement, conversionMap ) => {
* @example
* For example, for the given string:
*
* "This is a <span%1>string</span%1> with <a%1>a link</a%1>, a self-closing
* %1$s tag and a plain value %2$s"
* "This is a <span>string</span> with <a>a link</a>, a self-closing
* <CustomComponentB/> tag and a plain value <custom value/>"
*
* You would have something like this as the conversionMap value:
*
* ```js
* {
* 'span%1': { tag: CustomComponent, props: {} },
* 'a%1': { tag: 'a', props: { href: 'https://github.com' } },
* '%1$s': { tag: CustomComponentB, props: {} },
* '%2$s': { value: 'custom value' },
* span: { tag: CustomComponent, props: {} },
* a: { tag: 'a', props: { href: 'https://github.com' } },
* CustomComponentB: { tag: CustomComponentB, props: {} },
* 'custom value': { value: 'custom value' },
* }
* ```
*
* @param {string} interpolatedString The interpolation string to be parsed.
* @param {Object} conversionMap The map used to convert the string to
* @param {Object} conversionMap The map used to convert the string to
* a react element.
*
* @return {Element} A react element.
*/
const createInterpolateElement = ( interpolatedString, conversionMap ) => {
keyIndex = -1;

if ( ! isValidConversionMap( conversionMap ) ) {
return interpolatedString;
}

// verify that the object isn't empty.
conversionMap = Object.entries( conversionMap );
if ( conversionMap.length === 0 ) {
return interpolatedString;
}

return createElement(
Fragment,
{},
recursiveCreateElement(
interpolatedString,
Object.entries( conversionMap )
reorderMapByElementOrder( interpolatedString, conversionMap )
),
);
};

/**
* Validate conversion map.
*
* A map is considered valid if it's an object and does not have length.
*
* @param conversionMap {Object} The map being validated.
*
* @return boolean True means the map is valid.
*/
const isValidConversionMap = ( conversionMap ) => {
return typeof conversionMap === 'object' &&
typeof conversionMap.length === 'undefined';
};

export default createInterpolateElement;
51 changes: 26 additions & 25 deletions packages/element/src/test/create-interpolate-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,38 @@ describe( 'createInterpolateElement', () => {
const testString = 'This is a string';
expect(
createInterpolateElement( testString, [] )
).toEqual( <Fragment>This is a string</Fragment> );
).toEqual( 'This is a string' );
} );
it( 'returns same string when there are no tokens in the string', () => {
const testString = 'This is a string';
expect(
createInterpolateElement(
testString,
{ '%1$s': { value: 10 } }
{ someValue: { value: 10 } }
)
).toEqual( <Fragment>{ testString }</Fragment> );
} );
it( 'returns same string when there is an invalid conversion map', () => {
const testString = 'This is a %1$s string';
const testString = 'This is a <someValue/> string';
expect(
createInterpolateElement(
testString,
[ '%1$s', { value: 10 } ],
[ 'someValue', { value: 10 } ],
)
).toEqual( <Fragment>{ testString }</Fragment> );
).toEqual( testString );
} );
it( 'returns same string when there is an invalid token in the string', () => {
const testString = 'This is a %1$s string';
it( 'returns same string when there is an non matching token in the ' +
'string', () => {
const testString = 'This is a <nonParsed/> string';
expect(
createInterpolateElement(
testString,
{ '%2$s': { value: 20 } }
{ someValue: { value: 20 } }
)
).toEqual( <Fragment>{ testString }</Fragment> );
} );
it( 'returns expected react element for non nested components', () => {
const testString = 'This is a string with <a%1>a link</a%1>.';
const testString = 'This is a string with <a>a link</a>.';
const expectedElement = createElement(
Fragment,
{},
Expand All @@ -56,15 +57,15 @@ describe( 'createInterpolateElement', () => {
expect( createInterpolateElement(
testString,
{
'a%1': {
a: {
tag: 'a',
props: { href: 'https://github.com', className: 'some_class' },
},
}
) ).toEqual( expectedElement );
} );
it( 'returns expected react element for nested components', () => {
const testString = 'This is a <a1>string that is <em1>linked</em1></a1>.';
const testString = 'This is a <a>string that is <em>linked</em></a>.';
const expectedElement = createElement(
Fragment,
{},
Expand All @@ -88,13 +89,13 @@ describe( 'createInterpolateElement', () => {
expect( createInterpolateElement(
testString,
{
a1: { tag: 'a', props: {} },
em1: { tag: 'em', props: {} },
a: { tag: 'a', props: {} },
em: { tag: 'em', props: {} },
}
) ).toEqual( expectedElement );
} );
it( 'returns a value for a prop value type token replacement', () => {
const testString = 'This is a string with a value token: %1$s';
const testString = 'This is a string with a value token: <someValue/>';
const expectedElement = createElement(
Fragment,
{},
Expand All @@ -105,15 +106,15 @@ describe( 'createInterpolateElement', () => {
);
expect( createInterpolateElement(
testString,
{ '%1$s': { value: 10 } }
{ someValue: { value: 10 } }
) ).toEqual( expectedElement );
} );
it( 'returns expected output for a custom component with children ' +
'replacement', () => {
const TestComponent = ( props ) => {
return <div { ...props } >{ props.children }</div>;
};
const testString = 'This is a string with a <span1>Custom Component</span1>';
const testString = 'This is a string with a <span>Custom Component</span>';
const expectedElement = createElement(
Fragment,
{},
Expand All @@ -129,15 +130,15 @@ describe( 'createInterpolateElement', () => {
expect( createInterpolateElement(
testString,
{
span1: { tag: TestComponent, props: {} },
span: { tag: TestComponent, props: {} },
}
) ).toEqual( expectedElement );
} );
it( 'returns expected output for self closing custom component', () => {
const TestComponent = ( props ) => {
return <div { ...props } />;
};
const testString = 'This is a string with a self closing custom component: <span1/>';
const testString = 'This is a string with a self closing custom component: <span/>';
const expectedElement = createElement(
Fragment,
{},
Expand All @@ -152,16 +153,16 @@ describe( 'createInterpolateElement', () => {
expect( createInterpolateElement(
testString,
{
span1: { tag: TestComponent, props: {} },
span: { tag: TestComponent, props: {} },
}
) ).toEqual( expectedElement );
} );
it( 'returns expected output for complex replacement', () => {
const TestComponent = ( props ) => {
return <div { ...props } />;
};
const testString = 'This is a complex string having a %1$s value, with ' +
'a <a1>nested <em1>%2$s</em1> link</a1> and value: %3$s';
const testString = 'This is a complex string having a <concrete/> value, with ' +
'a <a1>nested <em1><value/></em1> link</a1> and value: <TestComponent/>';
const expectedElement = createElement(
Fragment,
{},
Expand Down Expand Up @@ -193,11 +194,11 @@ describe( 'createInterpolateElement', () => {
expect( JSON.stringify( createInterpolateElement(
testString,
{
'%1$s': { value: 'concrete' },
a1: { tag: 'a', props: {} },
TestComponent: { value: <TestComponent /> },
concrete: { value: 'concrete' },
em1: { tag: 'em', props: {} },
'%2$s': { value: 'value' },
'%3$s': { value: <TestComponent /> },
value: { value: 'value' },
a1: { tag: 'a', props: {} },
}
) ) ).toEqual( JSON.stringify( expectedElement ) );
} );
Expand Down

0 comments on commit 01a687e

Please sign in to comment.