Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed empty text between html elements #10

Merged
merged 8 commits into from
Nov 8, 2021
111 changes: 68 additions & 43 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -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':
Expand Down Expand Up @@ -57,91 +78,96 @@ 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 &nbsp 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;
}

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 [];
Expand All @@ -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
Expand Down Expand Up @@ -352,7 +378,6 @@ function replace(group, serialized, isInsideStatement) {
}

module.exports = {
iteratePureNodes,
iterateChildren,
iterateGroups,
serializeGroup,
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -26,4 +27,4 @@
"recursive-copy": "^2.0.13",
"temp": "^0.9.4"
}
}
}
69 changes: 69 additions & 0 deletions test/unit/br.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const testCase = require('../helpers/test-case');

describe('nested elements', function() {
testCase({
name: 'Br inside text',
input: `
<span>
Hello
<br />
world
</span>`,
output: `
<span>
{{t "Hello <br /> world" htmlSafe=true}}
</span>`
});

testCase({
name: 'No br at the end of the translation',
input: `
<span>
Hello
<br />
world
<br />
</span>`,
output: `
<span>
{{t "Hello <br /> world" htmlSafe=true}}
<br />
</span>`
});

testCase({
name: 'No br at the start of the translation',
input: `
<span>
<br />
Hello
<br />
world
</span>`,
output: `
<span>
<br />
{{t "Hello <br /> world" htmlSafe=true}}
</span>`
});

testCase({
name: 'No br at the start/end of the translation',
input: `
<span>
<br />
<br />
Hello
<br />
world
<br />
</span>`,
output: `
<span>
<br />
<br />
{{t "Hello <br /> world" htmlSafe=true}}
<br />
</span>`
});
})
31 changes: 31 additions & 0 deletions test/unit/emptyText.js
Original file line number Diff line number Diff line change
@@ -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: `
<strong></strong>
<strong></strong>
This is
<strong></strong>
`,
output: `
<strong></strong>
<strong></strong>
{{t "This is"}}
<strong></strong>
`
});

testCase({
name: 'Do not remove empty elements inside translation.',
input: `
This is
<strong></strong>
text.
`,
output: `
{{t "This is <strong></strong> text." htmlSafe=true}}
`
});
});
32 changes: 32 additions & 0 deletions test/unit/groupAllowedElements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

const testCase = require('../helpers/test-case');

describe('grouping allowed elements', function() {
testCase({
name: 'merge html elements without element-modifiers',
input: `
<div>
<i>a</i>
<em>b</em>
<b>c</b>
<strong>d</strong>
<bold>e</bold>
<span>f</span>
<a>g</a>
<div>Not Formatting Element</div>
<code>h</code>
<br />
<sup>i</sup>
<sub>j</sub>
</div>
`,
output: `
<div>
{{t "<i>a</i> <em>b</em> <b>c</b> <strong>d</strong> <bold>e</bold> <span>f</span> <a>g</a>" htmlSafe=true}}
<div>{{t "Not Formatting Element"}}</div>
{{t "<code>h</code> <br /> <sup>i</sup> <sub>j</sub>" htmlSafe=true}}
</div>
`
});

});
29 changes: 29 additions & 0 deletions test/unit/i.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const testCase = require('../helpers/test-case');

describe('test of auditboard-icons', function() {
testCase({
name: 'icon not translated',
input: `
<i class="auditboard-icons fs-14 mr-3">lock_fill</i> Changes
`,
output: `
<i class="auditboard-icons fs-14 mr-3">lock_fill</i> {{t "Changes"}}
`
});

testCase({
name: 'i translated',
input: `
<i class="fs-14 mr-3">This is</i> nice text.
`,
output: `
{{t "<i class='fs-14 mr-3'>This is</i> nice text." htmlSafe=true}}
`
});

testCase({
name: 'i translated',
input: `<i class="fs-14 mr-3">This is nice text.</i>`,
output: `<i class="fs-14 mr-3">{{t "This is nice text."}}</i>`
});
});
29 changes: 29 additions & 0 deletions test/unit/modifiers.js
Original file line number Diff line number Diff line change
@@ -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
<strong>interesting</strong>
text.
`,
output: `
{{t "This is <strong>interesting</strong> text." htmlSafe=true}}
`
});

testCase({
name: 'do not merge html elements if they have element-modifiers',
input: `
This is
<strong {{on 'click' this.doSomething}}>interesting</strong>
text.
`,
output: `
{{t "This is"}}
<strong {{on 'click' this.doSomething}}>{{t "interesting"}}</strong>
{{t "text."}}
`
});
});
Loading