Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/goto100/xpath into unshift
Browse files Browse the repository at this point in the history
  • Loading branch information
JLRishe committed Dec 16, 2023
2 parents fb45c01 + 4c181e8 commit 25f5d2e
Show file tree
Hide file tree
Showing 5 changed files with 1,292 additions and 65 deletions.
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xpath",
"version": "0.0.33",
"version": "0.0.34",
"description": "DOM 3 XPath implemention and helper for node.js and the web",
"engines": {
"node": ">=0.6.0"
Expand All @@ -19,6 +19,7 @@
"devDependencies": {
"@xmldom/xmldom": "^0.8.9",
"es-check": "^7.1.1",
"func-xml": "^0.0.10",
"mocha": "^9.0.2"
},
"typings": "./xpath.d.ts",
Expand All @@ -30,7 +31,10 @@
"type": "git",
"url": "https://github.com/goto100/xpath.git"
},
"main": "./xpath.js",
"main": "xpath.js",
"files": [
"xpath.d.ts"
],
"keywords": [
"xpath",
"xml"
Expand Down
294 changes: 290 additions & 4 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { allChildEls } = require('func-xml');
const xpath = require('./xpath.js');
const dom = require('@xmldom/xmldom').DOMParser;
const assert = require('assert');
const { strict: assert } = require('assert');

var xhtmlNs = 'http://www.w3.org/1999/xhtml';

Expand Down Expand Up @@ -356,7 +357,17 @@ describe('xpath', () => {
});

it('should correctly evaluate context position', () => {
var doc = parseXml("<books><book><chapter>The boy who lived</chapter><chapter>The vanishing glass</chapter></book><book><chapter>The worst birthday</chapter><chapter>Dobby's warning</chapter><chapter>The burrow</chapter></book></books>");
var doc = parseXml(`<books>
<book>
<chapter>The boy who lived</chapter>
<chapter>The vanishing glass</chapter>
</book>
<book>
<chapter>The worst birthday</chapter>
<chapter>Dobby's warning</chapter>
<chapter>The burrow</chapter>
</book>
</books>`);

var chapters = xpath.parse('/books/book/chapter[2]').select({ node: doc });

Expand All @@ -379,6 +390,189 @@ describe('xpath', () => {

assert.strictEqual(1, lastChapter.length);
assert.strictEqual("The burrow", lastChapter[0].textContent);

// #135 - issues with context position
const blockQuotes = parseXml(`<html>
<body>
<div>
<blockquote type="cite" class="blockquote">
<div id="1234">
<span style="font-family: 'Times New Roman', serif; font-size: 16px; line-height: normal;">
This is a test!
</span>
</div>
</blockquote>
</div>
<div><br/></div>
<blockquote style="margin-Top: 0px; margin-Bottom: 0px; margin-Left: 0.5em">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote type="cite" class="front-blockquote" style="margin-top: 0px;">
<blockquote>
<span style="font-family: 'Times New Roman', serif; font-size: 16px; line-height: normal;">
This is also a test!
</span>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</body>
</html>`);

const x = xpath
.parse(`(.//*[local-name(.)='blockquote'])[not(@class="gmail_quote") and not(ancestor::*[local-name() = 'blockquote'])][last()]`)
.select({ node: blockQuotes });

assert.strictEqual(1, x.length);

assert.strictEqual('This is also a test!', x[0].textContent.trim());
});

it('should select and sort namespace nodes properly', () => {
// #83

const doc = parseXml('<book xmlns:b="http://book.com" xmlns="default-book" xmlns:a="http://author.com" xmlns:p="http://publisher"/>');

const namespaces = xpath.parse('/*/namespace::*').select({ node: doc });

assert.strictEqual(5, namespaces.length);

assert.equal('http://www.w3.org/XML/1998/namespace', namespaces[0].nodeValue);
assert.equal('xml', namespaces[0].localName);

assert.equal('http://book.com', namespaces[1].nodeValue);
assert.equal('b', namespaces[1].localName);

assert.equal('default-book', namespaces[2].nodeValue);
assert.equal('', namespaces[2].localName);

assert.equal('http://author.com', namespaces[3].nodeValue);
assert.equal('a', namespaces[3].localName);

assert.equal('http://publisher', namespaces[4].nodeValue);
assert.equal('p', namespaces[4].localName);
});

it('should allow using node positions', () => {
const doc = parseXml(`<books>
<book>
<chapter>Chapter 1</chapter>
<chapter>Chapter 2</chapter>
<chapter>Chapter 3</chapter>
<chapter>Chapter 4</chapter>
</book>
<book>
<chapter>1章</chapter>
<chapter>2章</chapter>
<chapter>3章</chapter>
<chapter>4章</chapter>
</book>
</books>`)

assert.equal(
xpath.parse('/*/book/chapter[1]').evaluateString({ node: doc }),
'Chapter 1',
);

assert.equal(
xpath.parse('/*/book/chapter[2]').evaluateString({ node: doc }),
'Chapter 2',
);

assert.equal(
xpath.parse('/*/book/chapter[3]').evaluateString({ node: doc }),
'Chapter 3',
);

assert.equal(
xpath.parse('/*/book[2]/chapter[1]').evaluateString({ node: doc }),
'1章',
);

assert.equal(
xpath.parse('/*/book/chapter[5]').evaluateString({ node: doc }),
'',
);

assert.equal(
xpath.parse('(/*/book/chapter)[5]').evaluateString({ node: doc }),
'1章',
);

const pos1Nodes = xpath.parse('/*/book/chapter[1]').select({ node: doc });

assert.equal(pos1Nodes.length, 2);

assert.equal(pos1Nodes[0].textContent, 'Chapter 1');
assert.equal(pos1Nodes[1].textContent, '1章');

const first3Nodes = xpath.parse('/*/book/chapter[position() <= 3]').select({ node: doc });

assert.equal(first3Nodes.length, 6);

assert.equal(first3Nodes[5].textContent, '3章');
});

it('should respect reverse axes', () => {
const doc = parseXml(`<book>
<chapter>Chapter 1</chapter>
<chapter>Chapter 2</chapter>
<chapter>Chapter 3</chapter>
<chapter>Chapter 4</chapter>
</book>`)

assert.equal(
xpath.parse('/*/chapter[last()]/preceding-sibling::*[1]').evaluateString({ node: doc }),
'Chapter 3',
);

assert.equal(
xpath.parse('/*/chapter[last()]/preceding-sibling::*[2]').evaluateString({ node: doc }),
'Chapter 2',
);

assert.equal(
xpath.parse('/*/chapter[last()]/preceding-sibling::*[3]').evaluateString({ node: doc }),
'Chapter 1',
);

assert.equal(
xpath.parse('/*/chapter[last()]/preceding-sibling::*[4]').evaluateString({ node: doc }),
'',
);

assert.equal(
xpath.parse('/*/chapter[last()]/preceding-sibling::*[last()]').evaluateString({ node: doc }),
'Chapter 1',
);

assert.equal(
xpath.parse('/*/chapter[last()]/preceding::chapter[last()]').evaluateString({ node: doc }),
'Chapter 1',
);

assert.equal(
xpath.parse('/*/chapter[last()]/preceding::*[position() = 1]').evaluateString({ node: doc }),
'Chapter 3',
);

assert.equal(
xpath.parse('/*/chapter[last()]/preceding::*[. != "Chapter 3"][1]').evaluateString({ node: doc }),
'Chapter 2',
);
});
});

Expand Down Expand Up @@ -485,6 +679,44 @@ describe('xpath', () => {

assert.strictEqual(str, 'e');
});

it('should get string values from namespace nodes', () => {
const doc = parseXml('<book xmlns:author="http://author" xmlns="https://book" />');

assert.equal(
xpath.parse('string(/*/namespace::author)').evaluateString({ node: doc }),
'http://author'
);
assert.equal(
xpath.parse('name(/*/namespace::author)').evaluateString({ node: doc }),
'author'
);
assert.equal(
xpath.parse('local-name(/*/namespace::author)').evaluateString({ node: doc }),
'author'
);
assert.equal(
xpath.parse('namespace-uri(/*/namespace::author)').evaluateString({ node: doc }),
''
);

assert.equal(
xpath.parse('string(/*/namespace::*[not(local-name())])').evaluateString({ node: doc }),
'https://book'
);
assert.equal(
xpath.parse('name(/*/namespace::*[not(local-name())])').evaluateString({ node: doc }),
''
);
assert.equal(
xpath.parse('local-name(/*/namespace::*[not(local-name())])').evaluateString({ node: doc }),
''
);
assert.equal(
xpath.parse('namespace-uri(/*/namespace::*[not(local-name())])').evaluateString({ node: doc }),
''
);
});
});

describe('parsed expressions', () => {
Expand Down Expand Up @@ -965,18 +1197,35 @@ describe('xpath', () => {

assert.strictEqual('Heyy', translated);

var characters = parseXml('<characters><character>Harry</character><character>Ron</character><character>Hermione</character></characters>');
var characters = parseXml(`<characters>
<character>Harry</character>
<character>Ron</character>
<character>Hermione</character>
</characters>`);

var firstTwo = xpath.parse('/characters/character[position() <= 2]').select({ node: characters });

assert.strictEqual(2, firstTwo.length);
assert.strictEqual('Harry', firstTwo[0].textContent);
assert.strictEqual('Ron', firstTwo[1].textContent);

var last = xpath.parse('/characters/character[last()]').select({ node: characters });
const last = xpath.parse('/characters/character[last()]').select({ node: characters });

assert.strictEqual(1, last.length);
assert.strictEqual('Hermione', last[0].textContent);

const lastPrefiltered = xpath.parse('/characters/character[. != "Hermione"][last()]').select({ node: characters });

assert.strictEqual(1, lastPrefiltered.length);
assert.strictEqual('Ron', lastPrefiltered[0].textContent);

const lastStrict = xpath.parse('/characters/character[last() = 3]').select({ node: characters, });

assert.equal(3, lastStrict.length);

const lastStrictMiss = xpath.parse('/characters/character[last() = 2]').select({ node: characters, });

assert.equal(0, lastStrictMiss.length);
});
});

Expand Down Expand Up @@ -1052,6 +1301,43 @@ describe('xpath', () => {
});
});

describe('error handling', () => {
it('should reject unspecified expression', () => {
for (let expr of [null, undefined, '']) {
assert.throws(() => xpath.parse(expr), {
message: 'XPath expression unspecified.',
});
}
});

it('should reject non-string expression', () => {
for (let expr of [{}, []]) {
assert.throws(() => xpath.parse(expr), {
message: 'XPath expression must be a string.',
});
}
});
it('should reject non-nodes', () => {
for (let node of ['<n />', 0, 45, true, false, [], {}]) {
assert.throws(() => xpath.parse('/*').select({ node }), {
message: 'Context node does not appear to be a valid DOM node.',
});
}
});

it('should handle unspecified nodes', () => {
assert.throws(
() => xpath.parse('my:field').select(), {
message: 'Context node not found when evaluating XPath step: child::my:field',
});

assert.throws(
() => xpath.parse('/*').select(), {
message: 'Context node not found when determining document root.',
});
})
});

describe('Node type tests', () => {
it('should correctly identify a Node of type Element', () => {
var doc = parseXml('<book />');
Expand Down
2 changes: 1 addition & 1 deletion xpath.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function useNamespaces(namespaceMap: Record<string, string>): XPathSelect

// Type guards to narrow down the type of the selected type of a returned Node object
export function isNodeLike(value: SelectedValue): value is Node;
export function isArrayOfNodes(value: SelectedValue): value is Node[];
export function isArrayOfNodes(value: SelectReturnType): value is Node[];
export function isElement(value: SelectedValue): value is Element;
export function isAttribute(value: SelectedValue): value is Attr;
export function isTextNode(value: SelectedValue): value is Text;
Expand Down
Loading

0 comments on commit 25f5d2e

Please sign in to comment.