Commit 07773973 authored by Robert Knight's avatar Robert Knight

Add a new implementation of text position => Range conversion

Add a new implementation of conversion from text positions (that is,
offsets within an element's `textContent`) to DOM `Range`s along with
test cases.

This addresses an issue with the existing implementation of `toRange` in
the `dom-anchor-text-position` package where conversion fails when the
text position includes the end of the element's text.

Even if/when the issue is addressed upstream, I think it would be useful
to retain these test cases to guard against future regressions.

See #1329
parent 623a359b
'use strict';
const { toRange } = require('../text-position');
describe('text-position', () => {
let container;
before(() => {
container = document.createElement('div');
container.innerHTML = `<h1>Test article</h1>
<p>First paragraph.</p>
<p>Second paragraph.</p>`;
});
after(() => {
container.remove();
});
describe('toRange', () => {
const testCase = (description, text) => ({
description,
text,
expected: text,
});
[
testCase('start text of root', 'Test article'),
testCase('a whole text node', 'First paragraph.'),
testCase('end text of root', 'Second paragraph.'),
testCase('part of a text node', 'rst paragraph'),
{
description: 'negative start offset',
start: -5,
end: 5,
expected: new Error('invalid start offset'),
},
{
description: 'invalid start offset',
start: 1000,
end: 1010,
expected: new Error('invalid start offset'),
},
{
description: 'invalid end offset',
start: 0,
end: 1000,
expected: new Error('invalid end offset'),
},
{
description: 'an empty range',
start: 0,
end: 0,
expected: '',
},
{
description: 'a range with end < start',
start: 10,
end: 5,
expected: '',
},
].forEach(({ description, start, end, expected, text }) => {
it(`returns a range with the correct text (${description})`, () => {
if (text) {
start = container.textContent.indexOf(text);
end = start + text.length;
}
if (expected instanceof Error) {
assert.throws(() => {
toRange(container, start, end);
}, expected.message);
} else {
const range = toRange(container, start, end);
assert.equal(range.toString(), expected);
}
});
});
});
});
'use strict';
/**
* Functions to convert between DOM ranges and characters offsets within the
* `textContent` of HTML elements.
*
* These were added to work around issues in `dom-anchor-text-position`'s
* `toRange` implementation. When the issue is resolved upstream, we may still
* want to keep the test suite for this module.
*
* See https://github.com/hypothesis/client/issues/1329
*/
/**
* Convert `start` and `end` character offset positions within the `textContent`
* of a `root` element into a `Range`.
*
* Throws if the `start` or `end` offsets are outside of the range `[0,
* root.textContent.length]`.
*
* @param {HTMLElement} root
* @param {number} start - Character offset within `root.textContent`
* @param {number} end - Character offset within `root.textContent`
* @return {Range} Range spanning text from `start` to `end`
*/
function toRange(root, start, end) {
// The `filter` and `expandEntityReferences` arguments are mandatory in IE
// although optional according to the spec.
const nodeIter = root.ownerDocument.createNodeIterator(
root,
NodeFilter.SHOW_TEXT,
null, // filter
false // expandEntityReferences
);
let startContainer;
let startOffset;
let endContainer;
let endOffset;
let textLength = 0;
let node;
while ((node = nodeIter.nextNode()) && (!startContainer || !endContainer)) {
const nodeText = node.nodeValue;
if (
!startContainer &&
start >= textLength &&
start <= textLength + nodeText.length
) {
startContainer = node;
startOffset = start - textLength;
}
if (
!endContainer &&
end >= textLength &&
end <= textLength + nodeText.length
) {
endContainer = node;
endOffset = end - textLength;
}
textLength += nodeText.length;
}
if (!startContainer) {
throw new Error('invalid start offset');
}
if (!endContainer) {
throw new Error('invalid end offset');
}
const range = root.ownerDocument.createRange();
range.setStart(startContainer, startOffset);
range.setEnd(endContainer, endOffset);
return range;
}
module.exports = {
toRange,
};
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