Skip to content

Commit

Permalink
Combine xpath-util and xpath into a single module
Browse files Browse the repository at this point in the history
There was no good reason for the separation between the two any more.
  • Loading branch information
robertknight committed Dec 16, 2020
1 parent b4134e5 commit e8488b4
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 160 deletions.
95 changes: 94 additions & 1 deletion src/annotator/anchoring/test/xpath-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,99 @@
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 = `
Expand Down
96 changes: 0 additions & 96 deletions src/annotator/anchoring/test/xpath-util-test.js

This file was deleted.

62 changes: 0 additions & 62 deletions src/annotator/anchoring/xpath-util.js

This file was deleted.

63 changes: 62 additions & 1 deletion src/annotator/anchoring/xpath.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,65 @@
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
Expand Down

0 comments on commit e8488b4

Please sign in to comment.