1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
use std::io::Read;

use html5ever::tendril::TendrilSink;
use markup5ever_rcdom::RcDom;



macro_rules! res_opt_catch {
	($val:expr) => {
		match $val? {
			Some(v) => v,
			None => return Ok(None)
		}
	};
}


pub mod value;
pub mod result;
pub mod parser;
pub mod tokens;
pub mod context;
pub mod factory;
pub mod functions;
pub mod expressions;
pub mod nodetest;

pub(crate) use context::Evaluation;
pub(crate) use value::{Node, Nodeset};
pub(crate) use tokens::{ExprToken, AxisName, NodeType, Operator, PrincipalNodeType};
pub(crate) use parser::Tokenizer;
pub(crate) use nodetest::{NodeTest, NameTest};

pub use result::{Result, Error};
pub use value::Value;
pub use factory::{Factory, Document};


pub fn parse_document<R: Read>(data: &mut R) -> Result<Document> {
	let parse: RcDom = html5ever::parse_document(RcDom::default(), Default::default())
		.from_utf8()
		.read_from(data)?;

	Ok(Document::new(parse.document.into()))
}


#[cfg(test)]
mod tests {
	use std::io::Cursor;

	pub use crate::nodetest::{NodeTest, NameTest};
	pub use crate::result::{Result, Error};
	pub use crate::value::{Value, Node, Nodeset};
	pub use crate::tokens::{ExprToken, AxisName, NodeType, Operator, PrincipalNodeType};
	pub use crate::context::Evaluation;
	pub use crate::parser::Tokenizer;
	pub use crate::factory::{Factory, Document};
	pub use crate::parse_document;


	const WEBPAGE: &str = r#"
	<!DOCTYPE html>
	<html lang="en">
		<head>
			<meta charset="UTF-8">
			<meta http-equiv="X-UA-Compatible" content="IE=edge">
			<meta name="viewport" content="width=device-width, initial-scale=1.0">
			<title>Document</title>
		</head>
		<body>
			<div class="test1">Testing 1</div>
			<span class="test2">Testing 2</span>
			<span class="test3">Testing 3</span>
			<div class="group1" aria-label="Watch Out!">
				<h1>The Group is here!</h1>
				<br/>
			</div>
		</body>
	</html>"#;

	fn evaluate(doc: &Document, search: &str) -> Option<Result<Value>> {
		doc.evaluate(search)
			.and_then(|mut v| v.next().transpose())
			.transpose()
	}

	fn assert_eq_eval<I: Into<Value>>(doc: &Document, search: &str, value: I) {
		assert_eq!(
			evaluate(doc, search),
			Some(Ok(value.into())),
			"Eval {:?}", search
		);
	}

	#[test]
	fn expressions() {
		let doc = parse_document(&mut Cursor::new(WEBPAGE)).unwrap();

		// Simple
		assert_eq_eval(&doc, r#"1 + 1"#, 2.0);
		assert_eq_eval(&doc, r#"0 - 2"#, -2.0);

		assert_eq_eval(&doc, r#"-2"#, -2.0);

		assert_eq_eval(&doc, r#"1 != 1"#, false);
		assert_eq_eval(&doc, r#"1 != 2"#, true);

		assert_eq_eval(&doc, r#"1 = 1"#, true);
		assert_eq_eval(&doc, r#"1 = 2"#, false);

		assert_eq_eval(&doc, r#"2 > 1"#, true);
		assert_eq_eval(&doc, r#"1 > 2"#, false);
		// assert_eq_eval(&doc, r#"3 > 2 > 1"#, false);
		// assert_eq_eval(&doc, r#"1 > 2 > 3"#, false);

		assert_eq_eval(&doc, r#"2 < 1"#, false);
		assert_eq_eval(&doc, r#"1 < 2"#, true);

		assert_eq_eval(&doc, r#"2 >= 1"#, true);
		assert_eq_eval(&doc, r#"1 >= 1"#, true);

		assert_eq_eval(&doc, r#"2 <= 1"#, false);
		assert_eq_eval(&doc, r#"1 <= 1"#, true);


		// NaN (using true/false since NaNs' aren't equal)
		assert_eq!(evaluate(&doc, r#"1 + A"#).and_then(|v| v.ok()?.as_number().ok()).map(|v| v.is_nan()), Some(true));
		assert_eq!(evaluate(&doc, r#"A + 1"#).and_then(|v| v.ok()?.as_number().ok()).map(|v| v.is_nan()), Some(true));
	}

	#[test]
	fn paths() {
		let doc = parse_document(&mut Cursor::new(WEBPAGE)).unwrap();

		let factory = Factory::new("//head/title", &doc, &doc.root);
		println!("{:?}", factory.produce().expect("prod").collect_nodes());

		println!("Location Paths (Unabbreviated Syntax)");
		// assert_eq!(doc.evaluate("//head/title"), Ok(Value::Nodeset(vec![].into()))); // selects the document root (which is always the parent of the document element)
		// dbg!(doc.evaluate("self::para")); // selects the context node if it is a para element, and otherwise selects nothing
		// dbg!(doc.evaluate("child::para")); // selects the para element children of the context node
		// dbg!(doc.evaluate("child::*")); // selects all element children of the context node
		// dbg!(doc.evaluate("child::text()")); // selects all text node children of the context node
		// dbg!(doc.evaluate("child::node()")); // selects all the children of the context node, whatever their node type
		// dbg!(doc.evaluate("child::chapter/descendant::para")); // selects the para element descendants of the chapter element children of the context node
		// dbg!(doc.evaluate("child::*/child::para")); // selects all para grandchildren of the context node
		// dbg!(doc.evaluate("child::para[position()=1]")); // selects the first para child of the context node
		// dbg!(doc.evaluate("child::para[position()=last()]")); // selects the last para child of the context node
		// dbg!(doc.evaluate("child::para[position()=last()-1]")); // selects the last but one para child of the context node
		// dbg!(doc.evaluate("child::para[position()>1]")); // selects all the para children of the context node other than the first para child of the context node
		// dbg!(doc.evaluate("/child::doc/child::chapter[position()=5]/child::section[position()=2]")); // selects the second section of the fifth chapter of the doc document element
		// dbg!(doc.evaluate("child::para[attribute::type=\"warning\"]")); // selects all para children of the context node that have a type attribute with value warning
		// dbg!(doc.evaluate("child::para[attribute::type='warning'][position()=5]")); // selects the fifth para child of the context node that has a type attribute with value warning
		// dbg!(doc.evaluate("child::para[position()=5][attribute::type=\"warning\"]")); // selects the fifth para child of the context node if that child has a type attribute with value warning
		// dbg!(doc.evaluate("child::chapter[child::title='Introduction']")); // selects the chapter children of the context node that have one or more title children with string-value equal to Introduction
		// dbg!(doc.evaluate("child::chapter[child::title]")); // selects the chapter children of the context node that have one or more title children
		// dbg!(doc.evaluate("child::*[self::chapter or self::appendix]")); // selects the chapter and appendix children of the context node
		// dbg!(doc.evaluate("child::*[self::chapter or self::appendix][position()=last()]")); // selects the last chapter or appendix child of the context node
		// dbg!(doc.evaluate("attribute::name")); // selects the name attribute of the context node
		// dbg!(doc.evaluate("attribute::*")); // selects all the attributes of the context node
		// dbg!(doc.evaluate("ancestor::div")); // selects all div ancestors of the context node
		// dbg!(doc.evaluate("ancestor-or-self::div")); // selects the div ancestors of the context node and, if the context node is a div element, the context node as well
		// dbg!(doc.evaluate("following-sibling::chapter[position()=1]")); // selects the next chapter sibling of the context node
		// dbg!(doc.evaluate("preceding-sibling::chapter[position()=1]")); // selects the previous chapter sibling of the context node
		// dbg!(doc.evaluate("descendant::para")); // selects the para element descendants of the context node
		// dbg!(doc.evaluate("descendant-or-self::para")); // selects the para element descendants of the context node and, if the context node is a para element, the context node as well
		// dbg!(doc.evaluate("/descendant::para")); // selects all the para elements in the same document as the context node
		// dbg!(doc.evaluate("/descendant::olist/child::item")); // selects all the item elements that have an olist parent and that are in the same document as the context node
		// dbg!(doc.evaluate("/descendant::figure[position()=42]")); // selects the forty-second figure element in the document
	}

	#[test]
	fn paths_abbreviated() {
		let doc = parse_document(&mut Cursor::new(WEBPAGE)).unwrap();

		// println!("Location Paths (Abbreviated Syntax)");
		// para selects the para element children of the context node
		// * selects all element children of the context node
		// text() selects all text node children of the context node
		// @name selects the name attribute of the context node
		// @* selects all the attributes of the context node
		// para[1] selects the first para child of the context node
		// para[last()] selects the last para child of the context node
		// */para selects all para grandchildren of the context node
		// /doc/chapter[5]/section[2] selects the second section of the fifth chapter of the doc
		// chapter//para selects the para element descendants of the chapter element children of the context node
		// //para selects all the para descendants of the document root and thus selects all para elements in the same document as the context node
		// //olist/item selects all the item elements in the same document as the context node that have an olist parent
		// . selects the context node
		// .//para selects the para element descendants of the context node
		// .. selects the parent of the context node
		// ../@lang selects the lang attribute of the parent of the context node
		// para[@type="warning"] selects all para children of the context node that have a type attribute with value warning
		// para[@type="warning"][5] selects the fifth para child of the context node that has a type attribute with value warning
		// para[5][@type="warning"] selects the fifth para child of the context node if that child has a type attribute with value warning
		// chapter[title="Introduction"] selects the chapter children of the context node that have one or more title children with string-value equal to Introduction
		// chapter[title] selects the chapter children of the context node that have one or more title children
		// employee[@secretary and @assistant] selects all the employee children of the context node that have both a secretary attribute and an assistant attribute
	}

	#[test]
	fn general_examples() {
		let doc = parse_document(&mut Cursor::new(WEBPAGE)).unwrap();

		// Simple

		assert_eq_eval(&doc, r#"contains("abc123", "bc12")"#, true);
		assert_eq_eval(&doc, r#"contains("abc123", "4")"#, false);

		assert_eq_eval(&doc, r#"concat(true, "123")"#, Value::String("123".into()));
		assert_eq_eval(&doc, r#"concat(false, "123")"#, Value::String("123".into()));
		assert_eq_eval(&doc, r#"concat(1, "123")"#, Value::String("1123".into()));
		assert_eq_eval(&doc, r#"concat("abc", "123")"#, Value::String("abc123".into()));

		// TODO: Below doesn't work.

		// assert_eq_eval(&doc, r#"starts-with("abc123", "abc")"#, true);
		// assert_eq_eval(&doc, r#"starts-with("123", 1)"#, true);

		// assert_eq_eval(&doc, r#"substring-before("abc123", "1")"#, Value::String("abc".into()));

		// assert_eq_eval(&doc, r#"substring-after("abc123", "c")"#, Value::String("123".into()));


		// Document Lookups

		assert_eq_eval(&doc, r#"//div[contains(text(), "Testing 1")]/@class"#, Value::String("test1".into()));

		// println!("Examples");
		// dbg!(doc.evaluate("//*[@id='rcTEST']//*[contains(text(), 'TEST Interactive')]/../button[2]"));
		// dbg!(doc.evaluate("//*[@id='rcTEST']//*[contains(text(), 'TEST Interactive')]/..//*[contains(text(), 'Setting')]"));
		// dbg!(doc.evaluate("//*[@id='rcTEST']//*[contains(text(), 'TEST Interactive')]/following-sibling::button"));
		// dbg!(doc.evaluate("// address[@class='ng-scope ng-isolate-scope']//div[contains('Testing') and @id='msgTitle']"));
		// dbg!(doc.evaluate("//*[@name='myForm']//table[@id='tbl_ testdm']/tbody/tr/td[6]/"));
		// dbg!(doc.evaluate("input[@value='Open RFS']"));
		// dbg!(doc.evaluate("//*[@title='Songs List Applet']//table//td[contains(text(),'Author')]"));
		// dbg!(doc.evaluate("//*[@id='parameters']//*[@id='testUpdateTime']"));
		// dbg!(doc.evaluate("//*[@id='MODEL/PLAN']/div[1]/div[2]/div[1]/div[1]/widget/section/div[1]/div/div[1]/div/div/button[1]"));
		// dbg!(doc.evaluate("//*[contains(text(),'Watch Dial')]/../div/select[@data-ng-model='context.questions[subqts.subHandleSubId]']"));
		// dbg!(doc.evaluate("//*[@id='RESEARCH/PLAN']//*[contains(@id, 'A4')]/../../following-sibling::div[1]/div[1]/span[1]/span[1]"));
		// dbg!(doc.evaluate("//*[@id='ALARMDATA']//*[contains(@id, 'AFC2')]/../../preceding-sibling::div[1]/div[1]/span[1]/span[1]"));
		// dbg!(doc.evaluate("//*[@id='RESEARCH/REVIEW']//widget/section/div[1]/div/div[2]/div[1]/div[3]/div[1]//span[@class='details']"));
		// dbg!(doc.evaluate("//a[contains(.,'Parameter Data Manual Entry')]"));
		// dbg!(doc.evaluate("//*[contains(@style,'display: block; top:')]//input[@name='daterangepicker_end']"));
		// dbg!(doc.evaluate("//*[@id='dropdown-filter-serviceTools']/following-sibling::ul/descendant::a[text()='Notepad']"));
		// dbg!(doc.evaluate("//*[@id='dropdown-filter-serviceTools']/following-sibling::ul/descendant::a[text()='Trigger Dashboard']"));
		// dbg!(doc.evaluate("//h3[text()='Internal Debrief']"));
		// dbg!(doc.evaluate("//h3[contains(text(),'Helium Level')]/following-sibling::div/label/input"));
		// dbg!(doc.evaluate("//div[div[p[contains(text(),'Status')]]]/preceding-sibling::div/div/span[3]/span"));
		// dbg!(doc.evaluate("//*[@id='COUPLING']//*[contains(text(),'COUPLE Trend')]/../div/select"));
		// dbg!(doc.evaluate("//*[@id='ffaHeaderDropdown']//a[contains(text(),'Start Workflow')]"));
	}
}