From bb030dabc421ede979c2c89ffdc948d463fad837 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 30 May 2017 15:55:21 +0100 Subject: [PATCH 1/3] Add _tJsx() --- src/languageHandler.js | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/languageHandler.js b/src/languageHandler.js index af428d195f..7927a2dbc1 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -18,6 +18,7 @@ limitations under the License. import request from 'browser-request'; import counterpart from 'counterpart'; import q from 'q'; +import sanitizeHtml from "sanitize-html"; import UserSettingsStore from './UserSettingsStore'; @@ -37,6 +38,56 @@ export function _t(...args) { return counterpart.translate(...args); } +/* + * Translates stringified JSX into translated JSX. E.g + * _tJsx( + * "click here now", + * /(.*?)<\/a>/, + * (sub) => { return { sub }; } + * ); + * + * @param {string} jsxText The untranslated stringified JSX e.g "click here now". + * This will be translated by passing the string through to _t(...) + * + * @param {RegExp} pattern A regexp to match against the translated text. + * The captured groups from the regexp will be fed to 'sub'. + * Only the captured groups will be included in the output, the match itself is discarded. + * + * @param {Function} sub A function which will be called + * with multiple args, each arg representing a captured group of the matching regexp. + * This function must return a JSX node. + * + * @return A list of strings/JSX nodes. + */ +export function _tJsx(jsxText, pattern, sub) { + if (!pattern instanceof RegExp) { + throw new Error(`_tJsx: programmer error. expected RegExp for text: ${jsxText}`); + } + if (!sub instanceof Function) { + throw new Error(`_tJsx: programmer error. expected Function for text: ${jsxText}`); + } + + // tJsxText may be unsafe if malicious translators try to inject HTML. + // Run this through sanitize-html and bail if the output isn't identical + const tJsxText = _t(jsxText); + const sanitized = sanitizeHtml(tJsxText); + if (tJsxText !== sanitized) { + throw new Error(`_tJsx: translator error. untrusted HTML supplied. '${tJsxText}' != '${sanitized}'`); + } + let match = tJsxText.match(pattern); + if (!match) { + throw new Error(`_tJsx: translator error. expected translation to match regexp: ${pattern}`); + } + let capturedGroups = match.slice(1); + // Return the raw translation before the *match* followed by the return value of sub() followed + // by the raw translation after the *match* (not captured group). + return [ + tJsxText.substr(0, match.index), + sub.apply(null, capturedGroups), + tJsxText.substr(match.index + match[0].length), + ]; +} + // Allow overriding the text displayed when no translation exists // Currently only use din unit tests to avoid having to load // the translations in riot-web From 1d67358525780c5b456257b3720d98f088d74456 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 30 May 2017 16:17:42 +0100 Subject: [PATCH 2/3] Make it work with multiple regexps --- src/languageHandler.js | 60 +++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 7927a2dbc1..0564f6ca29 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -49,22 +49,38 @@ export function _t(...args) { * @param {string} jsxText The untranslated stringified JSX e.g "click here now". * This will be translated by passing the string through to _t(...) * - * @param {RegExp} pattern A regexp to match against the translated text. + * @param {RegExp|RegExp[]} patterns A regexp to match against the translated text. * The captured groups from the regexp will be fed to 'sub'. * Only the captured groups will be included in the output, the match itself is discarded. + * If multiple RegExps are provided, the function at the same position will be called. The + * match will always be done from left to right, so the 2nd RegExp will be matched against the + * remaining text from the first RegExp. * - * @param {Function} sub A function which will be called + * @param {Function|Function[]} subs A function which will be called * with multiple args, each arg representing a captured group of the matching regexp. * This function must return a JSX node. * * @return A list of strings/JSX nodes. */ -export function _tJsx(jsxText, pattern, sub) { - if (!pattern instanceof RegExp) { - throw new Error(`_tJsx: programmer error. expected RegExp for text: ${jsxText}`); +export function _tJsx(jsxText, patterns, subs) { + // convert everything to arrays + if (patterns instanceof RegExp) { + patterns = [patterns]; } - if (!sub instanceof Function) { - throw new Error(`_tJsx: programmer error. expected Function for text: ${jsxText}`); + if (subs instanceof Function) { + subs = [subs]; + } + // sanity checks + if (subs.length !== patterns.length || subs.length < 1) { + throw new Error(`_tJsx: programmer error. expected number of RegExps == number of Functions: ${subs.length} != ${patterns.length}`); + } + for (let i = 0; i < subs.length; i++) { + if (!patterns[i] instanceof RegExp) { + throw new Error(`_tJsx: programmer error. expected RegExp for text: ${jsxText}`); + } + if (!subs[i] instanceof Function) { + throw new Error(`_tJsx: programmer error. expected Function for text: ${jsxText}`); + } } // tJsxText may be unsafe if malicious translators try to inject HTML. @@ -74,18 +90,26 @@ export function _tJsx(jsxText, pattern, sub) { if (tJsxText !== sanitized) { throw new Error(`_tJsx: translator error. untrusted HTML supplied. '${tJsxText}' != '${sanitized}'`); } - let match = tJsxText.match(pattern); - if (!match) { - throw new Error(`_tJsx: translator error. expected translation to match regexp: ${pattern}`); + + let output = [tJsxText]; + for (let i = 0; i < patterns.length; i++) { + // convert the last element in 'output' into 3 elements (pre-text, sub function, post-text). + // Rinse and repeat for other patterns (using post-text). + let inputText = output.pop(); + let match = inputText.match(patterns[i]); + if (!match) { + throw new Error(`_tJsx: translator error. expected translation to match regexp: ${patterns[i]}`); + } + let capturedGroups = match.slice(1); + + // Return the raw translation before the *match* followed by the return value of sub() followed + // by the raw translation after the *match* (not captured group). + output.push(inputText.substr(0, match.index)); + output.push(subs[i].apply(null, capturedGroups)); + output.push(inputText.substr(match.index + match[0].length)); } - let capturedGroups = match.slice(1); - // Return the raw translation before the *match* followed by the return value of sub() followed - // by the raw translation after the *match* (not captured group). - return [ - tJsxText.substr(0, match.index), - sub.apply(null, capturedGroups), - tJsxText.substr(match.index + match[0].length), - ]; + + return output; } // Allow overriding the text displayed when no translation exists From 63a998ceb770b48e2e59014f8c84fb777a158aba Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 30 May 2017 17:37:13 +0100 Subject: [PATCH 3/3] Allow span... --- src/languageHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 0564f6ca29..961838b770 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -86,7 +86,7 @@ export function _tJsx(jsxText, patterns, subs) { // tJsxText may be unsafe if malicious translators try to inject HTML. // Run this through sanitize-html and bail if the output isn't identical const tJsxText = _t(jsxText); - const sanitized = sanitizeHtml(tJsxText); + const sanitized = sanitizeHtml(tJsxText, { allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'span' ]) }); if (tJsxText !== sanitized) { throw new Error(`_tJsx: translator error. untrusted HTML supplied. '${tJsxText}' != '${sanitized}'`); }