Commit e8488b43 authored by Robert Knight's avatar Robert Knight

Combine `xpath-util` and `xpath` into a single module

There was no good reason for the separation between the two any more.
parent b4134e52
import { nodeFromXPath } from '../xpath';
import { nodeFromXPath, xpathFromNode } from '../xpath';
describe('annotator/anchoring/xpath', () => {
describe('xpathFromNode', () => {
let container;
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>
<span>
<ul>
<li id="li-1">text1</li>
<li id="li-2">text</li>
<li id="li-3">text</li>
</ul>
</span>`;
beforeEach(() => {
container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it('throws an error if the provided node is not a descendant of the root node', () => {
const node = document.createElement('p'); // not attached to DOM
assert.throws(() => {
xpathFromNode(node, document.body);
}, 'Node is not a descendant of root');
});
[
{
id: 'a-1',
xpaths: ['/div[1]/p[1]/a[1]', '/div[1]/p[1]/a[1]/text()[1]'],
},
{
id: 'h1-1',
xpaths: ['/div[1]/h1[1]', '/div[1]/h1[1]/text()[1]'],
},
{
id: 'p-1',
xpaths: ['/div[1]/p[1]', '/div[1]/p[1]/text()[1]'],
},
{
id: 'a-1',
xpaths: ['/div[1]/p[1]/a[1]', '/div[1]/p[1]/a[1]/text()[1]'],
},
{
id: 'p-2',
xpaths: [
'/div[1]/p[2]',
'/div[1]/p[2]/text()[1]',
'/div[1]/p[2]/text()[2]',
],
},
{
id: 'em-1',
xpaths: ['/div[1]/p[2]/em[1]', '/div[1]/p[2]/em[1]/text()[1]'],
},
{
id: 'li-3',
xpaths: [
'/div[1]/span[1]/ul[1]/li[3]',
'/div[1]/span[1]/ul[1]/li[3]/text()[1]',
],
},
].forEach(test => {
it('produces the correct xpath for the provided node', () => {
let node = document.getElementById(test.id);
assert.equal(xpathFromNode(node, document.body), test.xpaths[0]);
});
it('produces the correct xpath for the provided text node(s)', () => {
let node = document.getElementById(test.id).firstChild;
// collect all text nodes after the target queried node.
const textNodes = [];
while (node) {
if (node.nodeType === Node.TEXT_NODE) {
textNodes.push(node);
}
node = node.nextSibling;
}
textNodes.forEach((node, index) => {
assert.equal(
xpathFromNode(node, document.body),
test.xpaths[index + 1]
);
});
});
});
});
describe('nodeFromXPath', () => {
let container;
const html = `
......
import { xpathFromNode } from '../xpath-util';
describe('annotator/anchoring/xpath-util', () => {
describe('xpathFromNode', () => {
let container;
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>
<span>
<ul>
<li id="li-1">text1</li>
<li id="li-2">text</li>
<li id="li-3">text</li>
</ul>
</span>`;
beforeEach(() => {
container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it('throws an error if the provided node is not a descendant of the root node', () => {
const node = document.createElement('p'); // not attached to DOM
assert.throws(() => {
xpathFromNode(node, document.body);
}, 'Node is not a descendant of root');
});
[
{
id: 'a-1',
xpaths: ['/div[1]/p[1]/a[1]', '/div[1]/p[1]/a[1]/text()[1]'],
},
{
id: 'h1-1',
xpaths: ['/div[1]/h1[1]', '/div[1]/h1[1]/text()[1]'],
},
{
id: 'p-1',
xpaths: ['/div[1]/p[1]', '/div[1]/p[1]/text()[1]'],
},
{
id: 'a-1',
xpaths: ['/div[1]/p[1]/a[1]', '/div[1]/p[1]/a[1]/text()[1]'],
},
{
id: 'p-2',
xpaths: [
'/div[1]/p[2]',
'/div[1]/p[2]/text()[1]',
'/div[1]/p[2]/text()[2]',
],
},
{
id: 'em-1',
xpaths: ['/div[1]/p[2]/em[1]', '/div[1]/p[2]/em[1]/text()[1]'],
},
{
id: 'li-3',
xpaths: [
'/div[1]/span[1]/ul[1]/li[3]',
'/div[1]/span[1]/ul[1]/li[3]/text()[1]',
],
},
].forEach(test => {
it('produces the correct xpath for the provided node', () => {
let node = document.getElementById(test.id);
assert.equal(xpathFromNode(node, document.body), test.xpaths[0]);
});
it('produces the correct xpath for the provided text node(s)', () => {
let node = document.getElementById(test.id).firstChild;
// collect all text nodes after the target queried node.
const textNodes = [];
while (node) {
if (node.nodeType === Node.TEXT_NODE) {
textNodes.push(node);
}
node = node.nextSibling;
}
textNodes.forEach((node, index) => {
assert.equal(
xpathFromNode(node, document.body),
test.xpaths[index + 1]
);
});
});
});
});
});
/**
* Get the node name for use in generating an xpath expression.
*
* @param {Node} node
*/
function getNodeName(node) {
const nodeName = node.nodeName.toLowerCase();
let result = nodeName;
if (nodeName === '#text') {
result = 'text()';
}
return result;
}
/**
* Get the index of the node as it appears in its parent's child list
*
* @param {Node} node
*/
function getNodePosition(node) {
let pos = 0;
/** @type {Node|null} */
let tmp = node;
while (tmp) {
if (tmp.nodeName === node.nodeName) {
pos += 1;
}
tmp = tmp.previousSibling;
}
return pos;
}
function getPathSegment(node) {
const name = getNodeName(node);
const pos = getNodePosition(node);
return `${name}[${pos}]`;
}
/**
* A simple XPath generator which can generate XPaths of the form
* /tag[index]/tag[index].
*
* @param {Node} node - The node to generate a path to
* @param {Node} root - Root node to which the returned path is relative
*/
export function xpathFromNode(node, root) {
let xpath = '';
/** @type {Node|null} */
let elem = node;
while (elem !== root) {
if (!elem) {
throw new Error('Node is not a descendant of root');
}
xpath = getPathSegment(elem) + '/' + xpath;
elem = elem.parentNode;
}
xpath = '/' + xpath;
xpath = xpath.replace(/\/$/, ''); // Remove trailing slash
return xpath;
}
export { xpathFromNode } from './xpath-util';
/**
* Get the node name for use in generating an xpath expression.
*
* @param {Node} node
*/
function getNodeName(node) {
const nodeName = node.nodeName.toLowerCase();
let result = nodeName;
if (nodeName === '#text') {
result = 'text()';
}
return result;
}
/**
* Get the index of the node as it appears in its parent's child list
*
* @param {Node} node
*/
function getNodePosition(node) {
let pos = 0;
/** @type {Node|null} */
let tmp = node;
while (tmp) {
if (tmp.nodeName === node.nodeName) {
pos += 1;
}
tmp = tmp.previousSibling;
}
return pos;
}
function getPathSegment(node) {
const name = getNodeName(node);
const pos = getNodePosition(node);
return `${name}[${pos}]`;
}
/**
* A simple XPath generator which can generate XPaths of the form
* /tag[index]/tag[index].
*
* @param {Node} node - The node to generate a path to
* @param {Node} root - Root node to which the returned path is relative
*/
export function xpathFromNode(node, root) {
let xpath = '';
/** @type {Node|null} */
let elem = node;
while (elem !== root) {
if (!elem) {
throw new Error('Node is not a descendant of root');
}
xpath = getPathSegment(elem) + '/' + xpath;
elem = elem.parentNode;
}
xpath = '/' + xpath;
xpath = xpath.replace(/\/$/, ''); // Remove trailing slash
return xpath;
}
/**
* Return the `index`'th immediate child of `element` whose tag name is
......
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