Commit dd0af4d8 authored by Robert Knight's avatar Robert Knight

Make TextRange ignore text in comments and processing instructions

`Node.textContent` has a quirk where it does not count text in comments
or processing instructions when called on an `Element` but does return
the comment / processing instruction data if called directly on a
`Comment` or `ProcessingInstruction` node.

To make conversion between `Range`s and `TextRange`s consistent in both
directions we need to always ignore text in comments/processing
instructions.
parent ba2d2c00
......@@ -76,6 +76,20 @@ describe('annotator/anchoring/text-range', () => {
assert.equal(offset, lastTextNode.data.length);
});
it('ignores text in comments and processing instructions', () => {
const el = document.createElement('div');
const text = document.createTextNode('some text');
const comment = document.createComment('some comment');
const piNode = document.createProcessingInstruction('foo', 'bar');
el.append(comment, piNode, text);
const pos = new TextPosition(el, 3);
const resolved = pos.resolve();
assert.equal(resolved.node, text);
assert.equal(resolved.offset, 3);
});
it('throws if offset exceeds current text content length', () => {
const pos = new TextPosition(
container,
......@@ -118,6 +132,21 @@ describe('annotator/anchoring/text-range', () => {
assert.equal(grandparentPos.element, grandparent);
assert.equal(grandparentPos.offset, 6);
});
it('ignores text in comments and processing instructions', () => {
const parent = document.createElement('div');
const child = document.createElement('span');
const comment = document.createComment('foobar');
const piNode = document.createProcessingInstruction('one', 'two');
child.append('def');
parent.append(comment, piNode, child);
const childPos = TextPosition.fromPoint(child.firstChild, 3);
const parentPos = childPos.relativeTo(parent);
assert.equal(parentPos.element, parent);
assert.equal(parentPos.offset, 3);
});
});
describe('fromPoint', () => {
......@@ -141,6 +170,18 @@ describe('annotator/anchoring/text-range', () => {
assert.equal(pos.offset, el.textContent.indexOf('bar'));
});
it('ignores text in comments and processing instructions', () => {
const el = document.createElement('div');
const comment = document.createComment('ignore me');
const piNode = document.createProcessingInstruction('one', 'two');
el.append(comment, piNode, 'foobar');
const pos = TextPosition.fromPoint(el.childNodes[2], 3);
assert.equal(pos.element, el);
assert.equal(pos.offset, 3);
});
it('throws if node is not a Text or Element', () => {
assert.throws(() => {
TextPosition.fromPoint(document, 0);
......
/**
* Return the combined length of text nodes contained in `node`.
*
* This is different than `node.textContent` if called on a comment or processing
* instruction directly.
*
* @param {Node} node
*/
function nodeTextLength(node) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
case Node.TEXT_NODE:
return /** @type {string} */ (node.textContent).length;
default:
return 0;
}
}
/**
* Return the total length of the text of all previous siblings of `node`.
*
......@@ -7,7 +25,7 @@ function previousSiblingsTextLength(node) {
let sibling = node.previousSibling;
let length = 0;
while (sibling) {
length += sibling.textContent?.length ?? 0;
length += nodeTextLength(sibling);
sibling = sibling.previousSibling;
}
return length;
......@@ -152,7 +170,7 @@ export class TextPosition {
// Get the text length before the `offset`th child of element.
let textOffset = 0;
for (let i = 0; i < offset; i++) {
textOffset += node.childNodes[i].textContent?.length ?? 0;
textOffset += nodeTextLength(node.childNodes[i]);
}
return new TextPosition(/** @type {Element} */ (node), textOffset);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment