import 'package:petitparser/reflection.dart'; import 'package:test/test.dart'; import 'package:xml/src/xpath/parser.dart'; import 'package:xml/xml.dart'; import 'package:xml/xpath.dart'; import 'utils/examples.dart'; import 'utils/matchers.dart'; void expectXPath( XmlNode? node, String expression, dynamic matcher, { Map variables = const {}, Map functions = const {}, }) => expect( node!.xpath(expression, variables: variables, functions: functions), matcher is Iterable ? orderedEquals( matcher.map( (each) => each is String ? isA().having( (node) => node.outerXml, 'outerXml', each, ) : each, ), ) : matcher, reason: expression, ); void expectEvaluate( XmlNode? node, String expression, dynamic matcher, { Map variables = const {}, Map functions = const {}, }) => expect( node!.xpathEvaluate(expression, variables: variables, functions: functions), matcher, reason: expression, ); Matcher isNodeSet(dynamic value) => isA().having((value) => value.nodes, 'nodes', value); Matcher isString(dynamic value) => isA().having((value) => value.string, 'string', value); Matcher isNumber(dynamic value) => isA().having((value) => value.number, 'number', value); Matcher isBoolean(dynamic value) => isA().having((value) => value.boolean, 'boolean', value); void main() { group('values', () { group('nodes', () { test('empty', () { const value = XPathNodeSet.empty; expect(value.nodes, isEmpty); expect(value.string, ''); expect(value.number, isNaN); expect(value.boolean, isFalse); expect(value.toString(), '[]'); }); test('document', () { final nodes = [XmlDocument.parse('123')]; final value = XPathNodeSet(nodes); expect(value.nodes, nodes); expect(value.string, '123'); expect(value.number, 123); expect(value.boolean, isTrue); expect(value.toString(), '[123]'); }); test('elements', () { final nodes = XmlDocument.parse( '12', ).rootElement.children; final value = XPathNodeSet(nodes); expect(value.nodes, nodes); expect(value.string, '1'); expect(value.number, 1); expect(value.boolean, isTrue); expect(value.toString(), '[1, 2]'); }); test('attributes', () { final nodes = XmlDocument.parse( '0', ).rootElement.attributes; final value = XPathNodeSet(nodes); expect(value.nodes, nodes); expect(value.string, '1'); expect(value.number, 1); expect(value.boolean, isTrue); expect(value.toString(), '[1, 2]'); }); test('crop long values', () { final nodes = [ XmlDocument.parse( '' '

The quick brown fox jumps over the lazy dog.

' '
', ).rootElement, ]; final value = XPathNodeSet(nodes); expect(value.nodes, nodes); expect(value.string, 'The quick brown fox jumps over the lazy dog.'); expect(value.number, isNaN); expect(value.boolean, isTrue); expect(value.toString(), '[The quick brown fox ...]'); }); test('crop many values', () { final nodes = XmlDocument.parse( '' '

First

' '

Second

' '

Third

' '

Fourth

' '
', ).rootElement.children; final value = XPathNodeSet(nodes); expect(value.nodes, nodes); expect(value.string, 'First'); expect(value.number, isNaN); expect(value.boolean, isTrue); expect(value.toString(), '[First, Second, Third, ...]'); }); test('sorting', () { final nodes = XmlDocument.parse( '', ).rootElement.children; final value = XPathNodeSet([nodes[2], nodes[1], nodes[0]]); expect(value.nodes, nodes); }); test('deduplication', () { final nodes = XmlDocument.parse( '', ).rootElement.children; final value = XPathNodeSet([ nodes[2], nodes[2], nodes[0], nodes[0], nodes[1], ]); expect(value.nodes, nodes); }); }); group('string', () { test('empty', () { const value = XPathString(''); expect(() => value.nodes, throwsStateError); expect(value.string, ''); expect(value.number, isNaN); expect(value.boolean, isFalse); expect(value.toString(), '""'); }); test('full', () { const value = XPathString('123'); expect(() => value.nodes, throwsStateError); expect(value.string, '123'); expect(value.number, 123); expect(value.boolean, isTrue); expect(value.toString(), '"123"'); }); }); group('number', () { test('0', () { const value = XPathNumber(0); expect(() => value.nodes, throwsStateError); expect(value.string, '0'); expect(value.number, 0); expect(value.boolean, isTrue); expect(value.toString(), '0'); }); test('1.14', () { const value = XPathNumber(1.14); expect(() => value.nodes, throwsStateError); expect(value.string, '1.14'); expect(value.number, 1.14); expect(value.boolean, isFalse); expect(value.toString(), '1.14'); }); test('nan', () { const value = XPathNumber(double.nan); expect(() => value.nodes, throwsStateError); expect(value.string, 'NaN'); expect(value.number, isNaN); expect(value.boolean, isFalse); expect(value.toString(), 'NaN'); }); test('+infinity', () { const value = XPathNumber(double.infinity); expect(() => value.nodes, throwsStateError); expect(value.string, 'Infinity'); expect(value.number, double.infinity); expect(value.boolean, isFalse); expect(value.toString(), 'Infinity'); }); test('-infinity', () { const value = XPathNumber(double.negativeInfinity); expect(() => value.nodes, throwsStateError); expect(value.string, '-Infinity'); expect(value.number, double.negativeInfinity); expect(value.boolean, isFalse); expect(value.toString(), '-Infinity'); }); }); group('boolean', () { test('true', () { const value = XPathBoolean(true); expect(() => value.nodes, throwsStateError); expect(value.string, 'true'); expect(value.number, 1); expect(value.boolean, isTrue); expect(value.toString(), 'true()'); }); test('false', () { const value = XPathBoolean(false); expect(() => value.nodes, throwsStateError); expect(value.string, 'false'); expect(value.number, 0); expect(value.boolean, isFalse); expect(value.toString(), 'false()'); }); }); }); group('literals', () { final xml = XmlDocument(); test('number', () { expectEvaluate(xml, '0', isNumber(0)); expectEvaluate(xml, '1', isNumber(1)); expectEvaluate(xml, '-1', isNumber(-1)); expectEvaluate(xml, '1.2', isNumber(1.2)); expectEvaluate(xml, '-1.2', isNumber(-1.2)); }); test('string', () { expectEvaluate(xml, '""', isString('')); expectEvaluate(xml, '"Bar"', isString('Bar')); expectEvaluate(xml, "''", isString('')); expectEvaluate(xml, "'Foo'", isString('Foo')); }); test('variable', () { expectEvaluate( xml, '\$a', isString('hello'), variables: const {'a': XPathString('hello')}, ); expectEvaluate( xml, '\$a', isNumber(123), variables: const {'a': XPathNumber(123)}, ); expectEvaluate( xml, '\$a', isBoolean(false), variables: const {'a': XPathBoolean(false)}, ); expect( () => expectEvaluate(xml, '\$unknown', anything), throwsA(isXPathEvaluationException()), ); }); test('function', () { expectEvaluate( xml, 'custom("hello", 42, true())', isString('ok'), functions: { 'custom': (context, arguments) { expect(context.node, same(xml)); expect(context.position, 1); expect(context.last, 1); expect(arguments, [ isString('hello'), isNumber(42), isBoolean(true), ]); return const XPathString('ok'); }, }, ); expect( () => expectEvaluate(xml, 'unknown()', anything), throwsA(isXPathEvaluationException()), ); }); }); group('functions', () { final xml = XmlDocument.parse('123'); group('nodes', () { test('last()', () { expectEvaluate(xml, 'last()', isNumber(1)); expectEvaluate( xml, '/r/*[last()]', isNodeSet([xml.rootElement.children.last]), ); }); test('position()', () { expectEvaluate(xml, 'position()', isNumber(1)); expectEvaluate( xml, '/r/*[position()]', isNodeSet(xml.rootElement.children), ); }); test('count(node-set)', () { expectEvaluate(xml, 'count(/*)', isNumber(1)); expectEvaluate(xml, 'count(/r/*)', isNumber(2)); expectEvaluate(xml, 'count(/r/b/*)', isNumber(1)); expectEvaluate(xml, 'count(/r/b/absent)', isNumber(0)); }); test('id(object)', () { final xml = XmlDocument.parse('ab'); expectEvaluate(xml, 'id("a b")', isNodeSet(xml.rootElement.children)); expectEvaluate( xml, 'id("b")', isNodeSet([xml.rootElement.children.last]), ); expectEvaluate(xml, 'id(/r/*)', isNodeSet(xml.rootElement.children)); expectEvaluate( xml, 'id(/r/b)', isNodeSet([xml.rootElement.children.last]), ); }); test('local-name(node-set?)', () { expectEvaluate(xml, 'local-name(/r/*)', isString('a')); expectEvaluate( xml, '/r/*[local-name()="a"]', isNodeSet(xml.findAllElements('a')), ); }); test('namespace-uri(node-set?)', () { expectEvaluate(xml, 'namespace-uri(/r/*)', isString('')); expectEvaluate( xml, '/r/*[namespace-uri()=""]', isNodeSet(xml.rootElement.findElements('*')), ); }); test('name(node-set?)', () { expectEvaluate(xml, 'name(/r/*)', isString('a')); expectEvaluate( xml, '/r/*[name()="a"]', isNodeSet(xml.findAllElements('a')), ); }); test('intersect(node-set, node-set)', () { final xml = XmlDocument.parse(''); final children = xml.rootElement.children; expectEvaluate(xml, '(r/*) intersect (r/*)', isNodeSet(children)); expectEvaluate(xml, '(r/*) intersect (r/b)', isNodeSet([children[1]])); expectEvaluate(xml, '(r/b) intersect (r/*)', isNodeSet([children[1]])); expectEvaluate(xml, '(r/b) intersect (r/b)', isNodeSet([children[1]])); expectEvaluate(xml, '(r/a) intersect (r/c)', isNodeSet(isEmpty)); }); test('except(node-set, node-set)', () { final xml = XmlDocument.parse(''); final children = xml.rootElement.children; expectEvaluate(xml, '(r/*) except (r/*)', isNodeSet(isEmpty)); expectEvaluate( xml, '(r/*) except (r/b)', isNodeSet([children[0], children[2]]), ); expectEvaluate(xml, '(r/b) except (r/*)', isNodeSet(isEmpty)); expectEvaluate(xml, '(r/b) except (r/b)', isNodeSet(isEmpty)); expectEvaluate(xml, '(r/a) except (r/c)', isNodeSet([children[0]])); }); test('union(node-set, node-set)', () { final xml = XmlDocument.parse(''); final children = xml.rootElement.children; expectEvaluate(xml, '(r/*) union (r/*)', isNodeSet(children)); expectEvaluate(xml, '(r/a) union (r/a)', isNodeSet([children[0]])); expectEvaluate( xml, '(r/a) union (r/c)', isNodeSet([children[0], children[2]]), ); expectEvaluate( xml, '(r/c) union (r/a)', isNodeSet([children[0], children[2]]), ); }); test('|(node-set, node-set)', () { final xml = XmlDocument.parse(''); final children = xml.rootElement.children; expectEvaluate(xml, '(r/*) | (r/*)', isNodeSet(children)); expectEvaluate(xml, '(r/a) | (r/a)', isNodeSet([children[0]])); expectEvaluate( xml, '(r/a) | (r/c)', isNodeSet([children[0], children[2]]), ); expectEvaluate( xml, '(r/c) | (r/a)', isNodeSet([children[0], children[2]]), ); }); }); group('string', () { test('string(nodes)', () { expectEvaluate(xml, 'string()', isString('123')); expectEvaluate(xml, 'string(/r/b)', isString('23')); }); test('string(string)', () { expectEvaluate(xml, 'string("")', isString('')); expectEvaluate(xml, 'string("hello")', isString('hello')); }); test('string(number)', () { expectEvaluate(xml, 'string(0)', isString('0')); expectEvaluate(xml, 'string(0 div 0)', isString('NaN')); expectEvaluate(xml, 'string(1 div 0)', isString('Infinity')); expectEvaluate(xml, 'string(-1 div 0)', isString('-Infinity')); expectEvaluate(xml, 'string(42)', isString('42')); expectEvaluate(xml, 'string(-42)', isString('-42')); expectEvaluate(xml, 'string(3.1415)', isString('3.1415')); expectEvaluate(xml, 'string(-3.1415)', isString('-3.1415')); }); test('string(boolean)', () { expectEvaluate(xml, 'string(false())', isString('false')); expectEvaluate(xml, 'string(true())', isString('true')); }); test('concat', () { expect( () => expectEvaluate(xml, 'concat()', anything), throwsA(isXPathEvaluationException(name: 'concat')), ); expect( () => expectEvaluate(xml, 'concat("a")', anything), throwsA(isXPathEvaluationException(name: 'concat')), ); expectEvaluate(xml, 'concat("a", "b")', isString('ab')); expectEvaluate(xml, 'concat("a", "b", "c")', isString('abc')); }); test('starts-with', () { expectEvaluate(xml, 'starts-with("abc", "")', isBoolean(true)); expectEvaluate(xml, 'starts-with("abc", "a")', isBoolean(true)); expectEvaluate(xml, 'starts-with("abc", "ab")', isBoolean(true)); expectEvaluate(xml, 'starts-with("abc", "abc")', isBoolean(true)); expectEvaluate(xml, 'starts-with("abc", "abcd")', isBoolean(false)); expectEvaluate(xml, 'starts-with("abc", "bc")', isBoolean(false)); }); test('contains', () { expectEvaluate(xml, 'contains("abc", "")', isBoolean(true)); expectEvaluate(xml, 'contains("abc", "a")', isBoolean(true)); expectEvaluate(xml, 'contains("abc", "b")', isBoolean(true)); expectEvaluate(xml, 'contains("abc", "c")', isBoolean(true)); expectEvaluate(xml, 'contains("abc", "d")', isBoolean(false)); expectEvaluate(xml, 'contains("abc", "ac")', isBoolean(false)); }); test('substring-before', () { expectEvaluate(xml, 'substring-before("abcde", "c")', isString('ab')); expectEvaluate(xml, 'substring-before("abcde", "x")', isString('')); }); test('substring-after', () { expectEvaluate(xml, 'substring-after("abcde", "c")', isString('de')); expectEvaluate(xml, 'substring-after("abcde", "x")', isString('')); expect( () => expectEvaluate(xml, 'substring-after("abcde")', anything), throwsA(isXPathEvaluationException(name: 'substring-after')), ); }); test('substring', () { expectEvaluate(xml, 'substring("12345", 3)', isString('345')); expectEvaluate(xml, 'substring("12345", 2, 3)', isString('234')); expectEvaluate(xml, 'substring("12345", 0, 3)', isString('12')); expectEvaluate(xml, 'substring("12345", 4, 9)', isString('45')); expectEvaluate(xml, 'substring("12345", 1.5, 2.6)', isString('234')); expectEvaluate(xml, 'substring("12345", 0 div 0, 3)', isString('')); expectEvaluate(xml, 'substring("12345", 1, 0 div 0)', isString('')); expectEvaluate( xml, 'substring("12345", -42, 1 div 0)', isString('12345'), ); expectEvaluate( xml, 'substring("12345", -1 div 0, 1 div 0)', isString(''), ); expect( () => expectEvaluate(xml, 'substring("abcde")', anything), throwsA(isXPathEvaluationException(name: 'substring')), ); }); test('string-length', () { expectEvaluate(xml, 'string-length("")', isNumber(0)); expectEvaluate(xml, 'string-length("1")', isNumber(1)); expectEvaluate(xml, 'string-length("12")', isNumber(2)); expectEvaluate(xml, 'string-length("123")', isNumber(3)); }); test('normalize-space', () { expectEvaluate(xml, 'normalize-space("")', isString('')); expectEvaluate(xml, 'normalize-space(" 1 ")', isString('1')); expectEvaluate(xml, 'normalize-space(" 1 2 ")', isString('1 2')); expectEvaluate(xml, 'normalize-space("1 \n2")', isString('1 2')); }); test('translate', () { expectEvaluate(xml, 'translate("bar", "abc", "ABC")', isString('BAr')); expectEvaluate(xml, 'translate("-aaa-", "a-", "A")', isString('AAA')); }); }); group('number', () { test('number(nodes)', () { expectEvaluate(xml, 'number()', isNumber(123)); expectEvaluate(xml, 'number(/r/b)', isNumber(23)); }); test('number(string)', () { expectEvaluate(xml, 'number("")', isNumber(isNaN)); expectEvaluate(xml, 'number("x")', isNumber(isNaN)); expectEvaluate(xml, 'number("1")', isNumber(1)); expectEvaluate(xml, 'number("1.2")', isNumber(1.2)); expectEvaluate(xml, 'number("-1")', isNumber(-1)); expectEvaluate(xml, 'number("-1.2")', isNumber(-1.2)); }); test('number(number)', () { expectEvaluate(xml, 'number(0)', isNumber(0)); expectEvaluate(xml, 'number(-1)', isNumber(-1)); expectEvaluate(xml, 'number(-1.2)', isNumber(-1.2)); }); test('number(boolean)', () { expectEvaluate(xml, 'number(true())', isNumber(1)); expectEvaluate(xml, 'number(false())', isNumber(0)); }); test('sum', () { expectEvaluate(xml, 'sum(//text())', isNumber(6)); final attr = XmlDocument.parse(''); expectEvaluate(attr, 'sum(/r/e/@a)', isNumber(42)); }); test('floor', () { expectEvaluate(xml, 'floor(-1.5)', isNumber(-2)); expectEvaluate(xml, 'floor(1.5)', isNumber(1)); }); test('abs', () { expectEvaluate(xml, 'abs(-2)', isNumber(2)); expectEvaluate(xml, 'abs(3)', isNumber(3)); }); test('ceiling', () { expectEvaluate(xml, 'ceiling(-1.5)', isNumber(-1)); expectEvaluate(xml, 'ceiling(1.5)', isNumber(2)); }); test('round', () { expectEvaluate(xml, 'round(-1.2)', isNumber(-1)); expectEvaluate(xml, 'round(1.2)', isNumber(1)); }); test('- (prefix)', () { expectEvaluate(xml, '-1', isNumber(-1)); expectEvaluate(xml, '--1', isNumber(1)); expectEvaluate(xml, '---1', isNumber(-1)); }); test('+ (prefix)', () { expectEvaluate(xml, '+1', isNumber(1)); expectEvaluate(xml, '++1', isNumber(1)); expectEvaluate(xml, '+++1', isNumber(1)); }); test('+', () { expectEvaluate(xml, '1 + 2', isNumber(3)); expectEvaluate(xml, '3 + 4', isNumber(7)); }); test('-', () { expectEvaluate(xml, '1 - 2', isNumber(-1)); expectEvaluate(xml, '4 - 3', isNumber(1)); }); test('*', () { expectEvaluate(xml, '2 * 3', isNumber(6)); expectEvaluate(xml, '3 * 2', isNumber(6)); }); test('div', () { expectEvaluate(xml, '6 div 3', isNumber(2)); expectEvaluate(xml, '5 div 2', isNumber(2.5)); }); test('idiv', () { expectEvaluate(xml, '5 idiv 2', isNumber(2)); expectEvaluate(xml, '8 idiv 2', isNumber(4)); }); test('neg', () { expectEvaluate(xml, '5 mod 2', isNumber(1)); expectEvaluate(xml, '8 mod 2', isNumber(0)); }); test('priority', () { expectEvaluate(xml, '2 + 3 * 4', isNumber(14)); expectEvaluate(xml, '2 * 3 + 4', isNumber(10)); }); test('parenthesis', () { expectEvaluate(xml, '(2 + 3) * 4', isNumber(20)); expectEvaluate(xml, '2 * (3 + 4)', isNumber(14)); }); }); group('boolean', () { test('boolean(nodes)', () { expectEvaluate(xml, 'boolean()', isBoolean(true)); expectEvaluate(xml, 'boolean(//a)', isBoolean(true)); expectEvaluate(xml, 'boolean(//absent)', isBoolean(false)); }); test('boolean(string)', () { expectEvaluate(xml, 'boolean("")', isBoolean(false)); expectEvaluate(xml, 'boolean("a")', isBoolean(true)); expectEvaluate(xml, 'boolean("ab")', isBoolean(true)); }); test('boolean(number)', () { expectEvaluate(xml, 'boolean(0)', isBoolean(true)); expectEvaluate(xml, 'boolean(1)', isBoolean(false)); expectEvaluate(xml, 'boolean(-1)', isBoolean(false)); expectEvaluate(xml, 'boolean(0 div 0)', isBoolean(false)); expectEvaluate(xml, 'boolean(1 div 0)', isBoolean(false)); }); test('boolean(boolean)', () { expectEvaluate(xml, 'boolean(true())', isBoolean(true)); expectEvaluate(xml, 'boolean(false())', isBoolean(false)); }); test('not(boolean)', () { expectEvaluate(xml, 'not(true())', isBoolean(false)); expectEvaluate(xml, 'not(false())', isBoolean(true)); }); test('true()', () { expectEvaluate(xml, 'true()', isBoolean(true)); }); test('false()', () { expectEvaluate(xml, 'false()', isBoolean(false)); }); test('lang(string)', () { final positives = [ '', '
', '', '', ]; for (final positive in positives) { final xml = XmlDocument.parse(positive); final start = xml.findAllElements('para').first; expectEvaluate(start, 'lang("en")', isBoolean(true)); } final negatives = [ '', '', '', ]; for (final positive in negatives) { final xml = XmlDocument.parse(positive); final start = xml.findAllElements('para').first; expectEvaluate(start, 'lang("en")', isBoolean(false)); } }); test('<', () { expectEvaluate(xml, '1 < 2', isBoolean(isTrue)); expectEvaluate(xml, '2 < 2', isBoolean(isFalse)); expectEvaluate(xml, '2 < 1', isBoolean(isFalse)); }); test('<=', () { expectEvaluate(xml, '1 <= 2', isBoolean(isTrue)); expectEvaluate(xml, '2 <= 2', isBoolean(isTrue)); expectEvaluate(xml, '2 <= 1', isBoolean(isFalse)); }); test('>', () { expectEvaluate(xml, '1 > 2', isBoolean(isFalse)); expectEvaluate(xml, '2 > 2', isBoolean(isFalse)); expectEvaluate(xml, '2 > 1', isBoolean(isTrue)); }); test('>=', () { expectEvaluate(xml, '1 >= 2', isBoolean(isFalse)); expectEvaluate(xml, '2 >= 2', isBoolean(isTrue)); expectEvaluate(xml, '2 >= 1', isBoolean(isTrue)); }); test('=', () { expectEvaluate(xml, '1 = 2', isBoolean(isFalse)); expectEvaluate(xml, '2 = 2', isBoolean(isTrue)); expectEvaluate(xml, '2 = 1', isBoolean(isFalse)); }); test('!=', () { expectEvaluate(xml, '1 != 2', isBoolean(isTrue)); expectEvaluate(xml, '2 != 2', isBoolean(isFalse)); expectEvaluate(xml, '2 != 1', isBoolean(isTrue)); }); test('and', () { expectEvaluate(xml, 'true() and true()', isBoolean(isTrue)); expectEvaluate(xml, 'true() and false()', isBoolean(isFalse)); expectEvaluate(xml, 'false() and true()', isBoolean(isFalse)); expectEvaluate(xml, 'false() and false()', isBoolean(isFalse)); }); test('or', () { expectEvaluate(xml, 'true() or true()', isBoolean(isTrue)); expectEvaluate(xml, 'true() or false()', isBoolean(isTrue)); expectEvaluate(xml, 'false() or true()', isBoolean(isTrue)); expectEvaluate(xml, 'false() or false()', isBoolean(isFalse)); }); }); }); group('axis', () { const input = '' '' ''; final document = XmlDocument.parse(input); final current = document.findAllElements('a2').single; test('..', () { expectXPath(current, '..', [document.rootElement]); }); test('.', () { expectXPath(current, '.', [ '', ]); }); test('/*', () { expectXPath(current, '/*', [document.rootElement]); }); test('//*', () { expectXPath(current, '//*', [ document.rootElement, '', '', '', '', '', '', '', '', ]); }); test('@*', () { expectXPath(current, '@*', ['b1="1"', 'b2="2"']); }); test('ancestor::*', () { expectXPath(current, 'ancestor::*', [document.rootElement]); }); test('ancestor-or-self::*', () { expectXPath(current, 'ancestor-or-self::*', [ document.rootElement, current, ]); }); test('attribute::*', () { expectXPath(current, 'attribute::*', ['b1="1"', 'b2="2"']); }); test('child::*', () { expectXPath(current, 'child::*', ['', '']); }); test('descendant::*', () { expectXPath(current, 'descendant::*', [ '', '', '', ]); }); test('descendant-or-self::*', () { expectXPath(current, 'descendant-or-self::*', [ '', '', '', '', ]); }); test('following::*', () { expectXPath(current, 'following::*', ['', '']); }); test('following-sibling::*', () { expectXPath(current, 'following-sibling::*', ['']); }); test('parent::*', () { expectXPath(current, 'parent::*', [document.rootElement]); }); test('preceding::*', () { expectXPath(current, 'preceding::*', ['', '']); }); test('preceding-sibling::*', () { expectXPath(current, 'preceding-sibling::*', ['']); }); test('self::*', () { expectXPath(current, 'self::*', [ '', ]); }); }); group('node test', () { const input = '' 'text'; final document = XmlDocument.parse(input); final current = document.rootElement; test('*', () { expectXPath(current, '*', ['', '']); }); test('e1', () { expectXPath(current, 'e1', ['']); expectXPath(current, 'e2', ['']); }); test('comment()', () { expectXPath(current, 'comment()', ['']); }); test('node()', () { expectXPath(current, 'node()', current.children); }); test('processing-instruction()', () { expectXPath(current, 'processing-instruction()', ['', '']); }); test('processing-instruction("p2")', () { expectXPath(current, 'processing-instruction("p2")', ['']); }); test('text()', () { expectXPath(current, 'text()', ['text', '']); }); }); group('predicate', () { const input = '' ''; final document = XmlDocument.parse(input); final current = document.rootElement; test('[n]', () { expectXPath(current, '*[1]', ['']); expectXPath(current, '*[2]', ['']); expectXPath(current, '*[3]', ['']); expectXPath(current, '*[4]', isEmpty); }); test('[last()]', () { expectXPath(current, '*[last()]', ['']); expectXPath(current, '*[last()-1]', ['']); expectXPath(current, '*[last()-2]', ['']); expectXPath(current, '*[last()-3]', isEmpty); }); test('[@attr]', () { expectXPath(current, '*[@a]', ['', '']); expectXPath(current, '*[@b]', ['', '']); }); test('[@attr="literal"]', () { expectXPath(current, '*[@a="2"]', ['']); expectXPath(current, '*[@b="3"]', ['']); }); }); group('errors', () { final xml = XmlDocument.parse(''); test('empty', () { expect( () => xml.xpath(''), throwsA( isXPathParserException( message: 'name expected', buffer: '', position: 0, ), ), ); }); test('predicate', () { expect( () => xml.xpath('*[1'), throwsA( isXPathParserException( message: 'end of input expected', buffer: '*[1', position: 1, ), ), ); }); }); group('examples', () { group('https://en.wikipedia.org/wiki/XPath#Examples', () { final document = XmlDocument.parse(wikimediaXml); test('select name attributes for all projects', () { expectXPath(document, '/Wikimedia/projects/project/@name', [ 'name="Wikipedia"', 'name="Wiktionary"', ]); }); test('select all editions of all projects', () { expectXPath( document, '/Wikimedia//editions', document.findAllElements('editions'), ); }); test('selects addresses of all English Wikimedia projects', () { expectXPath( document, '/Wikimedia/projects/project/editions/edition[@language=\'English\']/text()', ['en.wikipedia.org', 'en.wiktionary.org'], ); }); test('selects addresses of all Wikipedias', () { expectXPath( document, '/Wikimedia/projects/project[@name=\'Wikipedia\']/editions/edition/text()', [ 'en.wikipedia.org', 'de.wikipedia.org', 'fr.wikipedia.org', 'pl.wikipedia.org', 'es.wikipedia.org', ], ); }); }); group('https://www.w3.org/TR/1999/REC-xpath-19991116/#path-abbrev', () { final document = XmlDocument.parse(shiporderXsd); final namedNode = { for (final node in document.descendantElements) for (final attribute in node.attributes) if (attribute.qualifiedName == 'name') attribute.value: node, }; // Selects the para element children of the context node. test('para', () { expectXPath(document, 'xsd:schema', document.childElements); expectXPath(document.firstElementChild, 'xsd:element', [ '', '', ]); expectXPath(document, 'unknown', isEmpty); }); // Selects all element children of the context node. test('*', () { expectXPath( namedNode['PurchaseOrderType'], '*', namedNode['PurchaseOrderType']?.childElements, ); expectXPath(namedNode['purchaseOrder'], '*', isEmpty); }); // Selects all text node children of the context node. test('text()', () { final documentation = document .findAllElements('xsd:documentation') .single; expectXPath(documentation, 'text()', [documentation.innerText]); expectXPath(namedNode['shipTo'], 'text()', isEmpty); }); // Selects the name attribute of the context node. test('@name', () { expectXPath(namedNode['purchaseOrder'], '@name', [ 'name="purchaseOrder"', ]); expectXPath(namedNode['purchaseOrder'], '@type', [ 'type="PurchaseOrderType"', ]); expectXPath(namedNode['purchaseOrder'], '@unknown', isEmpty); }); // Selects all the attributes of the context node. test('@*', () { expectXPath(namedNode['purchaseOrder'], '@*', [ 'name="purchaseOrder"', 'type="PurchaseOrderType"', ]); }); // Selects the first para child of the context node. test('para[1]', () { expectXPath(namedNode['USAddress'], 'xsd:sequence[1]', [ namedNode['USAddress']?.firstElementChild, ]); expectXPath(namedNode['USAddress'], 'xsd:attribute[1]', [ namedNode['USAddress']?.lastElementChild, ]); expectXPath(namedNode['USAddress'], 'xsd:sequence[2]', isEmpty); }); // Selects the last para child of the context node. test('para[last()]', () { expectXPath(namedNode['USAddress'], 'xsd:sequence[last()]', [ namedNode['USAddress']?.firstElementChild, ]); expectXPath(namedNode['USAddress'], 'xsd:attribute[last()]', [ namedNode['USAddress']?.lastElementChild, ]); expectXPath(namedNode['USAddress'], 'xsd:sequence[last()-1]', isEmpty); }); // // Selects all para grandchildren of the context node. test('*/para', () { expectXPath(document, '*/xsd:element', [ '', '', ]); expectXPath(document, '*/xsd:attribute', isEmpty); }); // Selects the second section of the fifth chapter of the doc. test('/doc/chapter[5]/section[2]', () { expectXPath( document, '/xsd:schema/xsd:complexType[2]/xsd:attribute[1]', [''], ); expectXPath( document, '/xsd:schema/xsd:complexType[3]/xsd:attribute[1]', isEmpty, ); }); // Selects the para element descendants of the chapter element children of // the context node. test('chapter//para', () { expectXPath(document, 'xsd:schema//xsd:attribute', [ namedNode['orderDate'], namedNode['country'], namedNode['partNum'], ]); expectXPath(document, 'unknown//xsd:attribute', isEmpty); expectXPath(document, 'xsd:schema//unknown', isEmpty); }); // Selects all the para descendants of the document root and thus selects // all para elements in the same document as the context node. test('//para', () { expectXPath(document, '//xsd:attribute', [ namedNode['orderDate'], namedNode['country'], namedNode['partNum'], ]); expectXPath(document, '//unknown', isEmpty); }); // Selects all the item elements in the same document as the context node // that have an olist parent. test('//olist/item', () { expectXPath(document, '//xsd:complexType/xsd:attribute', [ namedNode['orderDate'], namedNode['country'], namedNode['partNum'], ]); expectXPath(document, '//unknown/xsd:attribute', isEmpty); expectXPath(document, '//xsd:complexType/unknown', isEmpty); }); // Selects the context node. test('.', () { expectXPath(document, '.', [document]); expectXPath(namedNode['country'], '.', [namedNode['country']]); }); // Selects the para element descendants of the context node. test('.//para', () { expectXPath(document, './/xsd:attribute', [ namedNode['orderDate'], namedNode['country'], namedNode['partNum'], ]); expectXPath(namedNode['item'], './/xsd:attribute', [ namedNode['partNum'], ]); expectXPath(document, './/unknown', isEmpty); }); // Selects the parent of the context node. test('..', () { expectXPath(document, '..', isEmpty); expectXPath(document.firstElementChild, '..', [document]); }); // Selects the lang attribute of the parent of the context node. test('../@lang', () { expectXPath(namedNode['country'], '../@name', ['name="USAddress"']); expectXPath(namedNode['country'], '../@unknown', isEmpty); }); // Selects all para children of the context node that have a type attribute // with value warning. test('para[@type="warning"]', () { expectXPath(namedNode['USAddress'], 'xsd:attribute[@name="country"]', [ namedNode['country'], ]); expectXPath( namedNode['USAddress'], 'unknown[@name="country"]', isEmpty, ); expectXPath( namedNode['USAddress'], 'xsd:attribute[@name="unknown"]', isEmpty, ); }); // Selects the fifth para child of the context node that has a type // attribute with value warning. test('para[@type="warning"][5]', () { expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[@type="xsd:string"][1]', [namedNode['name']], ); expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[@type="xsd:string"][4]', [namedNode['state']], ); expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[@type="xsd:string"][5]', isEmpty, ); expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[@type="xsd:decimal"][1]', [namedNode['zip']], ); expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[@type="unknown"][1]', isEmpty, ); expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[@unknown="xsd:decimal"][1]', isEmpty, ); }); // Selects the fifth para child of the context node if that child has a type // attribute with value warning. test('para[5][@type="warning"]', () { expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[4][@name="state"]', [namedNode['state']], ); expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[4][@name="unknown"]', isEmpty, ); expectXPath( namedNode['USAddress']?.firstElementChild, 'xsd:element[6][@name="state"]', isEmpty, ); }); // Selects the chapter children of the context node that have one or more // title children with string-value equal to Introduction. test('chapter[title="Introduction"]', () { expectXPath( document.firstElementChild, 'xsd:complexType[xsd:attribute]', [namedNode['PurchaseOrderType'], namedNode['USAddress']], ); expectXPath( document.firstElementChild, 'unknown[xsd:attribute]', isEmpty, ); expectXPath( document.firstElementChild, 'xsd:complexType[unknown]', isEmpty, ); expectXPath( document.firstElementChild, 'xsd:complexType[xsd:attribute="unknown"]', isEmpty, ); }); // Selects the chapter children of the context node that have one or more // title children. test('chapter[title]', () { expectXPath( document.firstElementChild, 'xsd:complexType[xsd:attribute]', [namedNode['PurchaseOrderType'], namedNode['USAddress']], ); expectXPath( document.firstElementChild, 'unknown[xsd:attribute]', isEmpty, ); expectXPath( document.firstElementChild, 'xsd:complexType[unknown]', isEmpty, ); }); // Selects all the employee children of the context node that have both a // secretary attribute and an assistant attribute. test('employee[@secretary and @assistant]', () { expectXPath( namedNode['PurchaseOrderType']?.firstElementChild, 'xsd:element[@name][@type]', [namedNode['shipTo'], namedNode['billTo'], namedNode['items']], ); expectXPath( namedNode['PurchaseOrderType']?.firstElementChild, 'xsd:element[@ref][@minOccurs]', [''], ); expectXPath( namedNode['PurchaseOrderType']?.firstElementChild, 'xsd:element[@unknown][@type]', isEmpty, ); expectXPath( namedNode['PurchaseOrderType']?.firstElementChild, 'xsd:element[@name][@unknown]', isEmpty, ); }); }); }); group('generator', () { for (final MapEntry(key: key, value: value) in allXml.entries) { test(key, () { final document = XmlDocument.parse(value); for (final node in [document, ...document.descendants]) { final expression = node.xpathGenerate(byId: 'id'); final result = document.xpath(expression); expect(result.single, node, reason: expression); } }); } }); group('parser', () { test('linter', () { final parser = const XPathParser().build(); expect(linter(parser), isEmpty); }); }); }