Commit d5066164 authored by Kyle Keating's avatar Kyle Keating Committed by Kyle Keating

Add to range-js module

range-js.js will eventually replace range.coffee and then rename to range.js once all the methods are converted.

Include 2 initial functions in range-js
- nodeFromXPath
- xpathFromNode
parent 6afb5cca
import { findChild, simpleXPathJQuery, simpleXPathPure } from './xpath-util';
/**
* Wrapper for simpleXPath. Attempts to call the jquery
* version, and if that excepts, then falls back to pure js
* version.
*/
export function xpathFromNode(el, relativeRoot) {
let result;
try {
result = simpleXPathJQuery(el, relativeRoot);
} catch (e) {
// eslint-disable-next-line no-console
console.log(
'jQuery-based XPath construction failed! Falling back to manual.'
);
result = simpleXPathPure(el, relativeRoot);
}
return result;
}
/**
* Finds an element node using an xpath relative to the document root.
*/
export function nodeFromXPath(xpath, root) {
const steps = xpath.substring(1).split('/');
let node = root;
steps.forEach(step => {
let [name, idx] = step.split('[');
idx = idx ? parseInt(idx.split(']')[0]) : 1;
node = findChild(node, name.toLowerCase(), idx);
});
return node;
}
import $ from 'jquery';
import { xpathFromNode, nodeFromXPath, $imports } from '../range-js';
describe('annotator/anchoring/range-js', () => {
describe('xpathFromNode', () => {
let container;
let fakeSimpleXPathJQuery;
let fakeSimpleXPathPure;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
fakeSimpleXPathJQuery = sinon.stub().returns('/div[1]');
fakeSimpleXPathPure = sinon.stub().returns('/div[1]');
$imports.$mock({
'./xpath-util': {
simpleXPathJQuery: fakeSimpleXPathJQuery,
simpleXPathPure: fakeSimpleXPathPure,
},
});
});
afterEach(() => {
container.remove();
});
it('calls `simpleXPathJQuery`', () => {
const xpath = xpathFromNode($(container), document.body);
assert.called(fakeSimpleXPathJQuery);
assert.equal(xpath, '/div[1]');
});
it('calls `simpleXPathPure` if `simpleXPathJQuery` throws an exception', () => {
sinon.stub(console, 'log');
fakeSimpleXPathJQuery.throws(new Error());
const xpath = xpathFromNode($(container), document.body);
assert.called(fakeSimpleXPathPure);
assert.equal(xpath, '/div[1]');
assert.isTrue(
// eslint-disable-next-line no-console
console.log.calledWith(
'jQuery-based XPath construction failed! Falling back to manual.'
)
);
// eslint-disable-next-line no-console
console.log.restore();
});
});
describe('nodeFromXPath', () => {
let container;
let fakeFindChild;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
fakeFindChild = sinon.stub().returns(document.body);
$imports.$mock({
'./xpath-util': {
findChild: fakeFindChild,
},
});
});
afterEach(() => {
container.remove();
});
it('returns the last node returned from `findChild`', () => {
const span = document.createElement('span');
container.appendChild(span);
fakeFindChild.onFirstCall().returns(container);
fakeFindChild.onSecondCall().returns(span);
assert.equal(nodeFromXPath('/div[1]/span[1]', document.body), span);
});
[
{
xpath: '/div[1]',
params: [
{
name: 'div',
idx: 1,
},
],
},
{
xpath: '/div[1]/span[3]/p[1]',
params: [
{
name: 'div',
idx: 1,
},
{
name: 'span',
idx: 3,
},
{
name: 'p',
idx: 1,
},
],
},
{
xpath: '/DIV[2]/TEXT()[3]/SPAN[1]',
params: [
{
name: 'div',
idx: 2,
},
{
name: 'text()',
idx: 3,
},
{
name: 'span',
idx: 1,
},
],
},
].forEach(test => {
it('calls `findChild` with the following node names and indices', () => {
nodeFromXPath(test.xpath, document.body);
test.params.forEach(call => {
assert.calledWith(fakeFindChild, document.body, call.name, call.idx);
});
});
});
});
});
......@@ -10,7 +10,7 @@ import {
} from '../xpath-util';
describe('annotator/anchoring/xpath-util', () => {
describe('#findChild', () => {
describe('findChild', () => {
let container;
beforeEach(() => {
......@@ -37,13 +37,7 @@ describe('annotator/anchoring/xpath-util', () => {
describe('with children', () => {
beforeEach(() => {
container = document.createElement('div');
container.innerHTML = `text 1<span>text 2</span><span>text 3</span>text 4`;
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it('finds the desired text() node at index 1', () => {
......@@ -65,9 +59,12 @@ describe('annotator/anchoring/xpath-util', () => {
});
});
describe('#getTextNodes', () => {
describe('getTextNodes', () => {
let container;
// Helper (expects a jquery array)
const nodeValues = nodes => nodes.map((i, n) => n.nodeValue);
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
......@@ -80,28 +77,29 @@ describe('annotator/anchoring/xpath-util', () => {
it('finds basic text nodes', () => {
container.innerHTML = `<span>text 1</span><span>text 2</span>`;
const result = getTextNodes($(container));
assert.equal(result.length, 2);
assert.equal(result[0].nodeValue, 'text 1');
assert.equal(result[1].nodeValue, 'text 2');
assert.deepEqual(nodeValues(result).toArray(), ['text 1', 'text 2']);
});
it('finds basic text nodes and whitespace', () => {
container.innerHTML = `<span>text 1</span>
<span>text 2</span>`;
const result = getTextNodes($(container));
assert.equal(result.length, 3);
assert.deepEqual(nodeValues(result).toArray(), [
'text 1',
'\n ',
'text 2',
]);
});
it('finds basic text nodes and whitespace but ignores comments', () => {
container.innerHTML = `<span>text 1</span>
<!--span>text 2</span-->`;
const result = getTextNodes($(container));
assert.equal(result.length, 2);
assert.equal(result[0].nodeValue, 'text 1');
assert.deepEqual(nodeValues(result).toArray(), ['text 1', '\n ']);
});
});
describe('#getLastTextNodeUpTo', () => {
describe('getLastTextNodeUpTo', () => {
let container;
beforeEach(() => {
......@@ -119,7 +117,7 @@ describe('annotator/anchoring/xpath-util', () => {
assert.equal(result.nodeValue, 'text');
});
it('gets the last text node nested', () => {
it('gets the last text node (with siblings)', () => {
container.innerHTML = `<span>text first</span><span>text last</span>`;
const result = getLastTextNodeUpTo(container);
assert.equal(result.nodeValue, 'text last');
......@@ -138,7 +136,7 @@ describe('annotator/anchoring/xpath-util', () => {
});
});
describe('#getFirstTextNodeNotBefore', () => {
describe('getFirstTextNodeNotBefore', () => {
let container;
beforeEach(() => {
......@@ -156,7 +154,7 @@ describe('annotator/anchoring/xpath-util', () => {
assert.equal(result.nodeValue, 'text');
});
it('gets the last text node nested', () => {
it('gets the first text node (with siblings)', () => {
container.innerHTML = `<span>text first</span><span>text last</span>`;
const result = getFirstTextNodeNotBefore(container);
assert.equal(result.nodeValue, 'text first');
......@@ -179,7 +177,7 @@ describe('annotator/anchoring/xpath-util', () => {
describe('xpath', () => {
let container;
const html = `<div id="root">
const html = `
<h1 id="h1-1">text</h1>
<p id="p-1">text<br/><br/><a id="a-1">text</a></p>
<p id="p-2">text<br/><em id="em-1"><br/>text</em>text</p>
......@@ -189,12 +187,10 @@ describe('annotator/anchoring/xpath-util', () => {
<li id="li-2">text</li>
<li id="li-3">text</li>
</ul>
</span>
</div>`;
</span>`;
beforeEach(() => {
container = document.createElement('div');
container.setAttribute('id', 'root');
container.innerHTML = html;
document.body.appendChild(container);
});
......@@ -203,7 +199,7 @@ describe('annotator/anchoring/xpath-util', () => {
container.remove();
});
describe('#xpathFromNode', () => {
describe('xpathFromNode', () => {
[
{
id: 'a-1',
......@@ -236,15 +232,19 @@ describe('annotator/anchoring/xpath-util', () => {
].forEach(test => {
it('produces the correct xpath for the provided nodes', () => {
let node = document.getElementById(test.id);
assert.equal(
simpleXPathJQuery($(node), document.querySelector('#root')),
test.xpath
);
assert.equal(simpleXPathJQuery($(node), document.body), test.xpath);
});
});
});
describe('#simpleXPathPure', () => {
describe('simpleXPathPure', () => {
it('throws an error if the provided node is not a descendant of `relativeRoot`', () => {
const node = document.createElement('p'); // not attached to DOM
assert.throws(() => {
simpleXPathPure($(node), document.body);
}, 'Called getPathTo on a node which was not a descendant of @rootNode. [object HTMLBodyElement]');
});
[
{
id: 'a-1',
......@@ -284,10 +284,7 @@ describe('annotator/anchoring/xpath-util', () => {
].forEach(test => {
it('produces the correct xpath for the provided node', () => {
let node = document.getElementById(test.id);
assert.equal(
simpleXPathPure($(node), document.querySelector('#root')),
test.xpaths[0]
);
assert.equal(simpleXPathPure($(node), document.body), test.xpaths[0]);
});
it('produces the correct xpath for the provided text node(s)', () => {
......@@ -302,7 +299,7 @@ describe('annotator/anchoring/xpath-util', () => {
}
textNodes.forEach((node, index) => {
assert.equal(
simpleXPathPure($(node), document.querySelector('#root')),
simpleXPathPure($(node), document.body),
test.xpaths[index + 1]
);
});
......
......@@ -32,12 +32,6 @@ function getNodeName(node) {
if (nodeName === '#text') {
result = 'text()';
}
if (nodeName === '#comment') {
result = 'comment()';
}
if (nodeName === '#cdata-section') {
result = 'cdata-section()';
}
return result;
}
......@@ -76,17 +70,17 @@ export function simpleXPathJQuery(nodes, relativeRoot) {
return paths.get();
}
function getPathSegment(node) {
const name = getNodeName(node);
const pos = getNodePosition(node);
return `${name}[${pos}]`;
}
/**
* A simple XPath evaluator using only standard DOM methods which can
* evaluate queries of the form /tag[index]/tag[index].
*/
export function simpleXPathPure(nodes, relativeRoot) {
function getPathSegment(node) {
const name = getNodeName(node);
const pos = getNodePosition(node);
return `${name}[${pos}]`;
}
let rootNode = relativeRoot;
function getPathTo(node) {
......@@ -106,9 +100,7 @@ export function simpleXPathPure(nodes, relativeRoot) {
xpath = xpath.replace(/\/$/, '');
return xpath;
}
const paths = nodes.map((index, node) => {
return getPathTo(node);
});
const paths = nodes.map((index, node) => getPathTo(node));
return paths.get();
}
......@@ -138,7 +130,7 @@ function flatten(array) {
export function getTextNodes(jq) {
const getTextNodes = node => {
if (node && node.nodeType !== Node.TEXT_NODE) {
let nodes = [];
const nodes = [];
if (node.nodeType !== Node.COMMENT_NODE) {
[...node.childNodes].forEach(child => {
nodes.push(getTextNodes(child));
......@@ -157,22 +149,22 @@ export function getTextNodes(jq) {
/**
* Determine the last text node inside or before the given node.
*/
export function getLastTextNodeUpTo(n) {
switch (n.nodeType) {
export function getLastTextNodeUpTo(node) {
switch (node.nodeType) {
case Node.TEXT_NODE:
return n; // We have found our text node.
return node; // We have found our text node.
case Node.ELEMENT_NODE:
// This is an element, we need to dig in
if (n.lastChild) {
if (node.lastChild) {
// Does it have children at all?
const result = getLastTextNodeUpTo(n.lastChild);
const result = getLastTextNodeUpTo(node.lastChild);
if (result) {
return result;
}
}
}
// Could not find a text node in current node, go backwards
const prev = n.previousSibling;
const prev = node.previousSibling;
if (prev) {
// eslint-disable-next-line no-unused-vars
return getLastTextNodeUpTo(prev);
......@@ -182,24 +174,24 @@ export function getLastTextNodeUpTo(n) {
}
/**
* Determine the first text node in or after the given jQuery node.
* Determine the first text node in or after the given node.
*/
export function getFirstTextNodeNotBefore(n) {
switch (n.nodeType) {
export function getFirstTextNodeNotBefore(node) {
switch (node.nodeType) {
case Node.TEXT_NODE:
return n; // We have found our text node.
return node; // We have found our text node.
case Node.ELEMENT_NODE:
// This is an element, we need to dig in
if (n.firstChild) {
if (node.firstChild) {
// Does it have children at all?
const result = getFirstTextNodeNotBefore(n.firstChild);
const result = getFirstTextNodeNotBefore(node.firstChild);
if (result) {
return result;
}
}
}
// Could not find a text node in current node, go forward
const next = n.nextSibling;
const next = node.nextSibling;
if (next) {
// eslint-disable-next-line no-unused-vars
return getFirstTextNodeNotBefore(next);
......
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