//
// xpath.rs
//
// amxml: XML processor with XPath.
// Copyright (C) 2018 KOYAMA Hiro <tac@amris.co.jp>
//
//!
//! XPath Processor (inner module) and helper structs.
//!
//! Retrieve or apply function to the nodes on XML DOM tree
//! that match the specified xpath.
//!
//! NodePtr methods <strong>eval_xpath()</strong>,
//! <strong>each_node()</strong>, <strong>get_first_node()</strong>,
//! <strong>get_nodeset()</strong>
//! accept xpath as argument.
//!
//! cf. <a href="../dom/index.html">Module amxml::dom</a> -> <a href="../dom/struct.NodePtr.html">Struct NodePtr</a> -> <a href="../dom/struct.NodePtr.html#methods">Methods</a>.
//!
//! ### Specification
//!
//! Specification of XPath by W3C: <a href="https://www.w3.org/TR/xpath-31/">XML Path Language (XPath) 3.1</a>
//!
//! Japanese translation of the document above: <a href="https://www.amris.co.jp/xp/xpath31.html">XML Path Language (XPath) 3.1: 日本語訳</a>
//!
//! ### Notes
//!
//! This processor supports only some of atomic types:
//! String, Integer, Decimal, Double, Boolean.
//! Features related to 'Type' is restrictive, since this processor
//! does not refer xml schema.
//!
//! Internally both Decimal and Double are implemented with type f64,
//! but there are some difference.
//! Decimal division <em>5.0 div 0.0</em> is error (division by zero),
//! while Double division <em>5E0 div 0E0</em> is +Infinity.
//!
//! ### Built-in functions that are implemented
//!
//! - nilled, string, data
//! - abs, ceiling, floor, round
//! - codepoints-to-string, string-to-codepoints
//! - compare, codepoint-equal
//! - concat, string-join, substring, string-length, normalize-space, upper-case, lower-case, translate
//! - contains, starts-with, ends-with, substring-before, substring-after
//! - true, false
//! - not
//! - name, local-name, namespace-uri, number, lang, root
//! - boolean, index-of
//! - empty, exists, head, tail, insert-before, remove, reverse, subsequence
//! - zero-or-one, one-or-more, exactly-one
//! - count, avg, max, min, sum
//! - position, last
//! - for-each, filter
//! - map:size, map:keys, map:contains, map:get
//! - array:size, array:get, array:flatten
//!
//! ### Features that are not implemented yet
//!
//! - treat as
//! - KindTest: SchemaElementTest | SchemaAttributeTest | DocumentTest
//! - Many built-in functions that are new in XPath 2.0 and above
//! - Collation (in built-in functions: contains, starts-with, etc.)
//! - XPath 1.0 compatible mode
//! - namespace axis (deprecated as of XPath 2.0)
//!
use std::error::Error;
use dom::*;
use xpath_impl::parser::*;
use xpath_impl::eval::*;
use xpath_impl::xitem::*;
use xpath_impl::xsequence::*;
// =====================================================================
//
impl NodePtr {
// =================================================================
// XML構文木のあるノードを起点としてxpathを評価し、
// シーケンス (典型的にはノード集合) を取得する。
/// Evaluates the xpath and returns the sequence.
///
/// # Examples
///
/// ```
/// use amxml::dom::*;
/// let xml = r#"<root><a v="x"/><a v="y"/></root>"#;
/// let doc = new_document(xml).unwrap();
/// let result = doc.eval_xpath(r#"some $a in /root/a satisfies $a/@v = "y" "#).unwrap();
/// assert_eq!(result.to_string(), "true");
/// ```
///
/// # Errors
///
/// - When syntax error or unimplemented feature in xpath.
///
pub fn eval_xpath(&self, xpath: &str) -> Result<Sequence, Box<Error>> {
let xnode = compile_xpath(&String::from(xpath))?;
let result = match_xpath(self, &xnode)?;
return Ok(new_sequence(&result));
}
// =================================================================
// XML構文木のあるノードを起点として、xpathに合致するノード集合を取得し、
// その最初のノードを返す。
/// Retrieves the first node that match with xpath.
/// Returns None if not found,
/// or when syntax error or unimplemented feature in xpath.
///
/// # Examples
///
/// ```
/// use amxml::dom::*;
/// let xml = r#"<root img="basic"><a img="a1"/><a img="a2"/></root>"#;
/// let doc = new_document(xml).unwrap();
/// let node = doc.get_first_node("//a").unwrap();
/// assert_eq!(node.attribute_value("img").unwrap(), "a1");
/// ```
///
/// # Panics
///
/// - When syntax error or unimplemented feature in xpath.
///
pub fn get_first_node(&self, xpath: &str) -> Option<NodePtr> {
let node_set_array = match self.get_nodeset(xpath) {
Ok(n) => n,
Err(_) => return None,
};
if node_set_array.len() != 0 {
return Some(node_set_array[0].rc_clone());
} else {
return None;
}
}
// =================================================================
// XML構文木のあるノードを起点として、xpathに合致する各ノードに対して
// 函数fnの処理を施す。
/// Applies func to each node that match with xpath.
///
/// # Examples
///
/// ```
/// use amxml::dom::*;
/// let xml = r#"<root img="basic"><a img="a1"/><a img="a2"/></root>"#;
/// let doc = new_document(xml).unwrap();
/// let mut img = String::new();
/// doc.each_node("/root/a", |n| {
/// img += n.attribute_value("img").unwrap().as_str();
/// });
/// assert_eq!("a1a2", img);
/// ```
///
/// # Errors
///
/// - When syntax error or unimplemented feature in xpath.
///
pub fn each_node<F>(&self, xpath: &str, mut func: F) -> Result<(), Box<Error>>
where F: FnMut(NodePtr) -> () {
let node_set_array = self.get_nodeset(xpath)?;
for node in node_set_array {
func(node.rc_clone());
}
return Ok(());
}
// =================================================================
// XML構文木のあるノードを起点として、xpathに合致するノード集合を
// 文書順で取得する。
/// Retrieves all nodes that match with xpath in document order.
///
/// # Examples
///
/// ```
/// use amxml::dom::*;
/// let xml = r#"<root img="basic"><a img="a1"/><a img="a2"/></root>"#;
/// let doc = new_document(xml).unwrap();
/// let nodeset = doc.get_nodeset("//a").unwrap();
/// assert_eq!(nodeset[0].attribute_value("img").unwrap(), "a1");
/// assert_eq!(nodeset[1].attribute_value("img").unwrap(), "a2");
/// ```
///
/// # Errors
///
/// - When syntax error or unimplemented feature in xpath.
///
pub fn get_nodeset(&self, xpath: &str) -> Result<Vec<NodePtr>, Box<Error>> {
let xnode = compile_xpath(&String::from(xpath))?;
let result = match_xpath(self, &xnode)?;
let nodeset = result.to_nodeset();
return Ok(nodeset);
}
}
// =====================================================================
/// Sequence: return value type of NodePtr#eval_xpath().
/// This is an ordered collection of zero or more items.
///
#[derive(Debug)]
pub struct Sequence {
seq: XSequence,
}
// ---------------------------------------------------------------------
//
fn new_sequence(xseq: &XSequence) -> Sequence {
return Sequence{seq: xseq.clone()};
}
// =====================================================================
/// Item: either an atomic value or a node.
///
pub struct Item {
item: XItem,
}
// ---------------------------------------------------------------------
//
fn new_item(xitem: &XItem) -> Item {
return Item{item: xitem.clone()};
}
// =====================================================================
//
impl Sequence {
// -----------------------------------------------------------------
/// Converts the sequence to the string, like:
///
/// (<elem attr="val">, 1, 2.0, 3e0, "str", true)
///
/// This is a sequence (ordered collection of items) of length 6.
/// Here, the first item is a node.
/// 1 is Integer item, 2.0 (with decimal point) is Decimal item,
/// 3e0 (scientific notation) is Double item.
/// And "str" is String item, true is Boolean item.
///
pub fn to_string(&self) -> String {
return self.seq.to_string();
}
// -----------------------------------------------------------------
/// Returns the length of sequence.
///
pub fn len(&self) -> usize {
return self.seq.len();
}
// -----------------------------------------------------------------
/// Returns an N'th item in the sequence.
/// pos must be less than self.len().
///
pub fn get_item(&self, pos: usize) -> Item {
let xitem = self.seq.get_item(pos);
return new_item(xitem);
}
}
// =====================================================================
//
impl Item {
// -----------------------------------------------------------------
/// Converts the item to the string.
///
pub fn to_string(&self) -> String {
return self.item.to_string();
}
// -----------------------------------------------------------------
/// Returns item as NodePtr (when this item is a node).
///
pub fn as_nodeptr(&self) -> Option<NodePtr> {
return self.item.as_nodeptr();
}
}
// =====================================================================
//
#[cfg(test)]
mod test {
use super::*;
use xpath_impl::helpers::compress_spaces;
use xpath_impl::helpers::subtest_xpath;
use xpath_impl::helpers::subtest_eval_xpath;
// -----------------------------------------------------------------
// - child::para は文脈ノードの子の para 要素すべてを選択する。
// - para は文脈ノードの para 子要素すべてを選択する。
// - child::* は文脈ノードの子要素すべてを選択する。
// - * は文脈ノードの子要素すべてを選択する。
//
#[test]
fn test_sample_01() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<chap base="base">
<para img="春"/>
<div img="夏"/>
<para img="秋"/>
<div img="冬"/>
</chap>
</root>
"#);
subtest_xpath("01", &xml, false, &[
( "child::para", "春秋" ),
( "para", "春秋" ),
( "child::*", "春夏秋冬" ),
( "*", "春夏秋冬" ),
]);
}
// -----------------------------------------------------------------
// - child::text() は文脈ノードの、すべての子テキストノードを選択する。
// - text() は文脈ノードの子テキストノードすべてを選択する。
//
#[test]
fn test_sample_02() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<chap base="base">
<para img="春">はる</para>
なつ
<para img="秋">あき</para>
ふゆ
</chap>
</root>
"#);
subtest_xpath("02", &xml, true, &[
( "child::text()", "なつふゆ" ),
( "text()", "なつふゆ" ),
// -------------------------------------
( "text()[preceding-sibling::*[1]/@img = '春']", "なつ" ),
( "text()[following-sibling::*[1]/@img = '秋']", "なつ" ),
]);
}
// -----------------------------------------------------------------
// - child::node() はノード型に関係なく、文脈ノードのすべての子ノードを選択する。
//
#[test]
fn test_sample_03() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<chap base="base">
<para img="春">はる</para>
なつ
<sub img="秋">あき</sub>
ふゆ
<!--季節-->
</chap>
</root>
"#);
subtest_xpath("03", &xml, true, &[
( "child::node()", "なつふゆ季節" ),
( "node()", "なつふゆ季節" ),
// -------------------------------------
( "text()", "なつふゆ" ),
( "comment()", "季節" ),
]);
subtest_xpath("03", &xml, false, &[
( "child::node()", "春秋" ),
( "node()", "春秋" ),
]);
}
// -----------------------------------------------------------------
// - attribute::name は文脈ノードの name 属性を選択する。
// - @name は文脈ノードの name 属性を選択する。
// - attribute::* は文脈ノードのすべての属性を選択する。
// - @* は文脈ノードの属性すべてを選択する。
//
#[test]
fn test_sample_04() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<chap base="base" name="四季" ruby="しき" ns:e="seasons">
<para img="春">はる</para>
</chap>
</root>
"#);
subtest_xpath("04", &xml, true, &[
( "attribute::name", "四季" ),
( "@name", "四季" ),
( "attribute::*", "base四季しきseasons" ),
( "@*", "base四季しきseasons" ),
// ----------------------------------------------
( "attribute::ns:e", "seasons" ),
( "@ns:e", "seasons" ),
( "attribute::ns:*", "seasons" ),
( "@ns:*", "seasons" ),
]);
}
// -----------------------------------------------------------------
// - descendant::para は文脈ノードの子孫の para 要素すべてを選択する。
// - .//para は文脈ノードの para 子孫要素すべてを選択する。
//
#[test]
fn test_sample_05() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<para base="base" img="四季">
<para img="春">はる</para>
<div img="夏">
<para img="夏" />
<span>
<para img="秋" />
</span>
</div>
</para>
</root>
"#);
subtest_xpath("05", &xml, false, &[
( "descendant::para", "春夏秋" ),
( ".//para", "春夏秋" ),
]);
}
// -----------------------------------------------------------------
// - ancestor::div は文脈ノードの先祖の div 要素すべてを選択する。
// - ancestor-or-self::div は文脈ノードの先祖の div 要素すべてに加え、文脈ノード自身が div 要素ならば自身も選択する。
//
#[test]
fn test_sample_06() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<div img="四季">
<div img="春">はる</div>
<section img="夏">
<div base="base" img="秋" />
<span>
<div img="冬" />
</span>
</section>
</div>
</root>
"#);
subtest_xpath("06", &xml, false, &[
( "ancestor::div", "四季" ),
( "ancestor-or-self::div", "四季秋" ),
]);
}
// -----------------------------------------------------------------
// - descendant-or-self::para は文脈ノードの子孫の para 要素すべてに加え、文脈ノード自身が para 要素ならば自身も選択する。
//
#[test]
fn test_sample_07() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<para base="base" img="四季">
<para img="春">はる</para>
<section img="夏">
<para img="秋" />
<span>
<para img="冬" />
</span>
</section>
</para>
</root>
"#);
subtest_xpath("07", &xml, false, &[
( "descendant-or-self::para", "四季春秋冬" ),
// -----------------------------------------------------
( ".//para", "春秋冬" ),
( "..//para", "四季春秋冬" ),
]);
}
// -----------------------------------------------------------------
// - self::para は文脈ノード自身が para 要素ならば自身を選択し、そうでなければ何も選択しない。
//
#[test]
fn test_sample_08() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<para base="base" img="四季">
<para img="春">はる</para>
</para>
</root>
"#);
subtest_xpath("08a", &xml, false, &[
// ( "self::para", "四季" ),
]);
let xml2 = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<section base="base" img="四季">
<para img="春">はる</para>
</section>
</root>
"#);
subtest_xpath("08b", &xml2, false, &[
( "self::para", "" ),
]);
}
// -----------------------------------------------------------------
// - child::chapter/descendant::para は文脈ノードの子の chapter 要素すべての子孫の para 要素すべてを選択する。
// - chapter//para は文脈ノードの chapter 子要素の para 子孫要素すべてを選択する。
// - child::*/child::para は文脈ノードの孫(子の子)の para 要素すべてを選択する。
// - */para は文脈ノードの para 孫要素すべてを選択する。
//
#[test]
fn test_sample_09() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<division base="base" img="四季">
<chapter img="chap-1">
<para img="春">はる</para>
<note img="梅雨">つゆ</note>
<span>
<para img="夏">なつ</para>
</span>
</chapter>
<chapter img="chap-2">
<para img="秋">あき</para>
<span>
<para img="冬">ふゆ</para>
</span>
</chapter>
</division>
</root>
"#);
subtest_xpath("09", &xml, false, &[
( "child::chapter/descendant::para", "春夏秋冬" ),
( "chapter//para", "春夏秋冬" ),
( "child::*/child::para", "春秋" ),
( "*/para", "春秋" ),
]);
}
// -----------------------------------------------------------------
// - / は文書ルートを選択する(文書ルートは常に文書要素の親となる)。
//
#[test]
fn test_sample_10() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<?style-sheet alt="1" src="sample.css"?>
<root>
<div base="base" img="四季">
<chapter img="chap-1" />
</div>
</root>
"#);
let doc = new_document(&xml).unwrap();
let base_node = doc.get_first_node(r#"//*[@base="base"]"#).unwrap();
let document_root = base_node.get_first_node("/").unwrap();
let pi = document_root.get_first_node("processing-instruction()").unwrap();
assert_eq!(pi.name(), "style-sheet");
}
// -----------------------------------------------------------------
// - /descendant::para は文脈ノードと同じ文書内の para 要素すべてを選択する。
// - //para は文書ルートの para 子孫要素すべてを選択する。
//
#[test]
fn test_sample_11() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<para base="base" img="四季">
<para img="春">はる</para>
<div img="夏">
<para img="夏" />
<span>
<para img="秋" />
</span>
</div>
</para>
</root>
"#);
subtest_xpath("11", &xml, false, &[
( "/descendant::para", "四季春夏秋" ),
( "//para", "四季春夏秋" ),
]);
}
// -----------------------------------------------------------------
// - /descendant::olist/child::item は文脈ノードと同じ文書内にある item 要素のうち、 olist 要素を親に持つものすべてを選択する。
// - //olist/item は、文脈ノードと同じ文書内にある item 要素のうち、olist 要素を親に持つものすべてを選択する。
//
#[test]
fn test_sample_12() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<para base="base" img="四季" />
<div>
<item img="x1">XX</item>
</div>
<olist>
<item img="春" />
<item img="夏" />
<ulist>
<item img="秋" />
</ulist>
</olist>
</root>
"#);
subtest_xpath("12", &xml, false, &[
( "/descendant::olist/child::item", "春夏" ),
( "//olist/item", "春夏" ),
]);
}
// -----------------------------------------------------------------
// - child::para[position()=1] は文脈ノードの子の para 要素のうち、最初のものを選択する。
// - para[1] は文脈ノードの最初の para 子要素を選択する。
// - child::para[position()=last()] は文脈ノードの子の para 要素のうち、最後のものを選択する。
// - para[last()] は文脈ノードの最後の para 子要素を選択する。
// - child::para[position()=last()-1] は文脈ノードの最後から 2 番目の para 子要素を選択する。
// - child::para[position()>1] は文脈ノードの para 子要素のうち、最初のものを除くすべてを選択する。
//
#[test]
fn test_sample_13() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<para base="base" img="四季">
<note img="きせつ"/>
<para img="春">はる</para>
<para img="夏">なつ</para>
<para img="秋">あき</para>
<note img="季節"/>
</para>
</root>
"#);
subtest_xpath("13", &xml, false, &[
( "child::para[position()=1]", "春" ),
( "para[1]", "春" ),
( "child::para[position()=last()]", "秋" ),
( "para[last()]", "秋" ),
( "child::para[position()=last()-1]", "夏" ),
( "child::para[position()>1]", "夏秋" ),
]);
}
// -----------------------------------------------------------------
// - following-sibling::chapter[position()=1] は文脈ノードの次の chapter 同胞要素を選択する。
// - preceding-sibling::chapter[position()=1] は文脈ノードの前の chapter 同胞要素を選択する。
//
#[test]
fn test_sample_14() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<para img="四季">
<chapter img="甲"/>
<chapter img="乙"/>
<chapter img="丙"/>
<chapter img="丁" base="base"/>
<chapter img="戊"/>
<chapter img="己"/>
<chapter img="庚"/>
<chapter img="辛"/>
<chapter img="壬"/>
<chapter img="癸"/>
</para>
</root>
"#);
subtest_xpath("14", &xml, false, &[
( "following-sibling::chapter[position()=1]", "戊" ),
( "preceding-sibling::chapter[position()=1]", "丙" ),
]);
}
// -----------------------------------------------------------------
// - /descendant::figure[position()=42] は文書内の 42 番目の figure 要素を選択する。
//
#[test]
fn test_sample_15() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<para img="p1">
<figure img="甲"/>
<span>
<figure img="乙"/>
</span>
</para>
<para base="base" img="p2">
<figure img="丙"/>
<span>
<figure img="丁"/>
<figure img="戊"/>
</span>
</para>
<para img="p3">
<figure img="己"/>
<figure img="庚"/>
<figure img="辛"/>
<figure img="壬"/>
<figure img="癸"/>
</para>
</root>
"#);
subtest_xpath("15", &xml, false, &[
( "/descendant::figure[position()=4]", "丁" ),
( "/descendant::figure[position()=6]", "己" ),
]);
}
// -----------------------------------------------------------------
// - /child::doc/child::chapter[position()=5]/child::section[position()=2] は文書要素 doc の 5 番目の chapter 子要素の、2 番目の section 子要素を選択する。
// - /doc/chapter[5]/section[2] はルートノードの doc 子要素(文書要素)の 5 番目の chapter 子要素の 2 番目の section 子要素を選択する。
//
#[test]
fn test_sample_16() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<doc>
<chapter img="c1">
</chapter>
<chapter img="c2">
<section img="S21" base="base"/>
</chapter>
<chapter img="c3">
</chapter>
<chapter img="c4">
</chapter>
<chapter img="c5">
<section img="S51"/>
<section img="S52"/>
<section img="S53"/>
</chapter>
<chapter img="c6">
</chapter>
</doc>
"#);
subtest_xpath("20", &xml, false, &[
( "/child::doc/child::chapter[position()=5]/child::section[position()=2]", "S52" ),
( "/doc/chapter[5]/section[2]", "S52" ),
]);
}
// -----------------------------------------------------------------
// - child::para[attribute::type="warning"] は文脈ノードの para 子要素のうち、 type 属性の値が warning のものすべてを選択する。
// - para[@type="warning"] は文脈ノードの para 子要素のうち、 type 属性の値が warning になるものすべてを選択する。
// - child::para[attribute::type='warning'][position()=5] は文脈ノードの para 子要素で type 属性の値が warning のものから、 5 番目の要素を選択する。
// - para[@type="warning"][5] は文脈ノードの para 子要素のうち、 type 属性の値が warning になるものの中から 5 番目のものを選択する。
// - child::para[position()=5][attribute::type="warning"] は、文脈ノードの 5 番目の para 子要素の type 属性の値が warning であれば、その para 子要素を選択する。
// - para[5][@type="warning"] は、文脈ノードの 5 番目の para 子要素の type 属性の値が warning ならば、その子要素を選択する。
//
#[test]
fn test_sample_17() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<doc>
<chapter img="c1" base="base">
<para img="甲" type="warning"/>
<para img="乙"/>
<para img="丙"/>
<para img="丁" type="warning"/>
<para img="戊" type="warning"/>
<para img="己"/>
<para img="庚" type="warning"/>
<para img="辛" type="warning"/>
<para img="壬"/>
<para img="癸" type="warning"/>
</chapter>
</doc>
"#);
subtest_xpath("17", &xml, false, &[
( "child::para[attribute::type='warning']", "甲丁戊庚辛癸" ),
( "para[@type='warning']", "甲丁戊庚辛癸" ),
( "child::para[attribute::type='warning'][position()=5]", "辛" ),
( "para[@type='warning'][5]", "辛" ),
( "child::para[position()=5][attribute::type='warning']", "戊" ),
( "para[5][@type='warning']", "戊" ),
]);
}
// -----------------------------------------------------------------
// - child::chapter[child::title='Introduction'] は、文脈ノードの chapter 子要素のうち、 文字列値 が Introduction になる title 要素を子要素に1個以上持つものすべてを選択する。
// - chapter[title="Introduction"] は、文脈ノードの chapter 子要素のうち、 文字列値 が Introduction になる title 子要素を1個以上持つものすべてを選択する。
// - child::chapter[child::title] は、文脈ノードの chapter 子要素のうち、 title 子要素を1個以上持つものすべてを選択する。
// - chapter[title] は文脈ノードの chapter 子要素のうち、 title 子要素を1個以上持つものすべてを選択する。
//
#[test]
fn test_sample_18() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<doc base="base">
<chapter img="春">
</chapter>
<chapter img="夏">
<title><bold>I</bold>ntroduction</title>
</chapter>
<chapter img="秋">
<title>NextStep</title>
</chapter>
<chapter img="冬">
</chapter>
</doc>
</root>
"#);
subtest_xpath("18", &xml, false, &[
( "child::chapter[child::title='Introduction']", "夏" ),
( "chapter[title='Introduction']", "夏" ),
( "child::chapter[child::title]", "夏秋" ),
( "chapter[title]", "夏秋" ),
]);
}
// -----------------------------------------------------------------
// - child::*[self::chapter or self::appendix] は、文脈ノードの chapter 子要素すべてと appendix 子要素すべてを選択する。
// - child::*[self::chapter or self::appendix][position()=last()] は、文脈ノードの chapter 子要素すべてと appendix 子要素すべてを併せた中から最後のものを選択する。
//
#[test]
fn test_sample_19() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<doc base="base">
<chapter img="春">
</chapter>
<note img="梅雨">ばいう</note>
<chapter img="夏">
なつ
</chapter>
<note img="重陽" />
<chapter img="秋">
あき
</chapter>
<chapter img="冬">
</chapter>
<appendix img="正月">
</appendix>
<appendix img="四季">
</appendix>
</doc>
</root>
"#);
subtest_xpath("19", &xml, false, &[
( "child::*[self::chapter or self::appendix]", "春夏秋冬正月四季" ),
( "child::*[self::chapter or self::appendix][position()=last()]", "四季" ),
]);
}
// -----------------------------------------------------------------
// - . は文脈ノードを選択する。
// - .. は文脈ノードの親ノードを選択する。
// - ../@lang は文脈ノードの親ノードの lang 属性を選択する。
//
#[test]
fn test_sample_20() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root lang="en" img="root">
<doc lang="ja" img="四季">
<chapter base="base" lang="ja-JP" img="春">
</chapter>
</doc>
</root>
"#);
subtest_xpath("20", &xml, false, &[
( ".", "春" ),
( "..", "四季" ),
]);
subtest_xpath("20", &xml, true, &[
( "../@lang", "ja" ),
]);
}
// -----------------------------------------------------------------
// - employee[@secretary and @assistant] は文脈ノードの employee 子要素のうち、 secretary 属性と assistant 属性の両方を持つものすべてを選択する。
//
#[test]
fn test_sample_21() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<hr base="base">
<employee img="John" secretary="t">John</employee>
<employee img="Jack" assistant="t">Jack</employee>
<employee img="Betty" secretary="t" assistant="t">Betty</employee>
<employee img="Tom" president="t">Tom</employee>
</hr>
</root>
"#);
subtest_xpath("21", &xml, false, &[
( "employee[@secretary and @assistant]", "Betty" ),
]);
}
// -----------------------------------------------------------------
// 注記: ロケーションパス //para[1] はロケーションパス /descendant::para[1] と同じではない。 後者は、最初の para 子孫要素を選択する一方、前者は、その要素の親にとって最初の para 子要素になるような para 子孫要素すべてを選択する。
//
#[test]
fn test_sample_51a() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<chap img="上" base="base">
<para img="春"/>
<para img="夏"/>
</chap>
<chap img="下">
<para img="秋"/>
<para img="冬"/>
</chap>
</root>
"#);
subtest_xpath("51a", &xml, false, &[
( "//para[1]", "春秋" ),
( "/descendant::para[1]", "春" ),
]);
}
// -----------------------------------------------------------------
// 「//」と文書順の関係。
// 注記: ロケーションパス //para[1] はロケーションパス /descendant::para[1] と同じではない。 後者は、最初の para 子孫要素を選択する一方、前者は、その要素の親にとって最初の para 子要素になるような para 子孫要素すべてを選択する。
//
#[test]
fn test_sample_51b() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<file base="base">
<body>
<unit img="A"/>
<group>
<unit img="B"/>
<unit img="C"/>
</group>
<unit img="D"/>
<unit img="E"/>
</body>
</file>
"#);
subtest_xpath("51b", &xml, false, &[
( "body//unit", "ABCDE" ),
( "body//unit[2]", "CD" ), // [X]
( "body/descendant-or-self::*/unit[2]", "CD" ), // [X] と同じ
( "body/descendant-or-self::unit[2]", "B" ), // [Y]: [X] とは違う
( "body/descendant::unit[2]", "B" ), // [Z]: [Y] と実質的に同じ
( "body//unit[3]", "E" ),
( "(body//unit)[1]", "A" ),
]);
// xml に現れる <unit> すべてのうち、文書順で2番目 (B) を得たければ、
// [X] ではなく [Y] のように書かなければならない。
}
// -----------------------------------------------------------------
// 注記: 述語 の意味は、適用する軸に大きく依存する。 例えば preceding::foo[1] は、述語 [1] を適用する軸が preceding 軸になるので、逆文書順で最初の foo 要素を返す。 一方、 (preceding::foo)[1] では、述語 [1] を適用する軸が child 軸になるので、文書順で最初の foo 要素を返す。
//
#[test]
fn test_sample_52() {
let xml = compress_spaces(r#"
<?xml version='1.0' encoding='UTF-8'?>
<root>
<foo img="上">
<foo img="春"/>
<baa img="夏"/>
</foo>
<foo img="下">
<foo img="秋" base="base"/>
<baa img="冬"/>
</foo>
</root>
"#);
subtest_xpath("52", &xml, false, &[
( "preceding::foo[1]", "春" ),
// preceding 軸の場合、先祖ノードは除外されるので、
// 「夏、春、上、xml」の順にたどる。
// そのうち foo は「春、上」であり、その1番なので
// 「春」になる。
// -----------------------------------------------------
( "(preceding::foo)[1]", "上" ),
// 同様であるが、この場合は正順にたどって1番なので
// 「上」になる。
// =====================================================
( "/descendant::foo[preceding::foo[1]/@img = '下']", "" ),
( "/descendant::foo[preceding::foo[1]/@img = '春']", "下秋" ),
// [1] は逆順で1番
// 「/descendant::foo」で見つかる、上春下秋のうち、
// 上: 「preceding::foo」が空なのでその [1] も空
// 春: 「preceding::foo」が空なのでその [1] も空
// - 「上」はancestorなのでprecedingに入らない
// 下: 「preceding::foo」は「春上」、その [1] は「春」
// 秋: 「preceding::foo」は「春上」、その [1] は「春」
( "/descendant::foo[preceding::foo[2]/@img = '上']", "下秋" ),
// 同様にして、
// 下: 「preceding::foo」は「春上」、その [2] は「上」
// 秋: 「preceding::foo」は「春上」、その [2] は「上」
( "/descendant::foo[(preceding::foo)[1]/@img = '上']", "下秋" ),
// [1] は正順で1番
// 「/descendant::foo」で見つかる、上春下秋のうち、
// 上: 「preceding::foo」が空なのでその [1] も空
// 春: 「preceding::foo」が空なのでその [1] も空
// - 「上」はancestorなのでprecedingに入らない
// 下: 「preceding::foo」は「上春」、その [1] は「上」
// 秋: 「preceding::foo」は「上春」、その [1] は「上」
( "/descendant::foo[(preceding::foo)[2]/@img = '春']", "下秋" ),
// 同様にして、
// 下: 「preceding::foo」は「上春」、その [2] は「春」
// 秋: 「preceding::foo」は「上春」、その [2] は「春」
( "/descendant::foo[(preceding::foo)[1]/@img = '春']", "" ),
]);
}
// -----------------------------------------------------------------
//
#[test]
fn test_basic_xpath() {
let xml = compress_spaces(r#"
<root base="base">
<student>
<name>George</name>
<exam subject="math" point="70"/>
<exam subject="science" point="90"/>
</student>
<student>
<name>Harry</name>
<exam subject="math" point="85"/>
<exam subject="science" point="95"/>
</student>
<student>
<name>Ivonne</name>
<exam subject="math" point="60"/>
<exam subject="science" point="75"/>
</student>
</root>
"#);
subtest_eval_xpath("basic_xpath", &xml, &[
( r#"
for $student in /root/student return
($student/name/text(),
every $exam in $student/exam satisfies number($exam/@point) > 80)
"#,
r#"(George, false, Harry, true, Ivonne, false)"# ),
]);
}
}