import 'package:meta/meta.dart'; import '../../xml/extensions/comparison.dart'; import '../../xml/nodes/document.dart'; import '../../xml/nodes/element.dart'; import '../../xml/nodes/node.dart'; import '../../xml/nodes/text.dart'; import 'context.dart'; import 'expression.dart'; /// Wrapper of XPath values. @immutable sealed class XPathValue implements XPathExpression { /// The value. dynamic get value; /// Returns the node-set of this value. Iterable get nodes; /// Returns the string of this value. String get string; /// Returns the numerical of this value. num get number; /// Returns the boolean of this value. bool get boolean; } /// Wrapper around an [Iterable] of [XmlNode]s in XPath. class XPathNodeSet implements XPathValue { /// Constructs a new node-set from [nodes]. By default we assume that the /// input require sorting (`isSorted = false`) and deduplication (`isUnique /// = false`). factory XPathNodeSet( Iterable nodes, { bool isSorted = false, bool isUnique = false, }) { if (!isUnique) nodes = nodes.toSet(); final list = nodes.toList(growable: false); if (!isSorted || !isUnique) { list.sort((a, b) => a.compareNodePosition(b)); } return XPathNodeSet._(list); } const XPathNodeSet._(this.value); /// The empty node-set as a reusable object static const empty = XPathNodeSet._([]); @override final List value; @override List get nodes => value; @override String get string { if (nodes.isEmpty) return ''; final buffer = StringBuffer(); _stringForNodeOn(nodes.first, buffer); return buffer.toString(); } void _stringForNodeOn(XmlNode node, StringBuffer buffer) { if (node is XmlDocument) { node = node.rootElement; } if (node is XmlElement) { _stringForElementOn(node, buffer); } else { buffer.write(node.value ?? ''); } } void _stringForElementOn(XmlElement element, StringBuffer buffer) { for (final child in element.children) { if (child is XmlText) { buffer.write(child.value); } else if (child is XmlElement) { _stringForElementOn(child, buffer); } } } @override num get number => num.tryParse(string) ?? double.nan; @override bool get boolean => value.isNotEmpty; @override XPathValue call(XPathContext context) => this; @override String toString() { final buffer = StringBuffer('['); final iterator = value.iterator; var i = 0; while (iterator.moveNext() && i < 3) { if (i > 0) buffer.write(', '); final inner = StringBuffer(); _stringForNodeOn(iterator.current, inner); if (inner.length > 20) { buffer.write(inner.toString().substring(0, 20)); buffer.write('...'); } else { buffer.write(inner.toString()); } i++; } if (i >= 3) buffer.write(', ...'); buffer.write(']'); return buffer.toString(); } } /// Wrapper around a [String] in XPath. class XPathString implements XPathValue { const XPathString(this.value); /// The empty string as a reusable object. static const empty = XPathString(''); @override final String value; @override Iterable get nodes => throw StateError('Unable to convert string "$value" to node-set'); @override String get string => value; @override num get number => num.tryParse(string) ?? double.nan; @override bool get boolean => value.isNotEmpty; @override XPathValue call(XPathContext context) => this; @override String toString() => '"$value"'; } /// Wrapper around a [num] in XPath. class XPathNumber implements XPathValue { const XPathNumber(this.value); @override final num value; @override Iterable get nodes => throw StateError('Unable to convert number $value to node-set'); @override String get string => value == 0 ? '0' : value.toString(); @override num get number => value; @override bool get boolean => value == 0; @override XPathValue call(XPathContext context) => this; @override String toString() => value.toString(); } /// Wrapper around a [bool] in XPath. class XPathBoolean implements XPathValue { const XPathBoolean(this.value); @override final bool value; @override Iterable get nodes => throw StateError('Unable to convert boolean $value to node-set'); @override String get string => value ? 'true' : 'false'; @override num get number => value ? 1 : 0; @override bool get boolean => value; @override XPathValue call(XPathContext context) => this; @override String toString() => '$value()'; }