diff --git a/.jshintrc b/.jshintrc index 3a7e093b8f..172f05cea8 100644 --- a/.jshintrc +++ b/.jshintrc @@ -83,5 +83,7 @@ "white" : false, // true: Check against strict whitespace and indentation rules // Custom Globals - "globals" : {} // additional predefined global variables + "globals" : { + "Promise": true + } // additional predefined global variables } \ No newline at end of file diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 0771ab56a5..d0d1b90251 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -339,6 +339,68 @@ Translator.moduleFactories = {}; + /** + * Remove the translator patterns from text + * @param {string} text + * @returns {string} + */ + Translator.removePatterns = function removePatterns(text) { + var len = text.length; + var cursor = 0; + var lastBreak = 0; + var level = 0; + var out = ''; + var sub; + + while (cursor < len) { + sub = text.slice(cursor, cursor + 2); + if (sub === '[[') { + if (level === 0) { + out += text.slice(lastBreak, cursor); + } + level += 1; + cursor += 2; + } else if (sub === ']]') { + level -= 1; + cursor += 2; + if (level === 0) { + lastBreak = cursor; + } + } else { + cursor += 1; + } + } + out += text.slice(lastBreak, cursor); + return out; + }; + + /** + * Escape translator patterns in text + * @param {string} text + * @returns {string} + */ + Translator.escape = function escape(text) { + return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text; + }; + + /** + * Unescape escaped translator patterns in text + * @param {string} text + * @returns {string} + */ + Translator.unescape = function unescape(text) { + return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text; + }; + + /** + * Construct a translator pattern + */ + Translator.compile = function compile() { + var args = Array.prototype.slice.call(arguments, 0); + + return '[[' + args.join(', ') + ']]'; + }; + return Translator; }()); @@ -348,12 +410,16 @@ */ Translator: Translator, + compile: Translator.compile, + escape: Translator.escape, + unescape: Translator.unescape, + getLanguage: Translator.getLanguage, + /** * Legacy translator function for backwards compatibility */ translate: function translate(text, language, callback) { - // console.warn('[translator] `translator.translate(text, [lang, ]callback)` is deprecated. ' + - // 'Use the `translator.Translator` class instead.'); + // TODO: deprecate? var cb = callback; var lang = language; @@ -373,31 +439,6 @@ }); }, - /** - * Construct a translator pattern - * @param {string} name - Translation name - * @param {string[]} args - Optional arguments for the pattern - */ - compile: function compile() { - var args = Array.prototype.slice.call(arguments, 0); - - return '[[' + args.join(', ') + ']]'; - }, - - /** - * Escape translation patterns from text - */ - escape: function escape(text) { - return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text; - }, - - /** - * Unescape translation patterns from text - */ - unescape: function unescape(text) { - return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text; - }, - /** * Add translations to the cache */ @@ -422,11 +463,6 @@ adaptor.getTranslations(language, namespace, callback); }, - /** - * Get the language of the current environment, falling back to defaults - */ - getLanguage: Translator.getLanguage, - toggleTimeagoShorthand: function toggleTimeagoShorthand() { var tmp = assign({}, jQuery.timeago.settings.strings); jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort); diff --git a/src/admin/search.js b/src/admin/search.js index e97e8d177e..76f127a4d8 100644 --- a/src/admin/search.js +++ b/src/admin/search.js @@ -2,12 +2,11 @@ var fs = require('fs'); var path = require('path'); -var nconf = require('nconf'); -var sanitize = require('sanitize-html'); +var sanitizeHTML = require('sanitize-html'); var languages = require('../languages'); -var meta = require('../meta'); var utils = require('../../public/src/utils'); +var Translator = require('../../public/src/modules/translator'); function walk(directory) { return new Promise(function (resolve, reject) { @@ -45,117 +44,102 @@ function loadLanguage(language, filename) { }); } +function filterDirectories(directories) { + return directories.map(function (dir) { + // get the relative path + return dir.replace(/^.*(admin.*?).tpl$/, '$1'); + }).filter(function (dir) { + // exclude partials + // only include subpaths + return !dir.includes('/partials/') && /\/.*\//.test(dir); + }); +} + function getAdminNamespaces() { return walk(path.resolve('./public/templates/admin')) - .then(function (directories) { - return directories.map(function (dir) { - return dir.replace(/^.*(admin.*?).tpl$/, '$1'); - }).filter(function (dir) { - return !dir.includes('/partials/'); - }).filter(function (dir) { - return dir.match(/\/.*\//); - }); - }); + .then(filterDirectories); +} + +function sanitize(html) { + // reduce the template to just meaningful text + // remove all tags and strip out scripts, etc completely + return sanitizeHTML(html, { + allowedTags: [], + allowedAttributes: [], + }); +} + +function simplify(translations) { + return translations + // remove all mustaches + .replace(/(?:\{{1,2}[^\}]*?\}{1,2})/g, '') + // collapse whitespace + .replace(/(?:[ \t]*[\n\r]+[ \t]*)+/g, '\n') + .replace(/[\t ]+/g, ' '); } var fallbackCache = {}; -function removeTranslatorPatterns(str) { - var len = str.len; - var cursor = 0; - var lastBreak = 0; - var level = 0; - var out = ''; - var sub; - - while (cursor < len) { - sub = str.slice(cursor, cursor + 2); - if (sub === '[[') { - if (level === 0) { - out += str.slice(lastBreak, cursor); - } - level += 1; - cursor += 2; - } else if (sub === ']]') { - level -= 1; - cursor += 2; - if (level === 0) { - lastBreak = cursor; - } - } else { - cursor += 1; - } - } - out += str.slice(lastBreak, cursor); - return out; +function initFallback(namespace) { + return readFile(path.resolve('./public/templates/', namespace + '.tpl')) + .then(function (template) { + var translations = sanitize(template); + translations = simplify(translations); + translations = Translator.removePatterns(translations); + + return { + namespace: namespace, + translations: translations, + }; + }); } function fallback(namespace) { - fallbackCache[namespace] = fallbackCache[namespace] || - readFile(path.resolve('./public/templates/', namespace + '.tpl')) - .then(function (template) { - // reduce the template to just meaningful text - // remove scripts, etc and replace all tags with divs - var translations = sanitize(template, { - transformTags: { - '*': function () { - return { - tagName: 'div' - }; - } - } - }) - // remove all html tags, templating stuff, and translation strings - .replace(/(?:
)|(?:<\/div>)|(?:\{[^\{\}]*\})/g, '') - // collapse whitespace - .replace(/([\n\r]+ ?)+/g, '\n') - .replace(/[\t ]+/g, ' '); - - translations = removeTranslatorPatterns(translations); - - return { - namespace: namespace, - translations: translations, - }; - }); - + // use cache if exists, else make it + fallbackCache[namespace] = fallbackCache[namespace] || initFallback(namespace); return fallbackCache[namespace]; } function initDict(language) { return getAdminNamespaces().then(function (namespaces) { return Promise.all(namespaces.map(function (namespace) { - return loadLanguage(language, namespace).then(function (translations) { - return { namespace: namespace, translations: translations }; - }).then(function (params) { - var namespace = params.namespace; - var translations = params.translations; - - var str = Object.keys(translations).map(function (key) { - return translations[key]; - }).join('\n'); - - return { - namespace: namespace, - translations: str - }; - }) - // TODO: Use translator to get title for admin route? - .catch(function () { - return fallback(namespace); - }) - .catch(function () { - return { namespace: namespace, translations: '' }; - }); + return loadLanguage(language, namespace) + .then(function (translations) { + // join all translations into one string separated by newlines + var str = Object.keys(translations).map(function (key) { + return translations[key]; + }).join('\n'); + + return { + namespace: namespace, + translations: str, + }; + }) + // TODO: Use translator to get title for admin route? + .catch(function () { + // no translations for this route, fallback to template + return fallback(namespace); + }) + .catch(function () { + // no fallback, just return blank + return { + namespace: namespace, + translations: '', + }; + }); })); }); } var cache = {}; -function getDict(language, term) { +function getDict(language) { + // use cache if exists, else make it cache[language] = cache[language] || initDict(language); return cache[language]; } module.exports.getDict = getDict; +module.exports.filterDirectories = filterDirectories; +module.exports.simplify = simplify; +module.exports.sanitize = sanitize; diff --git a/test/search-admin.js b/test/search-admin.js new file mode 100644 index 0000000000..5351018057 --- /dev/null +++ b/test/search-admin.js @@ -0,0 +1,82 @@ +'use strict'; +/*global require*/ + +var assert = require('assert'); +var search = require('../src/admin/search.js'); + +describe('admin search', function () { + describe('filterDirectories', function () { + it('should resolve all paths to relative paths', function (done) { + assert.deepEqual(search.filterDirectories([ + 'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl', + ]), [ + 'admin/gdhgfsdg/sggag', + ]); + done(); + }); + it('should exclude partials', function (done) { + assert.deepEqual(search.filterDirectories([ + 'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl', + 'dfahdfsgf/admin/partials/hgkfds/fdhsdfh.tpl', + ]), [ + 'admin/gdhgfsdg/sggag', + ]); + done(); + }); + it('should exclude files in the admin directory', function (done) { + assert.deepEqual(search.filterDirectories([ + 'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl', + 'dfdasg/admin/hjkdfsk.tpl', + ]), [ + 'admin/gdhgfsdg/sggag', + ]); + done(); + }); + }); + + describe('sanitize', function () { + it('should strip out scripts', function (done) { + assert.equal( + search.sanitize('Pellentesque tristique senectus' + + ' habitant morbi'), + 'Pellentesque tristique senectus' + + ' habitant morbi' + ); + done(); + }); + it('should remove all tags', function (done) { + assert.equal( + search.sanitize('

Pellentesque habitant morbi tristique senectus' + + 'Aenean vitae est.Mauris eleifend leo.

'), + 'Pellentesque habitant morbi tristique senectus' + + 'Aenean vitae est.Mauris eleifend leo.' + ); + done(); + }); + }); + + describe('simplify', function () { + it('should remove all mustaches', function (done) { + assert.equal( + search.simplify( + 'Pellentesque tristique {{senectus}}habitant morbi' + + 'liquam tincidunt {{mauris.eu}}risus' + ), + 'Pellentesque tristique habitant morbi' + + 'liquam tincidunt risus' + ); + done(); + }); + it('should collapse all whitespace', function (done) { + assert.equal( + search.simplify( + 'Pellentesque tristique habitant morbi' + + ' \n\n liquam tincidunt mauris eu risus.' + ), + 'Pellentesque tristique habitant morbi' + + '\nliquam tincidunt mauris eu risus.' + ); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/translator.js b/test/translator.js index 9df035cfde..91fbf8f696 100644 --- a/test/translator.js +++ b/test/translator.js @@ -233,3 +233,15 @@ describe('Translator modules', function () { done(); }); }); + +describe('Translator static methods', function () { + describe('.removePatterns', function () { + it('should remove translator patterns from text', function (done) { + assert.strictEqual( + Translator.removePatterns('Lorem ipsum dolor [[sit:amet]], consectetur adipiscing elit. [[sed:vitae, [[semper:dolor]]]] lorem'), + 'Lorem ipsum dolor , consectetur adipiscing elit. lorem' + ); + done(); + }); + }); +});