Skip to content

Commit

Permalink
Add getTextContent and setTextContent
Browse files Browse the repository at this point in the history
With tests

Add a spec compliant normalize

With tests

Make `normalize` on document work

Also support document fragments
Fixes #10

link to normalize spec
  • Loading branch information
dfreedm authored and usergenic committed Jun 5, 2018
1 parent d21f17e commit c878037
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 29 deletions.
102 changes: 84 additions & 18 deletions packages/dom5/dom5.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,83 @@ function hasClass(name) {
};
}

function collapseTextRange(parent, start, end) {
var text = '';
for (var i = start; i <= end; i++) {
text += getTextContent(parent.childNodes[i]);
}
parent.childNodes.splice(start, (end - start) + 1);
if (text) {
var tn = newTextNode(text);
tn.parentNode = parent;
parent.childNodes.splice(start, 0, tn);
}
}

/**
* Normalize the text inside an element
*
* Equivalent to `element.textContent` in the browser
* Equivalent to `element.normalize()` in the browser
* See https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
*/
function normalizeTextContent(node) {
var text = '';
if (isElement(node)) {
for (var i = 0, cn; i < node.childNodes.length; i++) {
cn = node.childNodes[i];
if (!isTextNode(cn)) {
continue;
function normalize(node) {
if (!(isElement(node) || isDocument(node) || isDocumentFragment(node))) {
return;
}
var textRangeStart = -1;
for (var i = node.childNodes.length - 1, n; i >= 0; i--) {
n = node.childNodes[i];
if (isTextNode(n)) {
if (textRangeStart == -1) {
textRangeStart = i;
}
if (i === 0) {
// collapse leading text nodes
collapseTextRange(node, 0, textRangeStart);
}
} else {
// recurse
normalize(n);
// collapse the range after this node
if (textRangeStart > -1) {
collapseTextRange(node, i + 1, textRangeStart);
textRangeStart = -1;
}
text += cn.value;
}
}
return text;
}

/**
* Return the text value of a node or element
*
* Equivalent to `node.textContent` in the browser
*/
function getTextContent(node) {
if (isCommentNode(node)) {
return node.data;
}
if (isTextNode(node)) {
return node.value;
}
var subtree = nodeWalkAll(node, isTextNode);
return subtree.map(getTextContent).join('');
}

/**
* Set the text value of a node or element
*
* Equivalent to `node.textContent = value` in the browser
*/
function setTextContent(node, value) {
if (isCommentNode(node)) {
node.data = value;
} else if (isTextNode(node)) {
node.value = value;
} else {
var tn = newTextNode(value);
tn.parentNode = node;
node.childNodes = [tn];
}
}

/**
Expand All @@ -100,14 +160,7 @@ function normalizeTextContent(node) {
*/
function hasTextValue(value) {
return function(node) {
if (isElement(node)) {
return normalizeTextContent(node) === value;
} else if (isTextNode(node)) {
return node.value === value;
} else if (isCommentNode(node)) {
return node.data === value;
}
return false;
return getTextContent(node) === value;
};
}

Expand Down Expand Up @@ -168,6 +221,14 @@ function hasAttrValue(attr, value) {
};
}

function isDocument(node) {
return node.nodeName === '#document';
}

function isDocumentFragment(node) {
return node.nodeName === '#document-fragment';
}

function isElement(node) {
return node.nodeName === node.tagName;
}
Expand Down Expand Up @@ -293,6 +354,11 @@ module.exports = {
hasAttribute: hasAttribute,
setAttribute: setAttribute,
removeAttribute: removeAttribute,
getTextContent: getTextContent,
setTextContent: setTextContent,
normalize: normalize,
isDocument: isDocument,
isDocumentFragment: isDocumentFragment,
isElement: isElement,
isTextNode: isTextNode,
isCommentNode: isCommentNode,
Expand Down
209 changes: 198 additions & 11 deletions packages/dom5/test/dom5_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,24 +162,98 @@ suite('dom5', function() {
dom5.setAttribute(text, 'bar', 'baz');
});
});
});

suite('removeAttribute', function() {
suite('removeAttribute', function() {

test('removes a set attribute', function() {
var divA = doc.childNodes[1].childNodes[1].childNodes[0];
dom5.removeAttribute(divA, 'foo');
assert.equal(dom5.getAttribute(divA, 'foo'), null);
});
test('removes a set attribute', function() {
var divA = doc.childNodes[1].childNodes[1].childNodes[0];
dom5.removeAttribute(divA, 'foo');
assert.equal(dom5.getAttribute(divA, 'foo'), null);
});

test('does not throw when called on a node without that attribute', function() {
var divA = doc.childNodes[1].childNodes[1].childNodes[0];
assert.doesNotThrow(function() {
dom5.removeAttribute(divA, 'ZZZ');
});
test('does not throw when called on a node without that attribute', function() {
var divA = doc.childNodes[1].childNodes[1].childNodes[0];
assert.doesNotThrow(function() {
dom5.removeAttribute(divA, 'ZZZ');
});
});
});

suite('getTextContent', function() {
var body;

suiteSetup(function() {
body = doc.childNodes[1].childNodes[1];
});

test('text node', function() {
var node = body.childNodes[0].childNodes[0];
var expected = 'a1';
var actual = dom5.getTextContent(node);
assert.equal(actual, expected);
});

test('comment node', function() {
var node = body.childNodes.slice(-1)[0];
var expected = ' comment ';
var actual = dom5.getTextContent(node);
assert.equal(actual, expected);
});

test('leaf element', function() {
var node = body.childNodes[0].childNodes[1];
var expected = 'b1';
var actual = dom5.getTextContent(node);
assert.equal(actual, expected);
});

test('recursive element', function() {
var expected = 'a1b1a2';
var actual = dom5.getTextContent(body);
assert.equal(actual, expected);
});
});

suite('setTextContent', function() {
var body;
var expected = 'test';

suiteSetup(function() {
body = doc.childNodes[1].childNodes[1];
});

test('text node', function() {
var node = body.childNodes[0].childNodes[0];
dom5.setTextContent(node, expected);
var actual = dom5.getTextContent(node);
assert.equal(actual, expected);
});

test('comment node', function() {
var node = body.childNodes.slice(-1)[0];
dom5.setTextContent(node, expected);
var actual = dom5.getTextContent(node);
assert.equal(actual, expected);
});

test('leaf element', function() {
var node = body.childNodes[0].childNodes[1];
dom5.setTextContent(node, expected);
var actual = dom5.getTextContent(node);
assert.equal(actual, expected);
assert.equal(node.childNodes.length, 1);
});

test('recursive element', function() {
dom5.setTextContent(body, expected);
var actual = dom5.getTextContent(body);
assert.equal(actual, expected);
assert.equal(body.childNodes.length, 1);
});

});

});

suite('Query Predicates', function() {
Expand Down Expand Up @@ -413,4 +487,117 @@ suite('dom5', function() {
});
});

suite('Text Normalization', function() {
var con = dom5.constructors;

function append(parent, node) {
node.parentNode = parent;
parent.childNodes.push(node);
}

test('normalizing text nodes or comment nodes is a noop', function() {
var tn = con.text('test');
var cn = con.comment('test2');

dom5.normalize(tn);
dom5.normalize(cn);
assert.equal(tn, tn);
assert.equal(cn, cn);
});

test("an element's child text nodes are merged", function() {
var div = con.element('div');
var tn1 = con.text('foo');
var tn2 = con.text('bar');
append(div, tn1);
append(div, tn2);

var expected = dom5.getTextContent(div);
assert.equal(expected, 'foobar');
dom5.normalize(div);
var actual = dom5.getTextContent(div);

assert.equal(actual, expected);
assert.equal(div.childNodes.length, 1);
});

test('only text node ranges are merged', function() {
var div = con.element('div');
var tn1 = con.text('foo');
var tn2 = con.text('bar');
var cn = con.comment('combobreaker');
var tn3 = con.text('quux');
append(div, tn1);
append(div, tn2);
append(div, cn);
append(div, tn3);

var expected = dom5.getTextContent(div);
assert.equal(expected, 'foobarquux');
dom5.normalize(div);
var actual = dom5.getTextContent(div);

assert.equal(actual, expected);
assert.equal(div.childNodes.length, 3);
assert.equal(dom5.getTextContent(div.childNodes[0]), 'foobar');
assert.equal(dom5.getTextContent(div.childNodes[1]), 'combobreaker');
assert.equal(dom5.getTextContent(div.childNodes[2]), 'quux');
});

test('empty text nodes are removed', function() {
var div = con.element('div');
var tn = con.text('');
append(div, tn);

assert.equal(div.childNodes.length, 1);
dom5.normalize(div);
assert.equal(div.childNodes.length, 0);
});

test('elements are recursively normalized', function() {
var div = con.element('div');
var tn1 = con.text('foo');
var space = con.text('');
append(div, tn1);
append(div, space);
var span = con.element('span');
var tn2 = con.text('bar');
var tn3 = con.text('baz');
append(span, tn2);
append(span, tn3);
append(div, span);

assert.equal(dom5.getTextContent(div), 'foobarbaz');

dom5.normalize(div);

assert.equal(div.childNodes.length, 2);
assert.equal(span.childNodes.length, 1);
});

test('document can be normalized', function() {
var doc = dom5.parse('<!DOCTYPE html>');
var body = doc.childNodes[1].childNodes[1];
var div = con.element('div');
var tn1 = con.text('foo');
var space = con.text('');
append(div, tn1);
append(div, space);
var span = con.element('span');
var tn2 = con.text('bar');
var tn3 = con.text('baz');
append(span, tn2);
append(span, tn3);
append(div, span);
append(body, div);

assert.equal(dom5.getTextContent(doc), 'foobarbaz');
dom5.normalize(doc);
assert.equal(dom5.getTextContent(doc), 'foobarbaz');

assert.equal(div.childNodes.length, 2);
assert.equal(span.childNodes.length, 1);
});
});

});

0 comments on commit c878037

Please sign in to comment.