diff --git a/lib/helpers.js b/lib/helpers.js index bf18243..a65dd6c 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,4 +1,25 @@ + +function unvantedFirstOrLastNode(node) { + if (node.tag === 'br' || (isEmptyText(node) && !isStatement(node))) { + return true; + } else { + return false; + } +} + +function isExcludedNode(node) { + if ((node.tag === 'i' && node.attributes.find(a => a.name === 'class' && a.value?.chars?.indexOf('auditboard-icons') !== -1)) || + (node.tag === 'span' && node.attributes.find(a => a.name === 'class' && a.value?.chars?.indexOf('clearfix') !== -1)) + ) { + return true; + } + return false; +} + function* iterateChildren(node) { + if (isExcludedNode(node)) { + return; + } switch (node.type) { case 'MustacheCommentStatement': case 'MustacheStatement': @@ -57,15 +78,32 @@ function replaceChildren(node, newChildren) { } } +// function return true for node if there is no text to translate. function isEmptyText(node) { - return node.type === 'TextNode' && node.chars.match(/^\s*$/); -} + // TextNode is empty when there are only white space or   or there are no characters; + let isEmptyTextNode = node.type === 'TextNode' && (node.chars.match(/^\s*( )*\s*$/) || node.chars.match(/^[^a-zA-Z]+$/)); -function isNotEmptyText(node) { - return node.type === 'TextNode' && !node.chars.match(/^\s*$/); + if (isEmptyTextNode || node.tag === 'br' || node.type === 'MustacheStatement') { + return true; + } + + //allow only allowed text elements + if (!allowedTextElements.includes(node.tag) || + (node.modifiers && node.modifiers.length)) {// || + return false; + } + + //exclude special nodes + if (isExcludedNode(node)) { + return false; + } + + let children = Array.from(iterateChildren(node)); + let emptyOrStatement = children.every(n => isEmptyText(n)); + return emptyOrStatement; } -const allowedTextElements = ['i', 'em', 'b', 'strong', 'bold', 'span', 'a', 'code', 'br']; +const allowedTextElements = ['i', 'em', 'b', 'strong', 'bold', 'span', 'a', 'code', 'br', 'sup', 'sub']; function isStatement(node) { if (node.type === 'MustacheStatement') { return true; @@ -73,75 +111,63 @@ function isStatement(node) { if (!allowedTextElements.includes(node.tag) || (node.modifiers && node.modifiers.length)) {// || - // node.attributes.find(a => a.value.params?.length || a.value.parts?.find(p => p.params?.length))) { + return false; + } + + //exclude special nodes + if (isExcludedNode(node)) { return false; } let children = Array.from(iterateChildren(node)); - let everyEmptyOrMustache = children.every(ch => isEmptyText(ch) || ch.type === 'MustacheStatement'); - return everyEmptyOrMustache; + let everyTextOrMustache = children.every(ch => isText(ch) || isStatement(ch)); + let atLeastOneMustache = children.find(ch => isStatement(ch)); + return everyTextOrMustache && atLeastOneMustache; } function isText(node) { - if ((node.type === 'TextNode' && isNotEmptyText(node)) || node.tag === 'br') { + if (node.type === 'TextNode' || node.tag === 'br' || node.type === 'MustacheStatement') { return true; } + //allow only allowed text elements if (!allowedTextElements.includes(node.tag) || (node.modifiers && node.modifiers.length)) {// || - // node.attributes.find(a => a.value.params?.length || a.value.parts?.find(p => p.params?.length))) { return false; } - let children = Array.from(iterateChildren(node)); - let everyText = children.every(n => isText(n) || isStatement(n) || isEmptyText(n)); - let onlyNotEmptys = children.filter(ch => !isEmptyText(ch)); - if (onlyNotEmptys.length) { - let firstText = isText(onlyNotEmptys[0]); - let lastText = isText(onlyNotEmptys[onlyNotEmptys.length - 1]); - return everyText && firstText && lastText; - } else { + //exclude special nodes + if (isExcludedNode(node)) { return false; } -} -function* iteratePureNodes(node, parent, index) { - if (isText(node) || isStatement(node)) { - yield { node, parent, index, isText, isStatement }; - } else { - let i = 0; - for (let subNode of iterateChildren(node)) { - for (let res of iteratePureNodes(subNode, node, i)) { - yield res; - } - i++; - } - } + + let children = Array.from(iterateChildren(node)); + let everyText = children.every(n => isText(n)); + return everyText; } function checksFirstLastStatement(group) { - let thereIsText = group.find(i => isText(i.node)); + let thereIsText = group.find(i => !isEmptyText(i.node)); if (thereIsText) { - let notEmptyItems = group.filter(i => !isEmptyText(i.node)); - //remove statements from beginning - while (group.length && notEmptyItems.length) { - let isStatementFirst = isStatement(notEmptyItems[0].node); + while (group.length) { + let isStatementFirst = unvantedFirstOrLastNode(group[0].node); if (isStatementFirst) { - notEmptyItems.shift(); + group.shift(); } else { break; } } - while (group.length && notEmptyItems.length) { - let isStatementLast = isStatement(notEmptyItems[notEmptyItems.length - 1].node); + while (group.length) { + let isStatementLast = unvantedFirstOrLastNode(group[group.length - 1].node); if (isStatementLast) { - notEmptyItems.pop(); + group.pop(); } else { break; } } - return notEmptyItems; + return group; } return []; @@ -150,7 +176,7 @@ function checksFirstLastStatement(group) { function* iterateGroups(node) { let group = []; for (let subNode of iterateChildren(node)) { - if (isText(subNode) || isEmptyText(subNode) || isStatement(subNode)) { + if (isText(subNode) || isEmptyText(subNode)) { group.push({ node: subNode, parent: node @@ -352,7 +378,6 @@ function replace(group, serialized, isInsideStatement) { } module.exports = { - iteratePureNodes, iterateChildren, iterateGroups, serializeGroup, diff --git a/package.json b/package.json index 5afdd36..74a193a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "scripts": { "start": "node runner.js", "lint": "eslint .", - "test": "mocha --recursive test/acceptance test/unit" + "test": "mocha --recursive test/acceptance test/unit", + "test-watch": "mocha --watch --recursive test/acceptance test/unit" }, "author": "", "license": "ISC", @@ -26,4 +27,4 @@ "recursive-copy": "^2.0.13", "temp": "^0.9.4" } -} +} \ No newline at end of file diff --git a/test/unit/br.js b/test/unit/br.js new file mode 100644 index 0000000..b08a0e7 --- /dev/null +++ b/test/unit/br.js @@ -0,0 +1,69 @@ +const testCase = require('../helpers/test-case'); + +describe('nested elements', function() { + testCase({ + name: 'Br inside text', + input: ` + + Hello +
+ world +
`, + output: ` + + {{t "Hello
world" htmlSafe=true}} +
` + }); + + testCase({ + name: 'No br at the end of the translation', + input: ` + + Hello +
+ world +
+
`, + output: ` + + {{t "Hello
world" htmlSafe=true}} +
+
` + }); + + testCase({ + name: 'No br at the start of the translation', + input: ` + +
+ Hello +
+ world +
`, + output: ` + +
+ {{t "Hello
world" htmlSafe=true}} +
` + }); + + testCase({ + name: 'No br at the start/end of the translation', + input: ` + +
+
+ Hello +
+ world +
+
`, + output: ` + +
+
+ {{t "Hello
world" htmlSafe=true}} +
+
` + }); +}) diff --git a/test/unit/emptyText.js b/test/unit/emptyText.js new file mode 100644 index 0000000..8908ab1 --- /dev/null +++ b/test/unit/emptyText.js @@ -0,0 +1,31 @@ +const testCase = require('../helpers/test-case'); + +describe('Empty text', function() { + testCase({ + name: 'Remove empty elements from start and begining of a translation', + input: ` + + + This is + + `, + output: ` + + + {{t "This is"}} + + ` + }); + + testCase({ + name: 'Do not remove empty elements inside translation.', + input: ` + This is + + text. + `, + output: ` + {{t "This is text." htmlSafe=true}} + ` + }); +}); \ No newline at end of file diff --git a/test/unit/groupAllowedElements.js b/test/unit/groupAllowedElements.js new file mode 100644 index 0000000..6dca63f --- /dev/null +++ b/test/unit/groupAllowedElements.js @@ -0,0 +1,32 @@ + +const testCase = require('../helpers/test-case'); + +describe('grouping allowed elements', function() { + testCase({ + name: 'merge html elements without element-modifiers', + input: ` +
+ a + b + c + d + e + f + g +
Not Formatting Element
+ h +
+ i + j +
+ `, + output: ` +
+ {{t "a b c d e f g" htmlSafe=true}} +
{{t "Not Formatting Element"}}
+ {{t "h
i j" htmlSafe=true}} +
+ ` + }); + +}); \ No newline at end of file diff --git a/test/unit/i.js b/test/unit/i.js new file mode 100644 index 0000000..d277770 --- /dev/null +++ b/test/unit/i.js @@ -0,0 +1,29 @@ +const testCase = require('../helpers/test-case'); + +describe('test of auditboard-icons', function() { + testCase({ + name: 'icon not translated', + input: ` + lock_fill Changes + `, + output: ` + lock_fill {{t "Changes"}} + ` + }); + + testCase({ + name: 'i translated', + input: ` + This is nice text. + `, + output: ` + {{t "This is nice text." htmlSafe=true}} + ` + }); + + testCase({ + name: 'i translated', + input: `This is nice text.`, + output: `{{t "This is nice text."}}` + }); +}); \ No newline at end of file diff --git a/test/unit/modifiers.js b/test/unit/modifiers.js new file mode 100644 index 0000000..4bdc2b1 --- /dev/null +++ b/test/unit/modifiers.js @@ -0,0 +1,29 @@ +const testCase = require('../helpers/test-case'); + +describe('test modifiers', function() { + testCase({ + name: 'merge html elements without element-modifiers', + input: ` + This is + interesting + text. + `, + output: ` + {{t "This is interesting text." htmlSafe=true}} + ` + }); + + testCase({ + name: 'do not merge html elements if they have element-modifiers', + input: ` + This is + interesting + text. + `, + output: ` + {{t "This is"}} + {{t "interesting"}} + {{t "text."}} + ` + }); +}); \ No newline at end of file diff --git a/test/unit/nested.js b/test/unit/nested.js index 88965c7..7830506 100644 --- a/test/unit/nested.js +++ b/test/unit/nested.js @@ -1,6 +1,6 @@ const testCase = require('../helpers/test-case'); -describe('nested elements', function () { +describe('nested elements', function() { testCase({ name: 'nested span', input: ` {{t "aaa test " sponsor_name=sponsor.name htmlSafe=true}} ` - }) + }); + + testCase({ + name: 'multiple nested span', + input: ` + + + Hello + + + world + + `, + output: ` + + {{t " Hello world " sponsor_name=sponsor.name htmlSafe=true}} + ` + }); + + testCase({ + name: 'nested span without text', + input: ` + + {{this.a}} + + {{this.b}} + + + {{this.c}} + + `, + output: ` + + {{this.a}} + + {{this.b}} + + + {{this.c}} + + `, + }); + + testCase({ + name: 'nested span with text', + input: ` + + Status + {{this.a}} + + {{this.b}} + + + {{this.c}} + + is interesting. + `, + output: ` + + {{t "Status {this_a} {this_b} {this_c} is interesting." this_a=this.a this_b=this.b sponsor_name=sponsor.name this_c=this.c htmlSafe=true}} + `, + }); + + testCase({ + name: 'no empty space between html tags', + input: ` + + Status + {{this.a}} + + {{this.b}} + + {{this.c}} + + is interesting. + `, + output: ` + + {{t "Status {this_a} {this_b} {this_c} is interesting." this_a=this.a this_b=this.b sponsor_name=sponsor.name this_c=this.c htmlSafe=true}} + `, + }); })