Skip to content

Commit

Permalink
Add XmlNode.xpathGenerate to generate an identifying XPath for all …
Browse files Browse the repository at this point in the history
…nodes.
  • Loading branch information
renggli committed Sep 18, 2023
1 parent 88c0fcb commit d27e3b2
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 0 deletions.
68 changes: 68 additions & 0 deletions lib/src/xpath/generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import '../xml/extensions/sibling.dart';
import '../xml/nodes/attribute.dart';
import '../xml/nodes/cdata.dart';
import '../xml/nodes/comment.dart';
import '../xml/nodes/document.dart';
import '../xml/nodes/element.dart';
import '../xml/nodes/node.dart';
import '../xml/nodes/processing.dart';
import '../xml/nodes/text.dart';

extension XPathGenerator on XmlNode {
/// Returns an XPath string that can be used to query for this [XmlNode].
///
/// If [byId] is giving a fully qualified attribute name, the presence of
/// the attribute causes the generation of a shorter lookup expression.
String xpathGenerate({String? byId}) {
final result = <String>[];
for (XmlNode? current = this; current != null; current = current.parent) {
switch (current) {
case XmlAttribute(qualifiedName: final name):
result.add(_createSegment(current,
where: (each) =>
each is XmlAttribute && each.qualifiedName == name,
filter: '@$name'));
case XmlElement(qualifiedName: final name):
if (byId != null) {
final attribute = current.getAttributeNode(byId);
if (attribute != null) {
result.add('//*[@${attribute.toXmlString()}]');
return result.reversed.join('/');
}
}
result.add(_createSegment(current,
where: (each) => each is XmlElement && each.qualifiedName == name,
filter: name));
case XmlText():
case XmlCDATA():
result.add(_createSegment(current,
where: (each) => each is XmlText || each is XmlCDATA,
filter: 'text()'));
case XmlComment():
result.add(_createSegment(current,
where: (each) => each is XmlComment, filter: 'comment()'));
case XmlProcessing():
result.add(_createSegment(current,
where: (each) => each is XmlProcessing,
filter: 'processing-instruction()'));
case XmlDocument():
result.add(this == current ? '/' : '');
default:
result.add(
_createSegment(current, where: (each) => true, filter: 'node()'));
}
}
return result.reversed.join('/');
}
}

String _createSegment(XmlNode node,
{required bool Function(XmlNode) where, required String filter}) {
final buffer = StringBuffer(filter);
final siblings =
node.hasParent ? node.siblings.where(where).toList() : [node];
if (siblings.length > 1) {
buffer.write('[${1 + siblings.indexOf(node)}]');
}
return buffer.toString();
}
1 change: 1 addition & 0 deletions lib/xpath.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export 'src/xpath/evaluation/functions.dart' show XPathFunction;
export 'src/xpath/evaluation/values.dart';
export 'src/xpath/exceptions/evaluation_exception.dart';
export 'src/xpath/exceptions/parser_exception.dart';
export 'src/xpath/generator.dart' show XPathGenerator;

extension XPathExtension on XmlNode {
/// Returns an iterable over the nodes matching the provided XPath
Expand Down
12 changes: 12 additions & 0 deletions test/xpath_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,18 @@ void main() {
});
});
});
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();
Expand Down

0 comments on commit d27e3b2

Please sign in to comment.