Skip to content

Commit

Permalink
replace regex with ast implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jamuhl committed Sep 16, 2017
1 parent 7e40590 commit d6acc24
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 69 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### 5.4.0
- replaces regex used to parse nodes from string to an ast implementation solving [#298](https://github.com/i18next/react-i18next/issues/298)

### 5.3.0
- Pass extra parameters to Trans parent component

Expand Down
14 changes: 14 additions & 0 deletions example/webpack2/app/components/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,22 @@ class TranslatableView extends React.Component {
const count = 10;
const name = 'Arthur';

const numOfItems = 11;

return (
<div>
<h1>{t('common:appName')}</h1>

<Trans i18nKey='testTransKey1' count={numOfItems}>
{{numOfItems}} items matched.
</Trans>
<Trans i18nKey='testTransKey2' count={numOfItems}>
<span className='matchCount'>{{numOfItems}}</span> items matched.
</Trans>
<Trans i18nKey='testTransKey3' count={numOfItems}>
Result: <span className='matchCount'>{{numOfItems}}</span> items matched.
</Trans>


<Trans i18nKey="transTest" count={count}>
Hello <strong title={t('nameTitle')}>{{name, format: 'uppercase'}}</strong>, you have {{count}} message. Open <Link to="/msgs">here</Link>.
Expand All @@ -47,6 +59,8 @@ class TranslatableView extends React.Component {
<input value="copyme" readOnly />
</Trans>



<button onClick={() => toggle('de')}>{t('nav:linkDE')}</button>
<button onClick={() => toggle('en')}>{t('nav:linkEN')}</button>
{
Expand Down
8 changes: 7 additions & 1 deletion example/webpack2/locales/en/view.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@
"transTest": "Hello <1><0>{{name}}</0></1>, you have <3>{{count}}</3> message. Open <5>hear</5>.",
"transTest_plural": "Hello <1><0>{{name}}</0></1>, you have <3>{{count}}</3> messages. Open <5>here</5>.",
"nameTitle": "this is your name",
"share": "Interpolated <0></0> component"
"share": "Interpolated <0></0> component",
"testTransKey1": "<0>{{numOfItems}}</0> item matched.",
"testTransKey1_plural": "<0>{{numOfItems}}</0> items matched.",
"testTransKey2": "<0><0>{{numOfItems}}</0></0> item matched.",
"testTransKey2_plural": "<0><0>{{numOfItems}}</0></0> items matched.",
"testTransKey3": "Result: <1><0>{{numOfItems}}</0></1> item matched.",
"testTransKey3_plural": "Result: <1><0>{{numOfItems}}</0></1> items matched."
}
2 changes: 1 addition & 1 deletion example/webpack2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"prop-types": "15.5.6",
"react": "15.4.2",
"react-dom": "15.4.2",
"react-i18next": "4.6.2"
"react-i18next": "5.4.0"
}
}
5 changes: 5 additions & 0 deletions example/webpack2/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@ module.exports = {
loaders: ['babel-loader'],
include: path.join(__dirname, 'app')
}]
},
resolve: {
alias: {
react: path.resolve(__dirname, 'node_modules/react/')
}
}
};
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
"url": "https://github.com/i18next/react-i18next.git"
},
"dependencies": {
"hoist-non-react-statics": "1.2.0"
"hoist-non-react-statics": "1.2.0",
"html-parse-stringify2": "2.0.1",
"prop-types": "^15.5.10",
"react": "^15.6.1",
"react-dom": "^15.6.1"
},
"devDependencies": {
"babel-cli": "6.24.1",
Expand Down Expand Up @@ -94,5 +98,6 @@
"!**/test/**",
"!**/example/**"
]
}
},
"lock": false
}
268 changes: 233 additions & 35 deletions react-i18next.js
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,214 @@ Interpolate.contextTypes = {
t: PropTypes.func.isRequired
};

/**
* This file automatically generated from `pre-publish.js`.
* Do not manually edit.
*/

var index$2 = {
"area": true,
"base": true,
"br": true,
"col": true,
"embed": true,
"hr": true,
"img": true,
"input": true,
"keygen": true,
"link": true,
"menuitem": true,
"meta": true,
"param": true,
"source": true,
"track": true,
"wbr": true
};

var attrRE = /([\w-]+)|=|(['"])([.\s\S]*?)\2/g;


var parseTag = function (tag) {
var i = 0;
var key;
var expectingValueAfterEquals = true;
var res = {
type: 'tag',
name: '',
voidElement: false,
attrs: {},
children: []
};

tag.replace(attrRE, function (match) {
if (match === '=') {
expectingValueAfterEquals = true;
i++;
return;
}

if (!expectingValueAfterEquals) {
if (key) {
res.attrs[key] = key; // boolean attribute
}
key=match;
} else {
if (i === 0) {
if (index$2[match] || tag.charAt(tag.length - 2) === '/') {
res.voidElement = true;
}
res.name = match;
} else {
res.attrs[key] = match.replace(/^['"]|['"]$/g, '');
key=undefined;
}
}
i++;
expectingValueAfterEquals = false;
});

return res;
};

/*jshint -W030 */
var tagRE = /(?:<!--[\S\s]*?-->|<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>)/g;

// re-used obj for quick lookups of components
var empty = Object.create ? Object.create(null) : {};
// common logic for pushing a child node onto a list
function pushTextNode(list, html, level, start, ignoreWhitespace) {
// calculate correct end of the content slice in case there's
// no tag after the text node.
var end = html.indexOf('<', start);
var content = html.slice(start, end === -1 ? undefined : end);
// if a node is nothing but whitespace, collapse it as the spec states:
// https://www.w3.org/TR/html4/struct/text.html#h-9.1
if (/^\s*$/.test(content)) {
content = ' ';
}
// don't add whitespace-only text nodes if they would be trailing text nodes
// or if they would be leading whitespace-only text nodes:
// * end > -1 indicates this is not a trailing text node
// * leading node is when level is -1 and list has length 0
if ((!ignoreWhitespace && end > -1 && level + list.length >= 0) || content !== ' ') {
list.push({
type: 'text',
content: content
});
}
}

var parse = function parse(html, options) {
options || (options = {});
options.components || (options.components = empty);
var result = [];
var current;
var level = -1;
var arr = [];
var byTag = {};
var inComponent = false;

html.replace(tagRE, function (tag, index) {
if (inComponent) {
if (tag !== ('</' + current.name + '>')) {
return;
} else {
inComponent = false;
}
}

var isOpen = tag.charAt(1) !== '/';
var isComment = tag.indexOf('<!--') === 0;
var start = index + tag.length;
var nextChar = html.charAt(start);
var parent;

if (isOpen && !isComment) {
level++;

current = parseTag(tag);
if (current.type === 'tag' && options.components[current.name]) {
current.type = 'component';
inComponent = true;
}

if (!current.voidElement && !inComponent && nextChar && nextChar !== '<') {
pushTextNode(current.children, html, level, start, options.ignoreWhitespace);
}

byTag[current.tagName] = current;

// if we're at root, push new base node
if (level === 0) {
result.push(current);
}

parent = arr[level - 1];

if (parent) {
parent.children.push(current);
}

arr[level] = current;
}

if (isComment || !isOpen || current.voidElement) {
if (!isComment) {
level--;
}
if (!inComponent && nextChar !== '<' && nextChar) {
// trailing text node
// if we're at the root, push a base text node. otherwise add as
// a child to the current node.
parent = level === -1 ? result : arr[level].children;
pushTextNode(parent, html, level, start, options.ignoreWhitespace);
}
}
});

// If the "html" passed isn't actually html, add it as a text node.
if (!result.length && html.length) {
pushTextNode(result, html, 0, 0, options.ignoreWhitespace);
}

return result;
};

function attrString(attrs) {
var buff = [];
for (var key in attrs) {
buff.push(key + '="' + attrs[key] + '"');
}
if (!buff.length) {
return '';
}
return ' ' + buff.join(' ');
}

function stringify(buff, doc) {
switch (doc.type) {
case 'text':
return buff + doc.content;
case 'tag':
buff += '<' + doc.name + (doc.attrs ? attrString(doc.attrs) : '') + (doc.voidElement ? '/>' : '>');
if (doc.voidElement) {
return buff;
}
return buff + doc.children.reduce(stringify, '') + '</' + doc.name + '>';
}
}

var stringify_1 = function (doc) {
return doc.reduce(function (token, rootEl) {
return token + stringify('', rootEl);
}, '');
};

var index$1 = {
parse: parse,
stringify: stringify_1
};

function hasChildren(node) {
return node && (node.children || node.props && node.props.children);
}
Expand Down Expand Up @@ -565,55 +773,45 @@ function nodesToString(mem, children, index) {
return mem;
}

var REGEXP = new RegExp('(?:<([^>]*)>(.*?)<\\/\\1>)', 'gi');
function renderNodes(children, targetString, i18n) {

function parseChildren(nodes, str) {
if (Object.prototype.toString.call(nodes) !== '[object Array]') nodes = [nodes];

var toRender = str.split(REGEXP).reduce(function (mem, match, i) {
if (match) mem.push(match);
return mem;
}, []);

return toRender.reduce(function (mem, part, i) {
// is a tag
var isTag = !isNaN(part);
var previousIsTag = i > 0 ? !isNaN(toRender[i - 1]) : false;
if (previousIsTag) {
var child = nodes[parseInt(toRender[i - 1], 10)] || {};
if (React__default.isValidElement(child) && !hasChildren(child)) previousIsTag = false;
}
// parse ast from string with additional wrapper tag
// -> avoids issues in parser removing prepending text nodes
var ast = index$1.parse('<0>' + targetString + '</0>');

// will be rendered inside child
if (previousIsTag) return mem;
function mapAST(reactNodes, astNodes) {
if (Object.prototype.toString.call(reactNodes) !== '[object Array]') reactNodes = [reactNodes];
if (Object.prototype.toString.call(astNodes) !== '[object Array]') astNodes = [astNodes];

if (isTag) {
var _child = nodes[parseInt(part, 10)] || {};
var isElement = React__default.isValidElement(_child);
return astNodes.reduce(function (mem, node, i) {
if (node.type === 'tag') {
var child = reactNodes[parseInt(node.name, 10)] || {};
var isElement = React__default.isValidElement(child);

if (typeof _child === 'string') {
mem.push(_child);
} else if (hasChildren(_child)) {
var inner = parseChildren(getChildren(_child), toRender[i + 1]);
if (typeof child === 'string') {
mem.push(child);
} else if (hasChildren(child)) {
var inner = mapAST(getChildren(child), node.children);

mem.push(React__default.cloneElement(_child, _extends({}, _child.props, { key: i }), inner));
} else if ((typeof _child === 'undefined' ? 'undefined' : _typeof(_child)) === 'object' && !isElement) {
var interpolated = i18n.services.interpolator.interpolate(toRender[i + 1], _child, i18n.language);
mem.push(React__default.cloneElement(child, _extends({}, child.props, { key: i }), inner));
} else if ((typeof child === 'undefined' ? 'undefined' : _typeof(child)) === 'object' && !isElement) {
var interpolated = i18n.services.interpolator.interpolate(node.children[0].content, child, i18n.language);
mem.push(interpolated);
} else {
mem.push(_child);
mem.push(child);
}
} else if (node.type === 'text') {
mem.push(node.content);
}

// no element just a string
if (!isTag && !previousIsTag) mem.push(part);

return mem;
}, []);
}

return parseChildren(children, targetString);
// call mapAST with having react nodes nested into additional node like
// we did for the string ast from translation
// return the children of that extra node to get expected result
var result = mapAST([{ children: children }], ast);
return result[0].props.children;
}

var Trans = function (_React$Component) {
Expand Down
Loading

0 comments on commit d6acc24

Please sign in to comment.