Skip to content

Commit 59e8603

Browse files
committed
[XPath] Implement fn:id, fn:element-with-id, and fn:idref
16204 72.0% successes 5 0.0% skipped 3954 17.6% failures 2351 10.4% errors
1 parent 291c2cc commit 59e8603

File tree

4 files changed

+158
-16
lines changed

4 files changed

+158
-16
lines changed

lib/src/xpath/functions/accessor.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import '../types/node.dart';
1212
import '../types/sequence.dart';
1313
import '../types/string.dart';
1414

15-
Object? _defaultToContextItem(XPathContext context) => context.item;
16-
1715
/// https://www.w3.org/TR/xpath-functions-31/#func-node-name
1816
const fnNodeName = XPathFunctionDefinition(
1917
name: 'fn:node-name',
@@ -206,3 +204,6 @@ const fnParseXmlFragment = XPathFunctionDefinition(
206204

207205
XPathSequence _fnParseXmlFragment(XPathContext context, String? arg) =>
208206
throw UnimplementedError('fn:parse-xml-fragment');
207+
208+
XmlNode _defaultToContextItem(XPathContext context) =>
209+
xsNode.cast(context.item);

lib/src/xpath/functions/date_time.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,6 @@ XPathSequence _fnTimezoneFromTime(XPathContext context, DateTime? arg) {
334334
return XPathSequence.single(arg.timeZoneOffset);
335335
}
336336

337-
/// https://www.w3.org/TR/xpath-functions-31/#func-adjust-dateTime-to-timezone
338-
Object _defaultTimezone(XPathContext context) => DateTime.now().timeZoneOffset;
339-
340337
/// https://www.w3.org/TR/xpath-functions-31/#func-adjust-dateTime-to-timezone
341338
const fnAdjustDateTimeToTimezone = XPathFunctionDefinition(
342339
name: 'fn:adjust-dateTime-to-timezone',
@@ -354,7 +351,7 @@ const fnAdjustDateTimeToTimezone = XPathFunctionDefinition(
354351
type: xsDuration,
355352
cardinality: XPathCardinality.zeroOrOne,
356353

357-
defaultValue: _defaultTimezone,
354+
defaultValue: _defaultToTimezone,
358355
),
359356
],
360357
function: _fnAdjustDateTimeToTimezone,
@@ -403,7 +400,7 @@ const fnAdjustDateToTimezone = XPathFunctionDefinition(
403400
type: xsDuration,
404401
cardinality: XPathCardinality.zeroOrOne,
405402

406-
defaultValue: _defaultTimezone,
403+
defaultValue: _defaultToTimezone,
407404
),
408405
],
409406
function: _fnAdjustDateTimeToTimezone,
@@ -426,7 +423,7 @@ const fnAdjustTimeToTimezone = XPathFunctionDefinition(
426423
type: xsDuration,
427424
cardinality: XPathCardinality.zeroOrOne,
428425

429-
defaultValue: _defaultTimezone,
426+
defaultValue: _defaultToTimezone,
430427
),
431428
],
432429
function: _fnAdjustDateTimeToTimezone,
@@ -556,3 +553,6 @@ const fnParseIetfDate = XPathFunctionDefinition(
556553

557554
XPathSequence _fnParseIetfDate(XPathContext context, [String? value]) =>
558555
throw UnimplementedError('fn:parse-ietf-date');
556+
557+
Object _defaultToTimezone(XPathContext context) =>
558+
DateTime.now().timeZoneOffset;

lib/src/xpath/functions/node.dart

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import '../../xml/nodes/processing.dart';
99
import '../definitions/cardinality.dart';
1010
import '../definitions/function.dart';
1111
import '../evaluation/context.dart';
12+
import '../exceptions/evaluation_exception.dart';
1213
import '../types/node.dart';
1314
import '../types/sequence.dart';
1415
import '../types/string.dart';
1516

16-
Object? _defaultToContextItem(XPathContext context) => context.item;
17-
1817
/// https://www.w3.org/TR/xpath-functions-31/#func-name
1918
const fnName = XPathFunctionDefinition(
2019
name: 'fn:name',
@@ -105,12 +104,30 @@ const fnId = XPathFunctionDefinition(
105104
cardinality: XPathCardinality.zeroOrMore,
106105
),
107106
],
108-
optionalArguments: [XPathArgumentDefinition(name: 'node', type: xsNode)],
107+
optionalArguments: [
108+
XPathArgumentDefinition(
109+
name: 'node',
110+
type: xsNode,
111+
cardinality: XPathCardinality.zeroOrOne,
112+
defaultValue: _defaultToContextItem,
113+
),
114+
],
109115
function: _fnId,
110116
);
111117

112118
XPathSequence _fnId(XPathContext context, XPathSequence arg, [XmlNode? node]) {
113-
throw UnimplementedError('fn:id');
119+
final ids = _parseIdStrings(arg);
120+
if (ids.isEmpty) return XPathSequence.empty;
121+
final root = node?.root;
122+
if (root == null) throw XPathEvaluationException('Invalid document');
123+
return XPathSequence(
124+
root.descendantElements.where(
125+
(element) => element.attributes.any(
126+
(attribute) =>
127+
_isIdAttribute(attribute) && ids.contains(attribute.value.trim()),
128+
),
129+
),
130+
);
114131
}
115132

116133
/// https://www.w3.org/TR/xpath-functions-31/#func-element-with-id
@@ -124,7 +141,14 @@ const fnElementWithId = XPathFunctionDefinition(
124141
cardinality: XPathCardinality.zeroOrMore,
125142
),
126143
],
127-
optionalArguments: [XPathArgumentDefinition(name: 'node', type: xsNode)],
144+
optionalArguments: [
145+
XPathArgumentDefinition(
146+
name: 'node',
147+
type: xsNode,
148+
cardinality: XPathCardinality.zeroOrOne,
149+
defaultValue: _defaultToContextItem,
150+
),
151+
],
128152
function: _fnElementWithId,
129153
);
130154

@@ -133,7 +157,19 @@ XPathSequence _fnElementWithId(
133157
XPathSequence arg, [
134158
XmlNode? node,
135159
]) {
136-
throw UnimplementedError('fn:element-with-id');
160+
final ids = _parseIdStrings(arg);
161+
if (ids.isEmpty) return XPathSequence.empty;
162+
final root = node?.root;
163+
if (root == null) throw XPathEvaluationException('Invalid document');
164+
final seen = <String>{};
165+
return XPathSequence(
166+
root.descendantElements.where(
167+
(element) => element.attributes.where(_isIdAttribute).any((attribute) {
168+
final value = attribute.value.trim();
169+
return ids.contains(value) && seen.add(value);
170+
}),
171+
),
172+
);
137173
}
138174

139175
/// https://www.w3.org/TR/xpath-functions-31/#func-idref
@@ -147,7 +183,14 @@ const fnIdref = XPathFunctionDefinition(
147183
cardinality: XPathCardinality.zeroOrMore,
148184
),
149185
],
150-
optionalArguments: [XPathArgumentDefinition(name: 'node', type: xsNode)],
186+
optionalArguments: [
187+
XPathArgumentDefinition(
188+
name: 'node',
189+
type: xsNode,
190+
cardinality: XPathCardinality.zeroOrOne,
191+
defaultValue: _defaultToContextItem,
192+
),
193+
],
151194
function: _fnIdref,
152195
);
153196

@@ -156,7 +199,19 @@ XPathSequence _fnIdref(
156199
XPathSequence arg, [
157200
XmlNode? node,
158201
]) {
159-
throw UnimplementedError('fn:idref');
202+
final ids = _parseIdStrings(arg);
203+
if (ids.isEmpty) return XPathSequence.empty;
204+
final root = node?.root;
205+
if (root == null) throw XPathEvaluationException('Invalid document');
206+
return XPathSequence(
207+
root.descendantElements.expand(
208+
(element) => element.attributes.where(
209+
(attribute) =>
210+
_isIdrefAttribute(attribute) &&
211+
attribute.value.trim().split(_whitespace).any(ids.contains),
212+
),
213+
),
214+
);
160215
}
161216

162217
/// https://www.w3.org/TR/xpath-functions-31/#func-generate-id
@@ -318,3 +373,23 @@ XPathSequence _fnPath(XPathContext context, [XmlNode? node]) {
318373
}
319374
return XPathSequence.single(components.reversed.join('/'));
320375
}
376+
377+
final _whitespace = RegExp(r'\s+');
378+
379+
XmlNode _defaultToContextItem(XPathContext context) =>
380+
xsNode.cast(context.item);
381+
382+
Set<String> _parseIdStrings(XPathSequence arg) => arg
383+
.map(xsString.cast)
384+
.expand((each) => each.split(_whitespace))
385+
.where((each) => each.isNotEmpty)
386+
.toSet();
387+
388+
bool _isIdAttribute(XmlAttribute attr) =>
389+
attr.name.local == 'id' || attr.name.qualified == 'xml:id';
390+
391+
bool _isIdrefAttribute(XmlAttribute attr) =>
392+
attr.name.local == 'idref' ||
393+
attr.name.local == 'idrefs' ||
394+
attr.name.qualified == 'xml:idref' ||
395+
attr.name.qualified == 'xml:idrefs';

test/xpath/functions/node_test.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,71 @@ void main() {
131131
isXPathSequence([children[1]]),
132132
);
133133
});
134+
135+
test('fn:id', () {
136+
final xml = XmlDocument.parse(
137+
'<r><a id="1" xml:id="2"/><b id="3"/><c xml:id="5"/><d xml:id="4"/></r>',
138+
);
139+
final a = xml.findAllElements('a').single;
140+
final b = xml.findAllElements('b').single;
141+
final c = xml.findAllElements('c').single;
142+
final d = xml.findAllElements('d').single;
143+
144+
expectEvaluate(xml, 'id("1")', isXPathSequence([a]));
145+
expectEvaluate(xml, 'id("2")', isXPathSequence([a]));
146+
expectEvaluate(xml, 'id("3")', isXPathSequence([b]));
147+
expectEvaluate(xml, 'id("4")', isXPathSequence([d]));
148+
expectEvaluate(xml, 'id("5")', isXPathSequence([c]));
149+
expectEvaluate(xml, 'id("1 3")', isXPathSequence([a, b]));
150+
expectEvaluate(xml, 'id(("1", "5"))', isXPathSequence([a, c]));
151+
expectEvaluate(xml, 'id("unknown")', isXPathSequence(<XmlNode>[]));
152+
});
153+
154+
test('fn:element-with-id', () {
155+
final xml = XmlDocument.parse(
156+
'<r><a id="1" xml:id="2"/><b id="3"/><c xml:id="5"/><d xml:id="4"/></r>',
157+
);
158+
final a = xml.findAllElements('a').single;
159+
final b = xml.findAllElements('b').single;
160+
final d = xml.findAllElements('d').single;
161+
162+
expectEvaluate(xml, 'element-with-id("1")', isXPathSequence([a]));
163+
expectEvaluate(xml, 'element-with-id("2")', isXPathSequence([a]));
164+
expectEvaluate(xml, 'element-with-id("3")', isXPathSequence([b]));
165+
expectEvaluate(xml, 'element-with-id("4")', isXPathSequence([d]));
166+
expectEvaluate(xml, 'element-with-id("1 3")', isXPathSequence([a, b]));
167+
expectEvaluate(
168+
xml,
169+
'element-with-id(("1", "3"))',
170+
isXPathSequence([a, b]),
171+
);
172+
});
173+
174+
test('fn:idref', () {
175+
final xml = XmlDocument.parse(
176+
'<r><a idref="1" xml:idref="2"/><b idrefs="3"/><c idref="1 2"/><d xml:idrefs="4"/></r>',
177+
);
178+
179+
final aIdref = xml.findAllElements('a').single.attributes[0];
180+
final aXmlidref = xml.findAllElements('a').single.attributes[1];
181+
final bIdrefs = xml.findAllElements('b').single.attributes[0];
182+
final cIdref = xml.findAllElements('c').single.attributes[0];
183+
final dXmlidrefs = xml.findAllElements('d').single.attributes[0];
184+
185+
expectEvaluate(xml, 'idref("1")', isXPathSequence([aIdref, cIdref]));
186+
expectEvaluate(xml, 'idref("2")', isXPathSequence([aXmlidref, cIdref]));
187+
expectEvaluate(xml, 'idref("3")', isXPathSequence([bIdrefs]));
188+
expectEvaluate(xml, 'idref("4")', isXPathSequence([dXmlidrefs]));
189+
expectEvaluate(
190+
xml,
191+
'idref("1 3")',
192+
isXPathSequence([aIdref, bIdrefs, cIdref]),
193+
);
194+
expectEvaluate(
195+
xml,
196+
'idref(("1", "3"))',
197+
isXPathSequence([aIdref, bIdrefs, cIdref]),
198+
);
199+
});
134200
});
135201
}

0 commit comments

Comments
 (0)