Skip to main content

libxml_rs/xml/xpath/
functions.rs

1//! XPath 1.0 Core Function Library (§25).
2//!
3//! Implements all XPath 1.0 core functions as specified in §4 of the
4//! XPath 1.0 Recommendation.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! All functions follow the XPath 1.0 specification and libxml2
9//! observable behavior, including edge cases and historical quirks.
10//!
11//! # Courts
12//!
13//! XPATH-FUNCTIONS-*
14//!
15//! # Upstream contract
16//!
17//! Mirrors the core-function library of upstream `xpath.c`
18//! (`SRC-LIBXML2-2.15.0-XPATH-C`, parity target libxml2 2.15.3 oracle):
19//! the 25 XPath 1.0 §4 functions (node-set, string, boolean, number
20//! groups) with libxml2 observable edge cases.
21//!
22//! # Conceptual behavior
23//!
24//! Implements each function over already-evaluated `XPathValue` arguments
25//! with the upstream argument-count and coercion behavior: node-set
26//! functions (last, position, count, id, local-name, namespace-uri, name),
27//! string functions (string, concat, starts-with, substring, translate,
28//! ...), boolean functions and number functions (number, sum, floor,
29//! ceiling, round). number()/string() route through the R-000166 number
30//! formatter (1e9/1e-5 scientific threshold, DBL_DIG=15 fraction digits).
31//!
32//! # Ownership & safety invariants
33//!
34//! Functions return owned `XPathValue`s; node-set arguments are borrowed
35//! views over the tree (valid for the call). No function stores or caches
36//! argument pointers — values are copied at the boundary, so the registry
37//! is safe to share.
38//!
39//! # Historical quirks & epochs
40//!
41//! R-000114 (attribute string-value must be the attribute content, not
42//! empty) and R-000166 (full double-precision value-of printing) were
43//! fixed against the 2.15.3 oracle; the number() corpus (967/967 cases)
44//! locks the formatting epoch. The E-008 stable libxslt epoch means any
45//! function-level divergence is a candidate bug, not an epoch difference.
46//!
47//! # Deliberate oddities
48//!
49//! round()/floor()/ceiling() reproduce libxml2 IEEE-754 handling
50//! (including negative zero and NaN propagation) rather than Rust
51//! rounding helpers, which differ on ties and sign.
52//!
53//! # Proving courts
54//!
55//! XPATH-FUNCTIONS-* differential probes and the 967/967 number() corpus
56//! compare results byte-identical against the oracle; the XSLT courts
57//! (CLI-XSLTPROC-0014/0015/0017) exercise value-of/format-number through
58//! these functions.
59//!
60//! # Tempting simplifications that would break parity
61//!
62//! Do not delegate number formatting to Rust float formatting: the
63//! scientific threshold, digit counts and exponent padding are
64//! oracle-observable (R-000166). Do not coerce arguments more eagerly
65//! than upstream (e.g. empty node-sets to string) — R-000114 proved the
66//! string-value rules are observable.
67
68use crate::xml::xpath::context::XPathContext;
69use crate::xml::xpath::types::{node_string_value, string_to_number, NodeSet, XPathValue};
70use std::collections::HashMap;
71
72/// Type alias for XPath functions.
73///
74/// Functions receive already-evaluated arguments as `XPathValue` slices.
75pub type XPathFunction = fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>;
76
77/// Get all registered XPath core functions.
78pub fn core_functions() -> HashMap<String, XPathFunction> {
79    let mut funcs: HashMap<String, XPathFunction> = HashMap::new();
80
81    // Node set functions (§4.1)
82    funcs.insert("last".into(), fn_last);
83    funcs.insert("position".into(), fn_position);
84    funcs.insert("count".into(), fn_count);
85    funcs.insert("id".into(), fn_id);
86    funcs.insert("local-name".into(), fn_local_name);
87    funcs.insert("namespace-uri".into(), fn_namespace_uri);
88    funcs.insert("name".into(), fn_name);
89
90    // String functions (§4.2)
91    funcs.insert("string".into(), fn_string);
92    funcs.insert("concat".into(), fn_concat);
93    funcs.insert("starts-with".into(), fn_starts_with);
94    funcs.insert("contains".into(), fn_contains);
95    funcs.insert("substring-before".into(), fn_substring_before);
96    funcs.insert("substring-after".into(), fn_substring_after);
97    funcs.insert("substring".into(), fn_substring);
98    funcs.insert("string-length".into(), fn_string_length);
99    funcs.insert("normalize-space".into(), fn_normalize_space);
100    funcs.insert("translate".into(), fn_translate);
101
102    // Boolean functions (§4.3)
103    funcs.insert("boolean".into(), fn_boolean);
104    funcs.insert("not".into(), fn_not);
105    funcs.insert("true".into(), fn_true);
106    funcs.insert("false".into(), fn_false);
107    funcs.insert("lang".into(), fn_lang);
108
109    // Number functions (§4.4)
110    funcs.insert("number".into(), fn_number);
111    funcs.insert("sum".into(), fn_sum);
112    funcs.insert("floor".into(), fn_floor);
113    funcs.insert("ceiling".into(), fn_ceiling);
114    funcs.insert("round".into(), fn_round);
115
116    funcs
117}
118
119// ═══════════════════════════════════════════════════════════════════════════════
120// Helper: extract typed arguments
121// ═══════════════════════════════════════════════════════════════════════════════
122
123fn get_string_arg(args: &[XPathValue], index: usize) -> String {
124    if index < args.len() {
125        args[index].as_string()
126    } else {
127        String::new()
128    }
129}
130
131fn get_number_arg(args: &[XPathValue], index: usize) -> f64 {
132    if index < args.len() {
133        args[index].as_number()
134    } else {
135        f64::NAN
136    }
137}
138
139fn get_boolean_arg(args: &[XPathValue], index: usize) -> bool {
140    if index < args.len() {
141        args[index].as_boolean()
142    } else {
143        false
144    }
145}
146
147fn get_node_set_arg(args: &[XPathValue], index: usize) -> NodeSet {
148    if index < args.len() {
149        match &args[index] {
150            XPathValue::NodeSet(ns) => ns.clone(),
151            _ => NodeSet::new(),
152        }
153    } else {
154        NodeSet::new()
155    }
156}
157
158fn get_first_node(
159    ctx: &XPathContext,
160    args: &[XPathValue],
161    index: usize,
162) -> Option<*mut crate::abi::structs::_xmlNode> {
163    if index < args.len() {
164        match &args[index] {
165            XPathValue::NodeSet(ns) => ns.first(),
166            _ => None,
167        }
168    } else {
169        // Default to context node
170        Some(ctx.context_node)
171    }
172}
173
174// ═══════════════════════════════════════════════════════════════════════════════
175// Node Set Functions (§4.1)
176// ═══════════════════════════════════════════════════════════════════════════════
177
178/// last() — context size.
179const fn fn_last(ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
180    Ok(XPathValue::Number(ctx.last() as f64))
181}
182
183/// position() — context position.
184const fn fn_position(ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
185    Ok(XPathValue::Number(ctx.position() as f64))
186}
187
188/// count(node-set) — number of nodes in node-set.
189fn fn_count(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
190    let ns = get_node_set_arg(args, 0);
191    Ok(XPathValue::Number(ns.len() as f64))
192}
193
194/// id(object) — select elements by ID.
195const fn fn_id(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
196    // id() is complex: requires DTD validation to know which attributes are ID.
197    // For now, return empty node-set.
198    Ok(XPathValue::NodeSet(NodeSet::new()))
199}
200
201/// local-name(node-set?) — local part of name.
202///
203/// # Safety
204///
205/// - The node returned by `get_first_node` must be NULL or a valid
206///   `_xmlNode` that stays alive for the call; its `name` field must be
207///   NULL or a valid NUL-terminated string.
208fn fn_local_name(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
209    let node = get_first_node(ctx, args, 0);
210    if let Some(node) = node {
211        unsafe {
212            let name = crate::xml::string::xmlstr_to_string((*node).name);
213            // Strip prefix if present
214            if let Some(pos) = name.find(':') {
215                Ok(XPathValue::String(name[pos + 1..].to_string()))
216            } else {
217                Ok(XPathValue::String(name))
218            }
219        }
220    } else {
221        Ok(XPathValue::String(String::new()))
222    }
223}
224
225/// namespace-uri(node-set?) — namespace URI of node.
226///
227/// # Safety
228///
229/// - The node returned by `get_first_node` must be NULL or a valid
230///   `_xmlNode` that stays alive for the call; its `ns` pointer, when
231///   non-NULL, must point to a valid `_xmlNs` whose `href` is NULL or a
232///   valid NUL-terminated string.
233fn fn_namespace_uri(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
234    let node = get_first_node(ctx, args, 0);
235    if let Some(node) = node {
236        unsafe {
237            if let Some(ns) = (*node).ns.as_ref() {
238                let uri = crate::xml::string::xmlstr_to_string(ns.href);
239                Ok(XPathValue::String(uri))
240            } else {
241                Ok(XPathValue::String(String::new()))
242            }
243        }
244    } else {
245        Ok(XPathValue::String(String::new()))
246    }
247}
248
249/// name(node-set?) — QName of node.
250///
251/// # Safety
252///
253/// - The node returned by `get_first_node` must be NULL or a valid
254///   `_xmlNode` that stays alive for the call; its `name` field must be
255///   NULL or a valid NUL-terminated string.
256fn fn_name(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
257    let node = get_first_node(ctx, args, 0);
258    if let Some(node) = node {
259        unsafe {
260            let name = crate::xml::string::xmlstr_to_string((*node).name);
261            Ok(XPathValue::String(name))
262        }
263    } else {
264        Ok(XPathValue::String(String::new()))
265    }
266}
267
268// ═══════════════════════════════════════════════════════════════════════════════
269// String Functions (§4.2)
270// ═══════════════════════════════════════════════════════════════════════════════
271
272/// string(object?) — convert to string.
273fn fn_string(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
274    if args.is_empty() {
275        // Default: context node's string value
276        Ok(XPathValue::String(node_string_value(ctx.context_node)))
277    } else {
278        Ok(XPathValue::String(args[0].as_string()))
279    }
280}
281
282/// concat(string, string, ...) — concatenate strings.
283fn fn_concat(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
284    let mut result = String::new();
285    for arg in args {
286        result.push_str(&arg.as_string());
287    }
288    Ok(XPathValue::String(result))
289}
290
291/// starts-with(string1, string2) — check prefix.
292fn fn_starts_with(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
293    let s1 = get_string_arg(args, 0);
294    let s2 = get_string_arg(args, 1);
295    Ok(XPathValue::Boolean(s1.starts_with(&s2)))
296}
297
298/// contains(string1, string2) — check substring.
299fn fn_contains(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
300    let s1 = get_string_arg(args, 0);
301    let s2 = get_string_arg(args, 1);
302    Ok(XPathValue::Boolean(s1.contains(&s2)))
303}
304
305/// substring-before(string1, string2) — before first occurrence.
306fn fn_substring_before(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
307    let s1 = get_string_arg(args, 0);
308    let s2 = get_string_arg(args, 1);
309    if let Some(pos) = s1.find(&s2) {
310        Ok(XPathValue::String(s1[..pos].to_string()))
311    } else {
312        Ok(XPathValue::String(String::new()))
313    }
314}
315
316/// substring-after(string1, string2) — after first occurrence.
317fn fn_substring_after(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
318    let s1 = get_string_arg(args, 0);
319    let s2 = get_string_arg(args, 1);
320    if let Some(pos) = s1.find(&s2) {
321        Ok(XPathValue::String(s1[pos + s2.len()..].to_string()))
322    } else {
323        Ok(XPathValue::String(String::new()))
324    }
325}
326
327/// substring(string, number, number?) — substring extraction.
328///
329/// UPSTREAM-PARITY: XPath substring uses 1-based indexing with rounding.
330fn fn_substring(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
331    let s = get_string_arg(args, 0);
332    let start = get_number_arg(args, 1);
333    let has_length = args.len() >= 3;
334    let length = if has_length {
335        get_number_arg(args, 2)
336    } else {
337        f64::MAX
338    };
339
340    let start_rounded = start.round() as isize;
341    let length_rounded = length.round() as isize;
342
343    // XPath 1.0: 1-based indexing
344    let start_index = if start_rounded < 1 {
345        0
346    } else {
347        (start_rounded - 1) as usize
348    };
349    let length = if length_rounded < 0 {
350        0
351    } else {
352        length_rounded as usize
353    };
354
355    if start_index >= s.len() || length == 0 {
356        Ok(XPathValue::String(String::new()))
357    } else {
358        let end = std::cmp::min(start_index + length, s.len());
359        Ok(XPathValue::String(s[start_index..end].to_string()))
360    }
361}
362
363/// string-length(string?) — length of string.
364fn fn_string_length(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
365    let s = if args.is_empty() {
366        node_string_value(ctx.context_node)
367    } else {
368        get_string_arg(args, 0)
369    };
370    Ok(XPathValue::Number(s.len() as f64))
371}
372
373/// normalize-space(string?) — normalize whitespace.
374fn fn_normalize_space(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
375    let s = if args.is_empty() {
376        node_string_value(ctx.context_node)
377    } else {
378        get_string_arg(args, 0)
379    };
380    let normalized: Vec<&str> = s.split_whitespace().collect();
381    Ok(XPathValue::String(normalized.join(" ")))
382}
383
384/// translate(string1, string2, string3) — character translation.
385fn fn_translate(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
386    let s = get_string_arg(args, 0);
387    let from = get_string_arg(args, 1);
388    let to = get_string_arg(args, 2);
389
390    let result: String = s
391        .chars()
392        .map(|c| {
393            if let Some(pos) = from.find(c) {
394                if pos < to.len() {
395                    to.chars().nth(pos).unwrap_or(c)
396                } else {
397                    '\0' // Remove character
398                }
399            } else {
400                c
401            }
402        })
403        .filter(|&c| c != '\0')
404        .collect();
405
406    Ok(XPathValue::String(result))
407}
408
409// ═══════════════════════════════════════════════════════════════════════════════
410// Boolean Functions (§4.3)
411// ═══════════════════════════════════════════════════════════════════════════════
412
413/// boolean(object) — convert to boolean.
414fn fn_boolean(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
415    Ok(XPathValue::Boolean(get_boolean_arg(args, 0)))
416}
417
418/// not(boolean) — logical NOT.
419fn fn_not(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
420    Ok(XPathValue::Boolean(!get_boolean_arg(args, 0)))
421}
422
423/// true() — constant true.
424const fn fn_true(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
425    Ok(XPathValue::Boolean(true))
426}
427
428/// false() — constant false.
429const fn fn_false(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
430    Ok(XPathValue::Boolean(false))
431}
432
433/// lang(string) — language test.
434///
435/// # Safety
436///
437/// - `ctx.context_node` must be NULL or a valid `_xmlNode`; the walk up
438///   the `parent` chain and through each node's `properties` must only
439///   touch valid nodes and attributes whose `name` and child `content`
440///   fields are NULL or valid NUL-terminated strings; the chain must stay
441///   alive and acyclic for the duration of the call.
442fn fn_lang(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
443    let lang = get_string_arg(args, 0);
444    let mut node = ctx.context_node;
445    unsafe {
446        while !node.is_null() {
447            let mut prop = (*node).properties;
448            while !prop.is_null() {
449                let attr_name = crate::xml::string::xmlstr_to_string((*prop).name);
450                if attr_name == "lang" || attr_name == "xml:lang" {
451                    // Attribute value is stored in children (text node's content)
452                    if !(*prop).children.is_null() {
453                        let attr_val =
454                            crate::xml::string::xmlstr_to_string((*(*prop).children).content);
455                        if attr_val.to_lowercase() == lang.to_lowercase()
456                            || attr_val
457                                .to_lowercase()
458                                .starts_with(&format!("{}-", lang.to_lowercase()))
459                        {
460                            return Ok(XPathValue::Boolean(true));
461                        }
462                    }
463                }
464                prop = (*prop).next;
465            }
466            node = (*node).parent;
467        }
468    }
469    Ok(XPathValue::Boolean(false))
470}
471
472// ═══════════════════════════════════════════════════════════════════════════════
473// Number Functions (§4.4)
474// ═══════════════════════════════════════════════════════════════════════════════
475
476/// number(object?) — convert to number.
477fn fn_number(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
478    if args.is_empty() {
479        Ok(XPathValue::Number(string_to_number(&node_string_value(
480            ctx.context_node,
481        ))))
482    } else {
483        Ok(XPathValue::Number(get_number_arg(args, 0)))
484    }
485}
486
487/// sum(node-set) — sum of string->number conversions.
488fn fn_sum(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
489    let ns = get_node_set_arg(args, 0);
490    let mut total = 0.0;
491    for node in ns.iter() {
492        let s = node_string_value(node);
493        total += string_to_number(&s);
494    }
495    Ok(XPathValue::Number(total))
496}
497
498/// floor(number) — largest integer <= value.
499fn fn_floor(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
500    let n = get_number_arg(args, 0);
501    Ok(XPathValue::Number(n.floor()))
502}
503
504/// ceiling(number) — smallest integer >= value.
505fn fn_ceiling(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
506    let n = get_number_arg(args, 0);
507    Ok(XPathValue::Number(n.ceil()))
508}
509
510/// round(number) — round to nearest integer.
511///
512/// UPSTREAM-PARITY: XPath 1.0 rounds towards positive infinity for .5 cases.
513/// Rust's f64::round() rounds half away from zero, which differs for negative .5 values.
514/// See XPath 1.0 §4.4.
515fn fn_round(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
516    let n = get_number_arg(args, 0);
517    if n.is_nan() || n.is_infinite() || n == 0.0 {
518        return Ok(XPathValue::Number(n));
519    }
520    // Rust's f64::round() uses "round half away from zero"
521    // XPath 1.0 uses "round half towards positive infinity"
522    // These differ for negative numbers with .5 fractional part
523    let rust_rounded = n.round();
524    let result = if n.is_sign_negative() && (n - rust_rounded).abs() == 0.5 {
525        // XPath: move towards positive infinity (i.e., add 1.0 to the Rust result)
526        rust_rounded + 1.0
527    } else {
528        rust_rounded
529    };
530    Ok(XPathValue::Number(result))
531}
532
533// ═══════════════════════════════════════════════════════════════════════════════
534// Tests
535// ═══════════════════════════════════════════════════════════════════════════════
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    #[test]
542    fn test_true_false() {
543        let mut ctx = XPathContext::new(std::ptr::null_mut());
544        assert!(fn_true(&mut ctx, &[]).unwrap().as_boolean());
545        assert!(!fn_false(&mut ctx, &[]).unwrap().as_boolean());
546    }
547
548    #[test]
549    fn test_boolean_conversion() {
550        let mut ctx = XPathContext::new(std::ptr::null_mut());
551        assert!(fn_boolean(&mut ctx, &[XPathValue::Boolean(true)])
552            .unwrap()
553            .as_boolean());
554        assert!(!fn_boolean(&mut ctx, &[XPathValue::Boolean(false)])
555            .unwrap()
556            .as_boolean());
557    }
558
559    #[test]
560    fn test_not() {
561        let mut ctx = XPathContext::new(std::ptr::null_mut());
562        assert!(!fn_not(&mut ctx, &[XPathValue::Boolean(true)])
563            .unwrap()
564            .as_boolean());
565        assert!(fn_not(&mut ctx, &[XPathValue::Boolean(false)])
566            .unwrap()
567            .as_boolean());
568    }
569
570    #[test]
571    fn test_number_round() {
572        let mut ctx = XPathContext::new(std::ptr::null_mut());
573        assert_eq!(
574            fn_floor(&mut ctx, &[XPathValue::Number(3.7)])
575                .unwrap()
576                .as_number(),
577            3.0
578        );
579        assert_eq!(
580            fn_ceiling(&mut ctx, &[XPathValue::Number(3.2)])
581                .unwrap()
582                .as_number(),
583            4.0
584        );
585        assert_eq!(
586            fn_round(&mut ctx, &[XPathValue::Number(3.5)])
587                .unwrap()
588                .as_number(),
589            4.0
590        );
591        assert_eq!(
592            fn_round(&mut ctx, &[XPathValue::Number(-3.5)])
593                .unwrap()
594                .as_number(),
595            -3.0
596        );
597    }
598
599    #[test]
600    fn test_string_functions() {
601        let mut ctx = XPathContext::new(std::ptr::null_mut());
602        assert_eq!(
603            fn_concat(
604                &mut ctx,
605                &[
606                    XPathValue::String("a".into()),
607                    XPathValue::String("b".into()),
608                    XPathValue::String("c".into())
609                ]
610            )
611            .unwrap()
612            .as_string(),
613            "abc"
614        );
615        assert!(fn_starts_with(
616            &mut ctx,
617            &[
618                XPathValue::String("hello".into()),
619                XPathValue::String("he".into())
620            ]
621        )
622        .unwrap()
623        .as_boolean());
624        assert!(!fn_starts_with(
625            &mut ctx,
626            &[
627                XPathValue::String("hello".into()),
628                XPathValue::String("x".into())
629            ]
630        )
631        .unwrap()
632        .as_boolean());
633        assert!(fn_contains(
634            &mut ctx,
635            &[
636                XPathValue::String("hello".into()),
637                XPathValue::String("ell".into())
638            ]
639        )
640        .unwrap()
641        .as_boolean());
642        assert_eq!(
643            fn_string_length(&mut ctx, &[XPathValue::String("hello".into())])
644                .unwrap()
645                .as_number(),
646            5.0
647        );
648    }
649
650    #[test]
651    fn test_core_functions_registered() {
652        let funcs = core_functions();
653        assert!(funcs.contains_key("last"));
654        assert!(funcs.contains_key("position"));
655        assert!(funcs.contains_key("count"));
656        assert!(funcs.contains_key("string"));
657        assert!(funcs.contains_key("concat"));
658        assert!(funcs.contains_key("boolean"));
659        assert!(funcs.contains_key("not"));
660        assert!(funcs.contains_key("number"));
661        assert!(funcs.contains_key("sum"));
662        assert!(funcs.contains_key("floor"));
663        assert!(funcs.contains_key("ceiling"));
664        assert!(funcs.contains_key("round"));
665        assert!(funcs.contains_key("name"));
666        assert!(funcs.contains_key("local-name"));
667        assert_eq!(funcs.len(), 27);
668        assert!(funcs.contains_key("namespace-uri"));
669    }
670}